@popovych.co/witness
v0.14.0
Published
Evidence-gated spec-driven development pipeline for AI coding agents: brainstorm to PR in six stages, where specs are state, plans are motion, and no transition passes without evidence. Drives from Claude Code or Pi.
Maintainers
Readme
witness
Spec-driven development for AI coding agents, where nothing advances without evidence.
witness takes a change from brainstorm to merged PR through a six-stage pipeline with five evidence-gated transitions, driven from Claude Code or Pi. Specs are state (what must be true), plans are motion (what we're changing); the CLI is the sole writer of both, and every transition leaves evidence in an append-only journal.
The name is the thesis: an agent claiming "done" is not evidence. A red→green test transition the CLI watched happen is. witness is the thing that watched.
Why
Agents are confident narrators of work they did not do. witness removes the narration from the critical path:
- Evidence, then human. No stage advances on an agent's say-so. Deterministic checks run first; reviewer verdicts are spawned and captured by the CLI, never relayed through the agent's own transcript.
- Specs are state, plans are motion. Specs describe what must be true and outlive the change; plans describe one change and die with it. Never mixed, never reversed.
- The CLI is the sole writer. Agents propose through a validating write path; a guard hook blocks direct edits to canon. Malformed state is unrepresentable, not merely discouraged.
- Red→green is witnessed, not claimed. Every added test must be observed failing, then
passing, with both shas journaled. A test that never failed is refused as
vacuous. - Append-only journal. Every check, verdict and human decision lands in
.witness/journal/*.jsonl— git-diffable, resumable, never compacted. - Resumable by construction. Kill any stage mid-run and re-run it; position is derived from frontmatter plus world state, never from session memory.
Status: all four slices landed — state core, deterministic evidence, gates &
motion, plugin + calibration. Reviewer calibration is the remaining pre-1.0
work (docs/graduation.md).
Prerequisite
witness gate spawns the Claude Code CLI headlessly for every reviewer, on every
harness (DESIGN.md row 87, decision 12). Install and authenticate claude on any
machine that runs gates, whatever drives the pipeline. witness check probes for it.
Quickstart
npx @popovych.co/witness init --agent claude-code # scaffold + install the engine, guard and dashboard
npx @popovych.co/witness recap --file recap.json # birth an effort from a confirmed scope recap
npx @popovych.co/witness write auth-refresh --effort auth-hardening --meta m.json --body b.md
npx @popovych.co/witness # dashboard: where you are, the one next actionInstalled globally (npm i -g @popovych.co/witness) the binary is witness.
Install per harness
witness is two halves: six skills, distributed through the
skills ecosystem installer, and the
engine + guard + dashboard, which no skills installer can place — witness init
--agent <name> writes those and commits them.
Claude Code — one step, the native path:
/plugin marketplace add popovych-co/witness # skills, engine, guard and dashboard togetherPi (and any other agent the installer supports):
npx skills@latest add https://registry.npmjs.org/@popovych.co/witness/-/witness-<version>.tgz
# choose your agent, then choose GLOBAL scope — see below
npx @popovych.co/witness init --agent piInstall skills at global scope, not project scope. Pi resolves project skills at
<cwd>/.pi/skills with no upward walk, and the implement stage runs with its cwd inside
.witness/worktrees/<plan-id> — a directory the installer never touched. A
project-scope install therefore loses every skill in the stage that does the most work.
Global (~/.pi/agent/skills) is cwd-independent. witness check warns when six skills
are installed where a worktree cannot see them, and states which harnesses have none.
A tarball URL is version-pinned, so skills update cannot resolve forward: re-run add
with the new version URL to upgrade.
Upgrading: skills first, then init --agent
Every surface that invokes witness pins the CLI — the engine prompt and all six skills
carry npx -y @popovych.co/witness@<version> — so a repo installed at an older release
only ever runs that release, which compares its payload against itself and reports clean.
The freeze is self-concealing, and a repo frozen at 0.6.0 or earlier cannot detect it,
because the detection ships in a CLI that repo never invokes. Unstick it from outside, in
this order:
npx skills@latest add https://registry.npmjs.org/@popovych.co/witness/-/witness-<new-version>.tgz
npx -y @popovych.co/witness@latest init --agent <name> # restamps the engine, guard and dashboardSkills first: their fresh pins are what invoke a CLI new enough to see the outdated
payload. Claude Code users on the marketplace plugin get both halves from
/plugin marketplace add and need no second step.
From 0.7.0 onward the order stops mattering: witness check asks the registry what
the published latest is and reports both halves of the skew — a CLI behind latest
(cli-behind) and any visible skill pinning something older (skills-behind). The query
is best-effort and silent on failure, so an offline machine reports nothing rather than a
complaint about the network.
witness init --agent <name> overwrites payload files it did not write and names what it
replaced (payload-overwritten); the previous content is one git revert away, because
witness commits the payload. It refuses the whole run if a payload path carries an
uncommitted change (payload-dirty) or if the CLI you are running is older than the
payload already installed (cli-behind-payload).
From 0.10.0 onward init installs into every home of the repository — the primary
root and each live .witness/worktrees/<plan-id> — because a worktree is a branch
checkout, so its payload is a different file on a different timeline. A worktree cut
before an upgrade used to keep the old pin forever, and the old pin is what selects the
CLI that runs there. Every home is preflighted before any is written, so a dirty payload
in one refuses the whole run rather than leaving the set half-upgraded, and witness check
now reports payload-stale per home and names the stale one.
The same release makes the bound structural: every journal entry records the CLI that
wrote it, and a CLI older than anything the repository's state has seen refuses every verb
with cli-behind-state rather than answering from rules the repository has moved past.
witness floor --show reports the bound. If you are deliberately rolling back — a bad
release, say — lower it first with witness floor --set <version> --note <why>; the
decision is journaled, and floor is the one verb that still runs under a bound it fails.
Support tiers
| Tier | Agents | What they get |
| --- | --- | --- |
| Supported | Claude Code, Pi | skills, /witness engine, canon guard, session dashboard |
| Skills only | the other agents the installer supports | the six skills; no engine, no guard, no dashboard — the Witness-State trailer audit remains the guarantee (DESIGN.md row 31) |
Verified against pi 0.83.0 and skills 1.5.21. Re-verify on any major bump of either.
Configuration keys
| Key | Meaning |
|---|---|
| harness: claude-code \| pi | the judge — which harness runs this repo's gate reviewers, is measured by calibrate, and is probed by check. Judgment resolves harness: → PI_CODING_AGENT → CLAUDECODE → claude-code; session lines (next's handoff, dispatch's relay, init --agent auto) resolve detection first, because those name the CLI you are about to type at. Committed, so it binds every teammate's gates; witness init --agent writes it once and never re-points it. Undeclared repos are judged by whichever agent's session is open — witness check and witness status say so |
| gates.reviewerTimeoutMs | milliseconds per reviewer invocation (default 600000) |
| drive.sessionTimeoutMs | milliseconds a drive-spawned session may run before SIGTERM (default 3600000) |
There is no provider: key. witness gate spawns the DECLARED harness's headless
mode for every reviewer (Decisions 88, 105): claude-code renders bare Anthropic ids, pi
renders provider/model[:thinking] with the provider witness knows it needs. The pi
reviewer runs hermetic (--no-extensions --no-skills --no-context-files); machine
extensions it must keep — e.g. an OAuth adapter that supplies your Anthropic auth —
are declared in machine config, not env:
# .witness/config.local.yaml — machine facts, gitignored, never committed
reviewerExtensions:
- /Users/you/.pi/agent/npm/node_modules/pi-claude-oauth-adapter
opener: xdg-open # optional; nonstandard desktops onlyRepo facts live in witness.config.yaml (committed); machine facts live in
.witness/config.local.yaml (gitignored). Every key has exactly one home — a key in
the wrong file refuses. There are no WITNESS_* env vars for configuration.
Verbs
| verb | does |
| --- | --- |
| init [--agent claude-code\|pi\|auto] | scaffold config, principles, journal — one trailer commit; --agent also installs that harness's engine, guard and dashboard (idempotent) |
| recap [--amend] --file <json> | validate + persist a scope recap; births the effort journal |
| write <id> --effort <e> --meta <json> --body <md> | validated manifest → spec/plan on disk + journal entry |
| diff <spec-id> | delta since the last realized state (plan pin → empty) |
| read <id> [--design] [--outline] [--lines <a>-<b>] | print a spec/plan/design from the primary root — the read route, since a worktree carries no canon copy; --outline names each anchor's line range for fat artifacts |
| check | schema, graph, invariants, needs, trailer audit, probes |
| index | id · summary · status · depends across the canon, then each spec's plans |
| satisfy <id> --need <text\|n> | flip a manual need via the write path |
| log <id> | render a journal stream |
| gate <decompose\|plan\|implement\|ship\|design> <id> | run the reviewer gate; journals the round, stamps on pass |
| decide <gate> <id> --approve\|--revise [--pin <policy>] | record the human decision on a stopped gate; --pin adds a standing content policy |
| design <spec-id> --file <html>\|--open\|--reconfirm | register/show a ui spec's approved look |
| start <plan-id> | create/re-attach the plan's worktree (.witness/worktrees/<id>) — code only: the canon dirs are excluded from it, so a plan cannot go stale in there |
| next | the one next action across every effort |
| drive [--flow <plan-id>] [--max-spawns <n>] | schedule green-path work through headless sessions; you appear only at judgment stops. TTY-only — agents are what drive spawns, never what spawns drive |
| ship <plan-id> | lanes → ship gate → PR → CI watch |
| test-evidence / verify-red | journal red/green criteria evidence from a worktree |
| adopt <path> | absolve a finished hand-edit into the journal |
| abandon <plan-id \| effort-slug> | wind a plan/effort down; reverts only specs that effort itself wrote |
| dispatch-report <plan-id> --steps-assigned <n> --steps-completed <n> | journal a session slice's telemetry |
| rename <old> <new> | id rename across canon, refs, journal |
| clean | reap stale worktrees |
| sync | pull --rebase + push state commits |
| calibrate <model> | run the reviewer calibration battery |
| recover [--complete\|--rollback] | resolve a crashed write transaction |
| status | orientation: flows, blocked docs, reconcile rows, pending gates, calibration state |
| (no verb) | the same screen as status |
Exit codes
0 ok · 1 findings · 2 refused (structured {field, rule, got, want} rows) · 3 blocked (lock/txn/untrusted cmd in non-TTY) · 9 test-only injected crash
State model
- Frontmatter is position — the CLI derives "where are we" by scanning it; nothing else stores pipeline state.
- The journal is history —
.witness/journal/<id>.jsonl, append-only, committed, never compacted. - Every state commit carries
Witness-State: 1—witness checkaudits that spec/plan diffs appear only in trailer-bearing commits. - Canon roots are configurable —
paths: { specs: docs/specs, plans: docs/plans }inwitness.config.yaml(defaults:specs/,plans/); scan, commit scoping, the guard hook, and criteria excludes all follow it.git mvexisting docs when changing it. - Local, never committed:
.witness/{lock,txn.json,allow.json,calibration.local.yaml}.
Development
pnpm install
pnpm test # vitest, in-process CLI against throwaway git repos
pnpm run typecheck
pnpm run build
pnpm run release patch # bump + stamp pins + commit + tag, in one act — see docs/RELEASING.mdReleasing is node scripts/release.mjs <major|minor|patch|x.y.z> followed by two pushes; the tag push is what publishes. The order is fixed because CI checks the tagged tree, not the intent — docs/RELEASING.md explains why and what to do if a tag was pushed early.
Run economics (operator notes)
Non-load-bearing knobs for the machine that hosts long implement runs — the design depends on none of them (DESIGN.md row 79):
- Keep the host awake. A suspend longer than an hour mid-run expires the prompt
cache; on wake the full agent context is re-written at cache-write price. On macOS:
caffeinate -dimsfor the session, or plug in and disable sleep. CLAUDE_CODE_AUTO_COMPACT_WINDOW(set at CLI launch) lowers the harness's auto-compaction threshold as belt-and-suspenders under the dispatch budget. It is documented for main sessions only — whether subagents honor it is unverified.- The designed mechanisms are the dispatch budget (
implement.stepsPerDispatch), the loop-width protocol, anddispatch-reporttelemetry — see DESIGN.md rows 79–81.
