Say what you want done — and what must stay untouched. cdir runs the work, watches what moves, and tells you what it checked — and what it couldn't.
You write down the job: the goal, the files in scope, and the promises that must hold — "these APIs don't change," "these tests still pass," "no new dependencies." That record is a Lock: a plain YAML file in your repo, one you can read and edit.
Then the work happens inside it — a script, a coding agent, your own
edits. cdir watches every file the work touches, blocks nothing on its
own, and afterwards shows you exactly what happened: what changed, what
stayed inside the agreement, which promises it could verify, and which
nobody could verify. The answer comes in plain English first; the
evidence sits underneath for when you want it.
cdir lock new "make the preview feel instant, don't touch export"
cdir run IL-0001 -- <the command that does the work>
cdir report IL-0001
It is for anyone who lets an agent — or a teammate, or themselves on a tired Tuesday — edit a repository, and wants the scope kept and the claims shown, not narrated.
You state what must be true. cdir finds out whether it is. It does not take an agent's word for it: it maps the repository, computes what a change could affect, holds the work to the scope you approved, and marks every claim with how it was checked.
Code Director uses more tokens than a raw agent. In the pilot benchmark (9 paired tasks), directed runs cost +72% total tokens and bought 9/9 correctness against 7/9 raw — two tasks the unguided agent genuinely misunderstood. The premium is prompt-side: skill instructions, the scope draft, richer working context. The fence itself — hooks, locks, verification, reports — is deterministic code and costs zero tokens.
This is a correctness-for-tokens trade, not a free lunch. For trivial one-file edits the skill's fast lane keeps the ceremony small; for anything that would hurt if built wrong, one avoided misunderstanding usually costs more than the premium. Data and its limits: intent-experiment/EXPERIMENT.md.
Every claim the system makes is one of:
| Class | Meaning | Produced by |
|---|---|---|
| Measured | Observed differentially, pre and post, same harness | test runs, output stdout hashes, verifyCommand |
| Proven | Structurally guaranteed, no execution needed | signature hash match, manifest/lockfile diff, clean typecheck |
| Asserted | Claimed without a differential check — always labeled | findings, any claim that lost its artifact reference |
| Unchecked | No check exists or none could run — named, with the reason | custom clauses, missing baselines, unavailable compilers |
Conservatism is policy: anything not differentially observed is Asserted at
best. A claim without an artifact reference is automatically Asserted —
enforced in code (enforceArtifactRule), not by discipline. The Unchecked
bucket is always rendered, even when empty; a report that hides it is lying.
Two honesty limits worth knowing:
- Signature evidence is a syntactic declaration surface — export and
visibility status, type parameters, the parameter list, return-type
annotations, and a type's member surface (methods with their visibility;
fields by annotation or initializer KIND). Unannotated arrow consts carry
their initializer's kind, so
() => 1→() => "1"moves the hash while() => 1→() => 2stays held; formatting-only edits (whitespace) never move it. Plain value consts still exclude the value, by design. (PARSER_VERSIONwas bumped, so old indexes re-parse.) - Baselines are tamper-evident, not tamper-proof. The baseline file's
sha256 is recorded in the run record at capture; verification re-hashes
and, on mismatch, marks every baseline-dependent check Unchecked —
"baseline modified during execution — run invalid" — and fails the run.
But
.codedirector/baselines/is still inside the tree the executed command can write: a command that rewrites the baseline and the record could evade this. True oracle separation (baseline outside the writable tree / read-only verifier process) remains a later phase.
npm install
npm run build
npm link # optional: puts `cdir` on your PATHRequires Node ≥ 20. No native builds: parsing is pure WASM.
Installing cdir puts the tools on your machine — it does not tell any AI
to use them. An agent only knows what its own instructions and conversation
contain, so you must attach one of these hooks (any one works; together is
best):
1. MCP — the toolbox. Run cdir mcp as an MCP server in your agent's
config. The agent gains nine tools (repo_map, blast_radius, lock_draft,
lock_amend, lock_check, lock_activate, run_locked, report, undo).
lock_draft and lock_amend take the promises with the scope — KEEP clauses,
verifyCommand, accept, budget and deny — so an MCP-driven task carries the
same contract as the CLI. Every call
leaves a line in .codedirector/runs/mcp.log — when it started, which tool, which
root, how long it took, and how it ended — so a call that appears to hang can
be read about afterwards instead of guessed at. Arguments are not recorded.
The log rotates at 5 MB, keeping one previous file.
Long run_locked commands and checks, and standalone report verification,
run in worker threads. Ping and tool listing remain available. Calls carrying
an MCP progress token receive elapsed-time liveness immediately and every five
seconds, without a completion percentage. Clients must opt into resetting
their request timeout on progress (SDK: resetTimeoutOnProgress: true);
progress cannot extend an absolute deadline or a client that ignores it.
Each server permits one tool call per canonical Git worktree at a time;
competing calls return “repository busy” promptly, including undo and lock
changes. Symlinks and project subfolders share that guard; other worktrees
remain available. This guard does not coordinate separate servers or CLI
processes. Cancellation or client timeout stops waiting, not the command:
the guard stays held until completion and the run record remains available
through report. Do not retry a timed-out command blindly. Closing input
cleanly drains active work before exit; forcibly killing the server cannot
promise completion or recovery. The locked command has no server-imposed
deadline: if it never exits, its guard and clean-disconnect drain wait
indefinitely. Check timeouts still apply to verification probes. Worker
errors return errors, never success. Other tools still run on the parent
thread; a large indexing request in another worktree can delay liveness.
Agent CLI CLI setup — add to ~/.agents/mcp.json:
{
"mcpServers": {
"codedirector": {
"command": "cdir",
"args": ["mcp"]
}
}
}Other MCP-capable agents (Claude Code, Gemini CLI, …) take the same
cdir mcp command in their own MCP config format. Step-by-step setup
guides: Gemini CLI ·
Grok Build (official xAI CLI) ·
Grok CLI (community build; contracts align).
2. Skill — the rulebook. Copy skills/codedirector/SKILL.md
into your agent's skills directory (e.g. .claude/skills/codedirector/ for
Claude Code, or your agent's equivalent). The agent reads it at session start
and follows the workflow without being reminded.
3. One-off — just tell it. No setup at all; paste this into the chat:
This project has cdir installed. Read its README first, then follow its
workflow for my request: draft a Vibe Check and wait for my approval
before editing, work inside the approved scope, finish with cdir report.
Whichever hook you use, the flow from your side stays the same: you say what you want, the agent shows you the scope before touching code, and you get a Change Report afterwards.
Swift is indexed, not merely fenced off: .swift files are walked and
parsed like any other source, so anchors, the repo map and blast radius see
an Apple project's own symbols. A 56k-line macOS app reads as 294 files, 213
of them Swift, in about a second and a half. Cross-file call edges are
weaker there than in TypeScript — every Swift import names a module rather
than a path, so import-based resolution cannot fire and same-file and
unique-name resolution carry it.
Swift / SwiftUI repos also get a draft preset:
cdir lock new "<words>" --profile apple. The profile denies vendor dirs and
dependency lockfiles (Pods, Carthage, SPM, Bundler), signing assets, and
DerivedData; under XcodeGen or Tuist it also denies the generated
.xcodeproj / .xcworkspace so nobody hand-edits them. When Package.swift
exists it sets verifyCommand: swift test with a 15-minute
verifyTimeoutMs (override with --test-timeout on run/verify). The matching
agent rulebook: skills/codedirector-apple/SKILL.md.
Build or refresh the structural index at .codedirector/index.json.
Incremental: each file's sha256 is stored; only new/changed files are
reparsed. Honored skips: .git, node_modules, dist, .codedirector,
plus root .gitignore / .ignore patterns (nested .gitignore files are
not yet honored).
cdir's ignore rules for its working state (index, baselines, runs,
checkpoints) live in .codedirector/.gitignore; locks stay commitable. The
project's own .gitignore is never edited. Older versions added a
# BEGIN cdir … # END cdir block there; where one exists, it is left as it
is and still works.
Prints a ranked repo map: symbols most relevant to the query, ordered by personalized PageRank over the symbol graph (restart vector biased to the anchor symbols that matched the query), packed to a token budget (1 token ≈ 4 chars). Deterministic: same index + same query ⇒ same map.
Blast radius for a symbol — genuinely useful with no AI involved:
- where it is defined (file:lines, signature)
- direct callers (reverse call edges)
- transitive callers to depth 3, with per-depth counts
- which test files reference it (
*.test.*/*.spec.*/__tests__) - historical co-change coupling (top files committed together with the
defining file, from
git log --name-only; degrades gracefully outside a git repo). Skipped with a printed reason on shallow clones (git rev-parse --is-shallow-repository) and on histories under 30 commits — on a--depth 1clone the single commit touches every file, so the coupling report would be pure noise.
--json emits the same report as deterministic JSON (sorted keys).
Exit codes: 0 success · 1 failure (e.g. symbol not found) · 2 usage error.
cdir lock new "<utterance>" [--goal TEXT] [--keep SPEC] [--deny GLOB] [--budget-files F] [--profile NAME] [--verify-timeout MS]
Drafts an Vibe Check — a compiled, enforceable contract, human-readable
YAML versioned at .codedirector/locks/IL-<NNNN>-<slug>.yaml. The command is
interactive-free: it resolves anchor symbols from your words, computes blast
radius, and PROPOSES budget.files (defining files of the top-ranked
symbols) plus a suggested deny list (dependency manifests, unrelated test
files). Anchor matching is tiered — exact name, then name-token, then lexical
fuzz — and when only lexical fuzz matches, the draft abstains: empty
budget plus a guidance assumption, rather than a confidently wrong one. Deny
suggestions are compressed to directory globs and capped at 8 entries; any
omission is counted in the lock's assumptions, never silent. The draft is
written with status: draft — edit it, then
cdir lock check IL-XXXX and cdir lock activate IL-XXXX.
Lock numbers are unique across every worktree of a clone. Inside git, each
number is reserved as an empty file in the clone's git common dir
(<common dir>/cdir/lock-ids/IL-NNNN, never deleted), which all worktrees
share; the next number is one above the highest lock file in this worktree
and the highest number the clone has handed out. Outside git, or when that
folder cannot be written, the reservation is a file in the locks folder, as
before. Ids order by their number, so IL-10000 comes after IL-9999.
The Lock schema (v1): utterance (your exact words, immutable), goal,
interpretation (the system's operationalization — editable), keep[],
deny[], change, verifyCommand (optional user test harness, e.g.
npm test — run as measured evidence), verifyTimeoutMs (timeout for it
and tests-pass runs; the draft estimates it from the command, and a lock
carrying a verifyCommand never falls back to 60s — --verify-timeout on
lock new and --test-timeout on run/verify override it),
verifyCovers[] (optional; languages you assert verifyCommand exercises —
see below), budget {files, symbols, maxFiles, maxLines}, accept[],
assumptions[].
cdir lock check refuses a Lock whose verifyCommand demonstrably cannot
reach the languages its budget touches — a budget of Swift files behind a
TypeScript-only check is not verified, it is unexamined. The command's reach
is read from the toolchains it names (swift, xcodebuild, tsc,
node --test, npm/yarn/pnpm, vitest/jest/mocha, pytest, go, cargo,
gradle/maven). A command naming none of them warns rather than refusing,
since refusing every bespoke harness would be worse than the hole; and
verifyCovers is how you say what a harness of your own actually runs. Files
whose extension names no language — .md, .gitignore — are not counted
against a Lock.
KEEP clause kinds:
| Kind | Payload | Checked how |
|---|---|---|
api-unchanged |
symbol ids | ✓ signature hash, diffed against the pre-change baseline (proven) |
no-new-dependency |
— | ✓ dependency maps in package.json + manifest/lockfile hashes (npm, SPM, CocoaPods, Carthage) vs baseline (proven) |
output-unchanged |
entry command + fixtures | ✓ stdout and stderr sha256, captured at baseline, re-run at verify (measured) |
tests-pass |
test glob | ✓ node --test <glob> (measured); empty glob is a violation |
custom |
free text | ? not machine-checkable — always Unchecked, human judges |
A Lock rejects clauses it cannot even in principle check, unless
kind: custom — and custom clauses are always flagged as human-judged.
--keep SPECs: api-unchanged:<file>#<symbol> (comma-separated ids),
tests-pass:<glob>, output-unchanged:<command>, no-new-dependency,
custom:<text>.
cdir lock ls · cdir lock show <id> · cdir lock check <id> · cdir lock activate <id> · cdir lock abandon <id> --reason "<text>" · cdir lock rebase <id> --accept-current
show renders the Lock with per-clause checkability markers (✓ machine-checkable / ? human judges).
check validates against the current index — budget files exist, symbols
resolve, deny globs valid, maxFiles >= files.length — and exits non-zero
on invalid. activate moves draft → active only when check passes; it is also
the re-approval of an active, verified or failed Lock whose content changed
(it re-seals). An abandoned Lock cannot be activated.
abandon closes a Lock for good — from any status except abandoned. It sets
status: abandoned, records the required --reason as a confirmed user
assumption (abandoned: <reason>), and re-pins the seal; an abandoned Lock never
runs again, and a report on it does not reopen it. It never touches the work
tree: if the Lock's last run changed files that are still uncommitted, it lists
them and says how to keep (commit) or drop (git checkout -- <files>, cdir undo) them.
It is a CLI command only; the MCP tool list does not change.
Work left behind. A new Lock takes the tree as it stands as "already there",
so an earlier Lock's uncommitted changes would go unmentioned in its report.
lock new and lock activate (and the MCP lock_draft and lock_activate)
therefore warn — never refuse — about every other Lock that is failed,
incomplete, active or verified and whose latest run changed files that are
still uncommitted (differ from HEAD, or untracked), newest first, at most five
files named. A file is blamed on the newest Lock, of any status, whose latest
run changed it — an older Lock is not named for a file a later one changed
again. A failed, incomplete or active one "left changes in the tree"; a
verified one "is verified but not committed". Abandoned Locks are never listed.
rebase --accept-current moves the lock's task baseline to the present
tree — the explicit revision, for a legacy lock or a reference that must
change. The replaced baseline is archived beside the new one; nothing moves
silently, and re-running never moves it.
Reads the optional .codedirector/checks.yaml — which checks each area of the repo needs — and says what it requires. Drafting fills the verifyCommand from it, lock check refuses a verifyCommand that leaves a needed check out, and a run or report fails on one (a CHECKS violation): see docs/checks.md.
Git-native safety net (blueprint §19): checkpoint records a lightweight
tag cdir/ckpt-<timestamp> at HEAD plus a byte snapshot of dirty /
skip-worktree files under .codedirector/ckpt-blobs/. With a lock id —
cdir checkpoint IL-XXXX — it also captures that lock's task baseline
once (the pre-change reference every later attempt is judged against; see
cdir run). undo resets to the tagged ref and restores that snapshot —
including a dirty tree as of checkpoint time (--force) and skip-worktree
files that git reset --hard would otherwise leave. Honest limit: git
cannot undo external side effects.
⚠ Behavior change from v0.1.0: untracked files created after the
checkpoint are deleted by undo (v0.1.0 left them in place). The files
are enumerated and printed to stderr before deletion, and the deleted list
is shown again in the undo output. Pass --keep-untracked to preserve them.
Verified execution inside an active Lock:
- auto-checkpoint (git tag) before anything;
- resolve the lock's task baseline — one immutable reference per lock
(signature hashes of every
api-unchangedsymbol, dependency fingerprints, stdout+stderr hashes of everyoutput-unchangedcommand — probes are isolated so they cannot mutate the tree — plus HEAD / dirty hashes / skip-worktree state) at.codedirector/baselines/<id>-task.json. Captured by the first run (orcdir checkpoint IL-XXXX); a retry REUSES it, so a broken promise cannot be laundered by running again; - run the command (spawned, stdio inherited);
- classify every file the command actually touched against the Lock — delta
vs the task baseline, not vs current HEAD, so
git commit,git mv, and skip-worktree cannot hide a deny. Writes outside--rootare out-of-budget. Pre-existing dirt is not billed to the command — but an earlier attempt's change IS. Denied or out-of-budget changes are violations; nothing is auto-reverted (cdir undois offered).--allow-expandis the logged override for scope growth only — a broken KEEP clause still fails; - run the verification ladder (see below) against the baseline;
- write the run record (including the full verification result) to
.codedirector/runs/, update the lock status (verified/failed/incomplete), and emit the Change Report (--no-reportsuppresses the rendering, not the verification).
A declared check that could not run — a verifyCommand or tests-pass
timeout, a clause whose baseline surface never captured — makes the status
incomplete, never verified; circumstantial skips (no tsconfig.json, no
compiler) stay informational.
Exit code: 0 only if the command succeeded AND no violations AND no
declared check went unrunnable. cdir run without a lock id is refused —
ad-hoc mode is a later phase; the whole point is the contract.
After execution, every KEEP clause and every lock-level claim is verified as far as possible, in ladder order (cheapest/strongest first):
- Structural (proven) — re-index; each
api-unchangedsymbol's signature hash compared pre/post (declaration surface: export status, parameters, annotations, member surfaces; whitespace-insensitive);no-new-dependencydiffs dependency maps inpackage.jsonplus lockfile hashes. Both produceprovenheld or violated. - Typecheck (proven when clean) — if
tsconfig.jsonexists and a compiler is available (node_modules/typescript, elsenpx --no-install tsc), runtsc --noEmitwith a 120s timeout. Clean → proven. The baseline records the errors that already existed before the run (file, code and message, without positions), and only errors the run added are a measured violation; errors that were all there before → held, measured. No compiler or no tsconfig → Unchecked with the reason named. - Tests (measured) — each
tests-passglob is expanded and run vianode --testwith a timeout; pass → measured, fail → measured violation with the failing test names, empty glob → violated (fail closed).node --testruns JavaScript and TypeScript only:lock checkrefuses a glob that names another file type (Tests/**/*.swift), and a glob that reaches such a file anyway is Unchecked with the reason, never "failing". Other suites belong inverifyCommand. A Lock-levelverifyCommand(e.g.npm test) runs the same way. Probes are isolated and stripNODE_TEST_CONTEXT. Afterwards the tree is put back as it was, but only the files a probe changed are written back, so every other file keeps its time. What a probe changed or added is moved to.codedirector/runs/put-back/and named in the Change Report: a probe can't tell its own writes from another session's, so nothing is lost. - Output (measured) — each
output-unchangedcommand is re-run in isolation and its stdout and stderr sha256 compared to the baseline capture. If the lock was edited after the baseline was captured, the clause is Unchecked — "no pre-change baseline" — never silently held. - Custom (unchecked) — always
unchecked — human judges, listed by text. This honesty is the feature.
The verifier writes nothing to the repository (the index refresh touches only
.codedirector/). The probed commands themselves run with normal repo
permissions — a fully sandboxed verifier is a later phase.
Re-runs the ladder without re-running the change command: the lock's task baseline, fresh index, all rungs. It also recomputes the scope delta from the current tree against that baseline — a file changed after the run is part of the receipt. Without a task baseline, structural and output checks report Unchecked ("no pre-change baseline") while tests and typecheck still run, and the verdict is incomplete — never verified. Updates the lock status; exit 0 only when verified.
Editing a Lock breaks its seal, so re-approval (cdir lock check then
cdir lock activate) is still a human act; moving the task baseline is
cdir lock rebase --accept-current, an explicit act of its own.
--test-timeout MS (on both run and verify) sets the timeout for
tests-pass runs and the lock's verifyCommand, overriding the lock's
verifyTimeoutMs (estimated from the command when the draft omitted it; a
lock carrying a verifyCommand never falls back to 60s).
PreToolUse bridge for agent lifecycle hooks (agent lifecycle hooks and compatible systems). Reads the agent's hook payload JSON from stdin
(tool_name, tool_input, cwd) and decides whether an edit-ish tool
call may proceed against the active Lock: deny-listed and out-of-budget
files are blocked (exit 2, reason on stderr — the agent receives it as a
failed tool result); edits with no active Lock are allowed with a workflow
reminder (exit 0, JSON message on stdout); non-edit tools, unknown
payload shapes, and any internal error fail open (exit 0). Paths under
.codedirector/ are always allowed — the layer manages its own state.
Per-edit enforcement covers file membership only; maxFiles / maxLines
stay with the run-time classifier, and the hook cannot see inside shell
commands — cdir run verification remains the hard floor.
Renders the Change Report — the product's signature artifact. It judges
the latest attempt against the current tree: the scope delta from the task
baseline is recomputed (a post-run edit is not invisible) and the ladder
re-runs against the task baseline. The recorded command outcome is kept as
that attempt's history and decides the verdict only for the attempt it
belongs to, so a fixed re-verification is never left reading FAILED. The
verdict is verified, failed, or incomplete — the last when there is no
task baseline to judge scope against, or a declared check could not run, and
it is never written as verified.
--attempt N renders a recorded attempt as it was, labeled with its id, and
writes nothing onto the lock: an old attempt is history, not the verdict.
A lock answers for the tree only until the next lock starts. Once
another lock's task baseline has been captured after this lock's last
attempt, the live tree holds that lock's work too, and judging the older
lock against it would blame it for changes it never made. So for a lock that
a later one has closed, cdir report — and cdir verify, and the MCP
report tool — show its last attempt as recorded, name the lock that closed
it ("Closed by IL-0042"), re-run nothing, and leave its status as it was.
cdir lock rebase opens it again, on the present tree. Two things this rule
does not reach: two locks running at the same time still see each other's
files, and an edit made between one lock's last attempt and the next lock's
start is not judged by any report once that lock exists.
The report opens with a plain-language summary generated from the same data as the
detail ("Done — verified. Only src/preview.ts changed, within the agreed
scope. 2 promises held (checked). 1 thing needs your judgment…" — or, on
failure, "Blocked: src/export.ts was outside the agreed scope. Nothing was
reverted — run cdir undo to restore."). The summary never says "verified"
when violations exist; the detail follows underneath:
- lock id + the original utterance, verbatim and immutable;
- changed files with classification (in-budget / out-of-budget / denied) and the budget they were measured against;
- every KEEP clause and lock-level check with verdict, evidence class, and artifact reference (which baseline file, which command, which exit code) — violations first, then verified-held, then the Unchecked bucket (always visible, each item with its reason);
- acceptance criteria (human judges), rendered beside the Unchecked bucket and counted in the summary's judgment line;
- findings: incidental observations (e.g. "src/x.ts changed in-budget but no test file references it — behavioral coverage unknown"), always Asserted and labeled. Two more never change the verdict: "Verified, not committed yet" (a verified report of the current tree whose changed files are still uncommitted; not added to the report that ends a live run) and "The command changed no file" (a run that exited 0 and changed nothing — a patch applied from the wrong folder is skipped silently, and the run proves nothing);
- a footer with counts per evidence class.
--format=md is PR-postable markdown; --format=json is deterministic
(stableStringify). The report builder downgrades any held claim lacking an
artifact reference to Asserted — schema-enforced, per the blueprint. Report
verdicts update the lock status, so cdir lock show reflects them.
Sample (clean run):
CHANGE REPORT · IL-0001 · verdict: VERIFIED
Said "make the preview feel instant"
Goal input-to-paint under 50ms during drag
Run node scripts/inline-debounce.js · record .codedirector/runs/IL-0001-….json
Changed files (1 changed · budget 1/2 files, 3/400 lines):
✓ src/preview.ts in-budget
Violations: none
Checks (violations first):
✓ proven api-unchanged · src/pipeline.ts#ImagePipeline.renderExport — signature unchanged
artifact: .codedirector/baselines/IL-0001-….json#signatures[src/pipeline.ts#…]
✓ measured tests-pass · test/*.test.ts — 1 test file(s) pass under node --test
artifact: node --test test/*.test.ts (1 file(s)) → exit 0
Unchecked (1) — named, not silently dropped:
? custom · feels right above 8K sources — not machine-checkable — human judges
Counts: proven 1 · measured 1 · asserted 0 · unchecked 1
- Human states what must be true; the system finds out whether it is.
- Evidence, not assertion. Every claim carries an evidence class and an artifact reference.
- Determinism. Index, map, and report outputs are byte-stable for identical inputs (sorted keys, stable ordering). The Change Report stage depends on this.
- Useful before it is smart.
cdir whyworks in minute one, with no configuration and no model in the loop. - Local-first. The index lives in the repo at
.codedirector/; nothing leaves the machine.
npm run eval runs the cases in eval/cases/*.yaml against the real CLI in
temp git repos and scores them against explicit expectations (exit codes,
violation substrings, per-clause evidence class + verdict, minimum Unchecked
bucket size). It is a regression gate: any failed case exits non-zero.
It is deliberately not more than that — no LLM judge, no statistics, not
blind. See eval/README.md.
Per the blueprint's phasing, honestly:
- No LLM agent loop.
cdir runwraps a command you (or your agent) provide; it does not plan, edit, or repair by itself. - No sandboxed verifier. Probed commands run with repo permissions; our code writes nothing, but a compromised test script could. Baselines are tamper-EVIDENT (capture-time sha256 re-checked at verify; a mismatch invalidates the run), not tamper-proof — full sandboxing and moving the baseline outside the writable tree are later phases.
- No characterization generation.
output-unchangedpins the output of commands you name; it does not auto-generate behavior-pinning tests over the KEEP surface (blueprint phase 1.5+, with a mutation-testing gate). - No performance measurement rung. Latency/memory claims are not
measurable yet — such clauses belong in
custom(human judges) for now. - No symbol-level budget enforcement.
budget.symbolsis advisory; enforcement is file-level. - Heuristic parsing. Call edges are name-resolved, not type-resolved; compiler-grade precision (LSP) is a later phase.
cdir is a contract checker, not a sandbox. Known evasion paths are
instrumented and reported, and every success claim carries its evidence
class — but we do not claim "the agent couldn't have cheated," only "if it
had, you would see it." Kernel-level isolation, a read-only verifier process,
and merge-only-in-budget worktrees are real options, and we have chosen not
to build them yet. The next layer gets picked by evidence, not by anxiety:
we consolidate what exists, let users hit the referee in daily work, and let
their complaints — not our imagination — decide what hardens next. The name
"lock" belongs to the YAML contract; it is not a security claim.
src/
cli.ts arg parsing, commands, exit codes
index.ts public API barrel — later stages import from here
core/
types.ts RepoIndex / FileIndex / SymbolInfo / CallSite / CallEdge
walk.ts repo walker (.gitignore-aware, fixed skip set)
parser.ts web-tree-sitter WASM extraction (symbols, imports, calls)
builder.ts incremental index builder (sha256 content hash per file)
store.ts .codedirector/index.json persistence (deterministic JSON)
graph.ts symbol graph: call-edge resolution, callers, tests
rank.ts personalized PageRank over the symbol graph
map.ts token-budgeted ranked repo map
why.ts blast-radius report (composed from graph + git)
git.ts co-change mining via git log --name-only
lock/
types.ts VibeCheck / KeepClause / LockBudget / schema v1
yaml.ts stable YAML round-trip + strict validation
glob.ts glob matching/validation for deny, budget, test globs
store.ts .codedirector/locks/IL-NNNN-slug.yaml persistence
leftovers.ts earlier locks' uncommitted changes — the warnings when work starts
draft.ts lock new — anchors + blast radius propose budget/deny
check.ts lock check — validation + signature-hash primitive
show.ts lock show — blueprint-style rendering, ✓ / ~ / ? markers
checkpoint/
index.ts git tag checkpoints, one-command undo, dirty-tree guard
run/
baseline.ts pre-run KEEP surface capture (outside the source tree)
classify.ts changed-file classification: denied/out-of-budget/in-budget
run.ts cdir run — checkpoint → baseline → execute → enforce → verify → record
probe.ts read-only command probes (bounded timeout, sanitized env)
verify/
types.ts EvidenceClass / VerificationItem / artifact-reference rule
verify.ts the ladder: structural → typecheck → tests → output → custom
report/
report.ts Change Report builder + findings + lock status transitions
format.ts terminal / markdown / deterministic-JSON renderers
eval/
cases/*.yaml regression-gate cases (repo setup, lock, command, expect)
run.ts npm run eval — executes cases against the real CLI, gates
Parsing uses web-tree-sitter (pure WASM, no native compilation) with the
official tree-sitter-typescript / tree-sitter-javascript npm packages,
which ship prebuilt .wasm grammars. This was the first-choice option and
installed cleanly; no fallback was needed.
Swift arrives the same way, from tree-sitter-wasms, which publishes
prebuilt grammars for 36 languages. The obvious package, tree-sitter-swift,
was rejected on inspection: it ships native bindings (node-gyp-build plus
per-platform prebuilds) and no .wasm at all, which would have cost this
project the promise the paragraph above opens with. Swift extraction lives in
its own module rather than branching inside the TypeScript one — the node
types share almost no names, and one function switching on language in four
places is where that code rots.
Extraction is syntax-level and
heuristic — call edges are resolved by name (same-file → import → unique
name), not by types. Method calls (obj.m()) are best-effort. This is
documented behavior, not a bug: compiler-grade precision is a later phase
(LSP integration).
Import from the package root (codedirector / dist/src/index.js):
buildIndex(rootDir, opts?) → { index: RepoIndex, stats: BuildStats }— incremental build;stats.filesParsedis the reparse counter.loadIndex(rootDir) → RepoIndex | null,saveIndex(rootDir, index),stableStringify(value)— persistence + deterministic JSON.buildGraph(index) → SymbolGraph— resolved edges,directCallers / directCallees / transitiveCallers(graph, id, depth),testFilesFor(index, symbol),resolveModule(fromFile, specifier, index).rankSymbols(graph, anchorIds) → RankedSymbol[]— personalized PageRank (damping 0.85, 50 iterations, call weight 1.0, same-file weight 0.15).buildRepoMap(index, query, { top, maxTokens }),formatRepoMap(...).blastRadius(rootDir, index, query) → BlastRadiusReport,formatBlastRadius(...),findSymbols(graph, query).coChange(rootDir, relFile),isGitRepo(rootDir).- Types:
RepoIndex,FileIndex,SymbolInfo,CallSite,CallEdge,BlastRadiusReport,INDEX_SCHEMA_VERSION,PARSER_VERSION.
Stage 2 additions (for Stage 3 — verification + Change Report):
- Vibe Check:
loadLock,listLocks,saveLock,nextLockId,draftLock,parseKeepClause,checkLock,signatureHash,lockToYaml/lockFromYaml(stable YAML round-trip),formatLock; typesVibeCheck,KeepClause,KeepClauseKind,LockBudget,LockAssumption,LockStatus,ClauseCheckability; glob helpersmatchPath,validateGlob,globToRegExp. - Checkpoint/undo:
createCheckpoint,latestCheckpoint,undo,gitStatusPorcelain,workTreeStatusPorcelain,dirtyPaths,gitPrefix,toRootRelative; typesCheckpoint,UndoRecord,UndoResult. - Run:
captureBaseline(rootDir, lock, index)+saveBaseline,classifyChanges(rootDir, lock),changedFiles,changedLineCount,runWithLock(rootDir, lockId, command, opts),formatRunReport; typesBaseline,RunRecord,RunOutcome,ClassifiedChange,KeepResult,BudgetStats.
Stage 3 additions (verification + Change Report + eval):
- Verify:
verifyLock(rootDir, lockId, opts),verifyWithBaseline(rootDir, lock, baseline, baselineRel, index, opts),enforceArtifactRule(items)(a held claim without an artifactRef becomes Asserted),runShellProbe/runArgvProbe(read-only probes),loadBaseline,latestBaselinePath; typesEvidenceClass,Verdict,VerificationItem,VerificationReport,VerifyOptions. - Report:
buildReport(rootDir, lockId, opts),finalizeLockStatus,latestRunRecord,formatReport/formatReportMarkdown/formatReportJson; typesChangeReport,Finding,BuildReportOptions. - Lock schema gained an optional
verifyCommandfield (user harness, run as measured evidence);Baselinegained optionaloutputs(stdout hashes ofoutput-unchangedcommands, captured pre-change).
Schema law for later stages: derived structures (edges, rankings,
reports) are computed from RepoIndex and never persisted, so a partial
re-index can never leave stale derived state. Keep it that way.
npm run build # tsc → dist/
npm test # build + node --test dist/test/
npm run eval # build + regression gate over eval/cases/*.yaml
node dist/src/cli.js why ImagePipeline --root examples/demo-repoexamples/demo-repo/ is a tiny TypeScript project (slider → pipeline →
export, an image-preview toy) used by the tests and by later stages as a
stable fixture.
This software is provided "as is", without warranty of any kind, express or implied. The author and contributors assume no responsibility or liability for any errors, issues, data loss, or consequences resulting from its use. Users are solely responsible for evaluating, testing, and verifying any operations or changes made with this tool.
MIT. See LICENSE.