jint

How to use

Who runs jint

jint is meant to be run by your agent, not by you. The agent writes code, runs jint over it, reads the findings, and fixes them before it hands the work back. That is how it checks its own work.

You still have a job: set the rules to your liking. In the run UI you edit a rule’s question and the code it reads, classify it, and compare the readings until the rule judges what you mean. You do not write the rule’s logic, only the question and the evidence.

Once a rule is set, the agent has to meet it. A rule is stronger than a rubric. A rubric is text the agent reads and applies by its own judgment, and nothing checks that it did. A rule is run by jint after the work, in CI or in a pre-commit hook. It returns an action for the agent to take: the rule’s handle gives a severity and a message that says what to do. An error fails the hook and sets the exit code, so the agent has to act on it before the work goes through.

The goal is rules calibrated to your code, so that they fire only when they need to. A rule that reads wrongly blocks good work or lets bad work pass. See Calibration for how to test a rule and edit it until it is right.

Ways to run it

Every form exists as an SDK option first; the CLI is a thin client of it.

One snippet from stdin

cat snippet.ts | jint run str -                # extensionless: hint the grammar
cat snippet.go | jint run str - --language go

The str noun declares piped stdin as one str unit (identity str:stdin). Its own inventory is derived; no grammar is ever inferred and it never WALK_FAILEDs. SDK: lint({ str: { content } }).

Directory sweep

jint run src/

The input kind is inferred by shape (exists-as-dir -> dir). The walk turns the tree into file units (node_modules, .git, gitignored paths, binaries skipped; exclude prunes on top); walk happens only where an inventory-scoped rule targets elements. Unsupported extensions are sweep coverage, not errors: parsing ts, tsx, py, go · not parsed (no grammar): .md, .json.

Single file

jint run src/a.ts

Explicit path: the user asked for THIS file, so silence would read as “nothing wrong” — an unknown extension fails MISSING_GRAMMAR naming the supported grammars (the walker skips non-source silently; explicit does not).

Path list

git diff --name-only HEAD~1 | jint run paths -
jint run paths - < list.txt

One flat list of paths, one per line — the caller’s own VCS pipeline; jint never shells out and never parses a VCS. Entries are the caller’s bytes used verbatim; a malformed list is a named LIST_* setup error; a listed path that resolves to nothing is a not-in-the-walk note, never a silent skip. SDK: lint({ paths: { content } }).

One element

jint run src/a.ts@L58                # element whose span contains line 58: one classifier call
jint run src/a.ts@L40-80             # every element fully enclosed in the range
jint run src/a.ts@L40-80 --rule is_function_srp

The finding’s @L is pre-edit; re-check at the element’s CURRENT line, or re-run whole.

Custom rules

Custom rules are authored INLINE in a TS config’s rules array (the config takes the same keys a cconfig file takes): a declarative rule composed next to the shipped domains, or a callback rule for the cases data can’t express.

cat > jint.config.ts <<'EOF'
import { rules as naming } from "jint:rules/naming";
import { defineConfig } from "jint:sdk";

export default defineConfig({
  rules: { use: [
    ...naming,
    {
      name: "is_error_message_actionable",
      target: "functions",
      classify: (self) => ({
        instructions: "…",
        criteria: { true: "…", false: "…" },
        state: { source: self.source },
      }),
      handle: { warn: { greater_than: 0.5 } },
    },
  ] },
});
EOF

Handlers return data; jint, never the rule, calls the classifier. Names are global: a callback rule re-declaring a declarative rule’s name is a fatal load error.

Package-scoped configs

cd packages/app && bun run check:jint

A package keeps its own jint.config.json (input, rule selection, excludes scoped to that package) and a check script that runs the CLI with it — a repo of packages is a repo of configs, not one config with path algebra.