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.