The manifest is the plan. The evidence ledger is what actually ran. Commands move work through both, the board mirrors the plan, and the panel is the same levers with a mouse.
Written against the plugin, not against one repository: every command, flag and default here is
the same wherever it is installed, and a value you supply appears as <org>,
<project> or <phaseId>. This page names no version on
purpose — it carried two and they disagreed. Run /plugin list for yours; a
flag named here that it rejects means the install is behind.
Two commands, typed into Claude Code rather than a shell: the first registers the marketplace this plugin ships from, the second installs the plugin out of it.
First session? This page is the reference, not the introduction — read QUICKSTART.md instead: install to a rendered report, one page, in order.
/plugin marketplace add AleksandarBisevac/claude-plugins
/plugin install audit@quality-gates
Requirements are deliberately thin: Python 3.8 or newer, reachable as
python3, python or py; on Windows, run inside Git Bash.
There is nothing to pip install — a guard that needs a
dependency is a guard that is switched off on most machines.
The guard hooks activate in all your projects, not just the one you
installed from. What the plan gate does there is graded on evidence: with no
manifest it only observes, with a manifest and nothing running it
warns, and it denies an out-of-plan edit only while a phase
has work in flight — a phase that only awaits sign-off does not hold it. So installing does not start refusing edits in repositories that
never opted in. /audit:doctor prints the tier and its basis:
[OK ] plan gate deny - phase P2 has work in progress, so edits are held to the running plan
The refusal says different things to you and to a subagent. You get the two ways forward: widen the task's scope, or the single-use human bypass. A subagent gets neither — it is told to stop and report the path to the orchestrator, because it may not edit the manifest and has no way to ask you. It also cannot edit the plan to get around a refusal: the exemption that lets the orchestrator do its bookkeeping does not extend to a task, since a task that edits the plan it is judged by is one nobody can review. When a scope really is too narrow, widen it and tell the running agent to carry on — it does not need to be replaced, and replacing it throws away everything it has read.
/plugin install takes a --scope. The choice is about where the
enabling record is written, not about which repositories the plugin can read.
| Scope | Written to | Who gets it |
|---|---|---|
user | ~/.claude/settings.json | every repository you open; nothing lands in the repo |
project | the repo’s .claude/settings.json | committed, so teammates are prompted to install it too |
local | .claude/settings.local.json | only you; the file is gitignored |
Installing enables as a side effect; the separate enable verb exists
only to switch a disabled plugin back on.
/plugin marketplace update quality-gates
/plugin install audit@quality-gates
/reload-plugins
Each one undoes a different kind of staleness, which is why dropping one usually leaves you on the old version wondering why a documented flag is rejected:
It does not follow later releases: a repo set up months ago can still be running the version it was installed with while user scope has moved on. Re-run the install in that repository to bump it.
/plugin list # audit@quality-gates, enabled
/audit:status # renders the manifest, or points you at /audit:init
If /audit:* comes back not recognized, it is nearly always one of two
things: installed but not enabled, or only ever installed at project scope for some
other repository. The three commands above resolve both, and
/audit:doctor reports what the hooks will actually use once they do.
The plan hangs off one file — the manifest. It holds phases, each holding tasks, plus bugs and parked proposals. Commands read and write that file; nothing else is the source of truth about what is meant to happen.
What did happen is kept somewhere else. Every gate run is appended to an
evidence ledger beside the manifest, and the manifest caches only a pointer
— testEvidence — at the run that last exercised each task and each
phase. The pointer is disposable: deleting it is always safe, the next run writes it back, and
absent means no run was recorded, never “failed”.
Running phases is where that gets read.
done by being
closed on the board; it reaches it through sign-off, and the board is told afterwards.
Every command below carries what it touches: read-only writes manifest writes ADO takes lock
The manifest is generated once, before the pull request, so most of you will never run this. It is here so the output makes sense when you see it.
Interviews for scope, fans out read-only explorers across the codebase, synthesizes a plan,
and shows it for approval before writing anything. What you approve becomes
live phases; what you decline is parked as proposals rather than thrown away. It also asks
how to store the manifest: one file, or one per phase (sharded) for parallel
worktrees.
The first thing to run when something behaves oddly. Reports the interpreter the hooks use,
git root, config, manifest and shard integrity, the active plan-gate tier, build runners,
whether hooks have ever fired, whether the capability policy is inert, whether every
testEvidence pointer names a run this checkout holds, and what earlier runs
left behind in git — worktrees whose branch already reached its parent, records
pointing at directories somebody deleted by hand, and branches it could not compare.
--deep adds the journal chain.
It reports the residue and never reaps it. The remedy it prints is the
read-only command, and for a worktree this plugin did not create it prints
remove --path <dir> — a diagnostic that deleted somebody's working
directory would be the last one anyone let run.
Which build is running, and whether a newer one is published: the running copy's version,
what the marketplace it came from offers and whether that marketplace
auto-updates, every installed copy by project, and the newest release on
the plugin's repository — each line naming where it was read. When a newer release is
out it prints the update commands and exits 1; when the release feed cannot be reached it
says so rather than calling you up to date. The feed is the plugin's one outbound request,
and --offline skips it.
Auto-update is Claude Code's, not the plugin's. For a third-party
marketplace it is off until you turn it on in /plugin →
Marketplaces, or declare "autoUpdate": true for the marketplace in
settings; /audit:version tells you which it is.
meta.buildCommands names the commands a phase's sign-off runs, and
/audit:doctor checks each one's runner against this machine's
PATH. It warns per runner, because a missing one is
infrastructure failure, not a red test — one verdict says
“your change is wrong” and the other says “nothing was measured”.
And the gate is chosen per phase, not per manifest: each phase's
testGate lists which keys must pass, so where a repo's Python runner is absent a
phase naming only the JS keys still runs a gate that grades. Read the warning against the
phase you are about to start.
Measured on a conda project: buildCommands.pytest read
.venv/bin/python and no .venv existed anywhere. The repair was not a
different command — the environment the project already declares in
environment.yml, carrying both missing runners, had simply never been created on
that machine. Repointing the key would have hidden that.
A phase is the unit of work and the unit of review. Run a whole phase, one task, or
“whatever is next” — each of them previews first with
--dry-run.
Every ready task in the phase — in parallel where that is safe — then
Phase sign-off in a fixed order: review, the test gate, an invariant check,
an optional runtime boot, the sign-off commit, the merge. A phase already done
is refused and points you at /audit:review.
/audit:phase P0 --dry-run # see the plan
/audit:phase P0 # then run it
The next ready task — phase order, then task-id order — then what became ready.
One task by id, with guards on done, blocked and
in_progress, plus blocker checks.
The in-progress phase, continued from the first uncommitted task — the answer to “my session died mid-phase”.
Re-runs sign-off on demand, after fixes, without re-running the tasks.
The git worktrees this plan owns. add sets one up for a phase so two
people can run two phases at once: open a session in the printed path and run
/audit:phase there. Sharded manifests merge back
conflict-free, which is why that layout exists.
sweep is for the ones that outlived a run. It is read-only until
you pass --apply plus a verb, and it reaps a worktree only when
four things hold. Two are about permission: this plugin must have
created it — add writes that record inside the worktree's own
admin directory, where git deletes it along with its subject — and the phase must be
settled: signed off, no task still open, mergedAt stamped. Only
then do the two about safety apply: the branch is contained in its parent, and the tree
is clean. Every one of them fails closed.
A worktree you opened yourself is not the plugin's to remove, and by
branch name and merge state it is indistinguishable from one the plugin made — which is
why nothing but an explicit record is allowed to decide. Those are reported and left
alone; remove --path <dir> is how one comes down, named by the person
who wants it gone. The clean-tree rule matters for the same reason in the other
direction: removal also destroys ignored files (.env,
node_modules) that git status never mentioned.
Phases, tasks, bugs, the ready-now list, what each pending task waits on, and a
tests column carrying what the run that last exercised it said. No locks, no
mutations — safe mid-phase.
It lists the work in hand, not the whole plan. Finished phases
— done and cancelled — fold into one line saying
how many phases and task rows it left out; --view all brings them back,
--view archived shows only those. A plan whose phases are all finished
opens on everything, because an empty table answers nothing. Same three views, and the
same default, as the report's toggle and the panel's Overview select. The render is
printed verbatim, so what it costs you is its length — and an archive only grows.
P2 Input validation In progress [###.........] 1/4 audit/p2-input-validation
[x] P2.1 Validate the checkout payload Done sonnet - passed c4a11b9
[~] P2.2 Sanitize the product-search query In progress opus - - -
[!] P2.3 Escape server-rendered template output Blocked sonnet - failed -
[ ] P2.4 Add zod schemas for cart mutations Pending haiku - - - READY
That column appears only once a task in view has recorded a run, so a plan that has never run
a gate renders as it always did, and - inside it means no run was
recorded — not a failure.
The gate is never run by hand. It goes through run-test-gate.py, which brackets it
and answers the two questions an exit code cannot: did the gate change the working
tree (git status --porcelain before and after) and did anything
actually run. Both failures used to be exit 0. --record then writes the run
into the evidence ledger and points the plan at it.
# what /audit:run does for a task, and /audit:phase at sign-off
python3 "$CLAUDE_PLUGIN_ROOT/scripts/governance/run-test-gate.py" \
docs/audit/audit-plan.json P2 --task P2.3 --record
evidence: recorded 2026-07-15T09:40:00Z.13be6d
pointer: P2.3 now names it
Those last two lines are separate facts. Rows land in
<manifest dir>/evidence/<YYYY-MM>.<writer>.jsonl — one file
per writer per month, so two worktrees never conflict on merge — and the directory is
meant to be committed: it is the record somebody hands to a client
(evidence.dir moves it). A row keeps what ran, what it answered and what it
observed; never a runner's output.
The ledger row is written first and the plan is pointed at it second, so the only reachable
partial state is the harmless one: a run that happened with nothing yet naming it. When
another live session holds the phase lock, the second line reads
pointer: NOT updated - the phase lock is held by another live run (…).
Do not re-run the gate to chase it — the repair reads the ledger
against the plan and runs no tests at all:
python3 "$CLAUDE_PLUGIN_ROOT/scripts/governance/run-test-gate.py" \
docs/audit/audit-plan.json P2 --reconcile
[run-test-gate] reconcile: 7 subject(s) in the ledger
already: phase P1
already: task BF1.1
A task commit stages the evidence directory, so a run that fails at attempt one and succeeds
at attempt two is already durable, failure included. What is not durable is a run
whose task or phase never commits at all — a task going blocked, an
infrastructure stop, a red sign-off gate. commit-task-work.py --state covers
exactly those points, staging the phase's manifest file, the manifest index, the journal and
the evidence directory — and never the task's own files: the index is read back and compared
against that list, and anything else refuses the commit rather than riding along. A failed
task's code stays uncommitted; only the record of the failure lands.
The verdict is one word from the run, read off the ledger rather than the cached
pointer. Only passed and empty-gate can sign work off.
| Word | Shown as | Means |
|---|---|---|
passed | Passed | every command exited 0, the tree is unchanged, and at least one check ran |
failed | Failed | a command ran to completion and came back non-zero |
no-checks | No checks ran | exit 0 and not a verdict: every step reported zero checks |
timed-out | Timed out | a step overran its bound and was torn down; its exit code is an artefact of the kill |
cancelled | Cancelled | the run stopped rather than answered |
could-not-run | Could not run | the runner never started — a missing binary, not a red test |
empty-gate | Empty gate | the phase declares no gate, so sign-off rests on review alone |
| — | No gate configured | neither the task's tests.gate nor its phase's testGate exists: nothing could have run |
| — | No evidence | a gate is configured and no run has been recorded. Not a failure |
| — | Pointer without evidence | the plan names a runId this checkout's ledger does not carry — the evidence directory was not committed, or was pruned |
A run also carries observations, which are not verdicts and sit beside the
badge rather than inside it: not “what did it say” but “what else is true
about the run that said it”. Each is three-valued, and null is never
0.
| Marker | Means |
|---|---|
| tree mutated | the gate rewrote files. A gate is a measurement — do not commit on that run |
| tree unknown | no comparison was made: git could not describe the tree, or the run was interrupted |
| no overlap | the run named paths, and none is a file this work declares. A runner that prints suite paths still counts: tests/parser.spec.ts is matched to the src/parser.ts it is named after, across directories — only for test-shaped paths and only onto the exact stem, because a false overlap would tell you your work was exercised when it was not |
| coverage unknown | the runner printed no paths, so overlap is not knowable from its output |
| checks unknown | this runner reports no check count — which is not the same as reporting zero |
Every row below is a real subject in the published example report, acme-store.
| You see | It means |
|---|---|
P2.1 — Passed, marked tree mutated |
Both at once: every command exited 0, and the gate rewrote
src/checkout/validate.ts on its way through. Do not commit on that run;
repair it with the read-only spelling of the check, via
/audit:phase retarget. |
BF1.1 — No checks ran |
npm test exited 0, and pre-commit run --all-files exited 0
having reported zero hooks. A gate that skipped everything and one that verified
everything are the same exit code. |
P2.3 — Failed, marked no overlap |
Two facts: npm test came back non-zero, and the run named none of the
file this task declares. The report's Earlier runs disclosure carries the
attempts before it. |
BF1.2 — Passed, no markers |
A clean row: tree unchanged, both declared files named by the run, and a real check count behind the verdict. Nothing left unknown. |
P2.2 — No evidence, - in the terminal |
Its phase declares lint, test, typecheck, so a gate would grade this
task — and none has run against it. A gap in the record, not a red test. |
P4.1 — No gate configured |
P4 is documentation and declares no testGate. Running the gate says so
rather than passing: [run-test-gate] P4 declares an EMPTY gate: nothing here can
prove it done, so sign-off rests on review alone |
--fail-on gained two conditions. failing-tests trips on a task
or phase whose pointer names a run that cannot sign work off;
no-test-evidence trips on a done task or phase carrying no pointer
at all. Neither is in the default set, and neither reads a silence as a failure — a plan
that has never recorded a run trips neither.
/audit:status --gate --fail-on invalid,failing-tests
GATE FAILED: failing-tests (2 subject(s): task P2.3 failed; task BF1.1 no-checks)
Both read the manifest and nothing more; whether that runId still resolves is
/audit:doctor's question. /audit:status --help prints the whole
condition list, rendered from the same tuple the gate evaluates, and
docs/examples/azure-pipelines.yml shows the shape of a pipeline built around
it.
The plan is not frozen. /audit:task and /audit:phase change its
shape, and every verb records what it did — these are journalled operations, not edits.
Every answer is a flag, and the dialogue only covers what you did not pass, so
a fully-flagged call is non-interactive.
Allocates the id, initializes every orchestrator field, updates fileIndex, and
revalidates before returning.
/audit:task add "Extract the KPI query into a service" --phase P2 \
--files src/kpi/query.ts,src/kpi/service.ts --tests-mode regression --risk med
Re-scopes a task that already exists — files, gate, test mode, risk, edges. The
alternative was cancel plus a fresh add, which lost the id and
everything hanging off it.
Renumbers the task into the target phase, rewrites every reference to it,
and records a chained task.move row. You do not chase the id by hand.
Adds a phase to a plan that already exists, without re-running /audit:init.
--outcome is required — a phase with no statement of what success is
cannot be signed off against anything.
/audit:phase add "Cache the catalog query" --outcome "The catalog page issues one query" \
--area backend --gate lint --gate test --blocked-by P2
Changes what a phase is measured by — gate, area tags, desired outcome. This is the
repair when a gate turns out to rewrite the tree. Refused on a phase already
done or cancelled, or signed off and awaiting its merge, whose
sign-off was given against the gate it had.
--rename changes the phase title, and it is the one option here with a
deadline: the title is what the branch name is slugified from, so it may be corrected
while the phase is still waiting and is refused once the branch has been cut. After that
the title is the record of what the branch was cut for.
Records a phase’s sign-off once its review and the gates sign-off runs have passed:
the verdict, the review’s outcome, the summary, the run’s claim released, and a
journal row. It never writes the phase’s status — that is
derived, and reads done once every task is finished, the verdict is recorded
and, for a phase with a branch, the merge has landed. Until then a phase whose work is
finished says sign-off due wherever it is shown, and it does not hold the plan
gate in its denying tier. Refused with open work, on a phase with no task, a second time,
and on a closed phase. skipped is honest where no review ran; the summary
says so.
Pins a phase ahead of the others among work that is already ready:
priority reorders the ready set, it does not override the graph.
It is a phase lever, not a task one — tasks have no priority at all,
and their order inside a phase comes from the dependency graph and their ids.
/audit:task priority is the legacy spelling and still works.
Closes a phase — and every unfinished task under it — as terminal but
not done, recording the reason, the moment and a journal row;
/audit:task cancel <taskId> does one task. Use it instead of deleting:
“we decided not to” is information, and a deleted task cannot say it. Readiness
treats a cancelled blocker as settled, so a plan never deadlocks on work nobody will do.
Bugs live in the manifest beside the plan. fix materializes a TDD fix task, and
the repro test must fail red-first — that is what proves the bug is
the bug. Execution stays in the normal run commands.
/audit:bug add "Sparkline drops the last day on month boundaries"
/audit:bug fix BUG-3 --phase P1
/audit:run T14
A phase can carry area: one tag ("backend") or a list for
cross-cutting work (["backend","frontend"], grouped under each).
meta.areas is the registry that gives a tag properties — a
root, a description, a default reviewSkill, default
executor skills, an advisory owner. Reviewer resolution is
phase.reviewSkill → areas[tag].reviewSkill →
meta.reviewSkill; executor skills are the area's plus the task's own, deduped, area
first. Where a phase carries several tags, written order decides.
This is a manifest label and it reaches no board. It groups and filters in
status, the report and the panel, and selects reviewers and skills. It is
not System.AreaPath — a backend card and a frontend card leave for
ADO carrying the same tag set, and where the board has to tell them apart that is
meta.ado.areaPath or a tag on the item itself, which a push preserves.
Two asymmetries. An absent tag is not a tag: a phase with no
area raises nothing, and the validator warns only about an unregistered
one — usually a typo — so leaving a phase untagged beats inventing a tag to fill the
field. And registering an area makes its silence visible: an area declaring no
skills resolves none, so a task with none of its own warns
no skills resolve. Answer it with the task's own skills, defaults on
the area, or "skills": null — that last one records the intention, where
leaving the warning standing records nothing.
Work that /audit:init proposed and nobody accepted yet lives in
proposals[], outside the phases. It is a queue, not a plan — an empty live
plan with parked proposals means nothing is scheduled, not that something was lost.
Prints id | status | reserved phase (task count) | name | openQuestions.
Default shows proposed only; list all adds materialized and
dropped history.
Materialization is a move, not a re-synthesis. The proposal enters the
plan as it was parked, so settle any open question that would change what the phase
contains first — otherwise you materialize a guess and edit it afterwards.
Phase ids are allocated at materialization, with a collision guard that
reports [renamed from …], so a blockedBy P0 on a parked
proposal points at nothing until then.
When one materialization waits on a still-parked proposal you get exactly one question:
--with-deps pulls in what it waits on (recommended),
--drop-edges cuts the edge and records the cut in the phase
description, or you abort. --all implies --with-deps and
goes in dependency order.
Parks it as declined, with the reason kept.
Brings a dropped proposal back to proposed.
The connector is configured in meta.ado in the manifest. Credentials never go
there — not in the manifest, not in the config, not in any file. Auth belongs to
az or the MCP server, and the secret guard blocks a token that tries to get
printed.
Start here. Read-only rungs first and one write at the end, so nothing is
configured until access is proven — and the thing that proves it is no longer a
push, which is also the first thing that can create items on somebody's real
board. It checks the transport, reports which auth path is in effect
rather than assuming one, probes read-only in the Work Items scope, and detects the
process. Re-run against a configured manifest, it re-probes and reports rather than
clobbering.
Every sync invocation walks the same ladder — manifest,
meta.ado, enabled, transport — and stops at the first rung that
fails, saying which. Nothing reaches the board before it passes, and enabled: false
stops push and pull while status still
runs. To write the block by hand, this is all that is required; everything else
(stateMap, sprint, pull, onComplete,
comments, echo, phaseWorkItems, fields,
conventions, enabled) is optional and edited by the panel's ADO
card:
"ado": { "organization": "<org>", "project": "<project>",
"areaPath": null, "iterationPath": null,
"types": { "bug": "Bug", "task": "Task" } }
Configuration travels in the manifest and arrives with a merge; access does not — every teammate authenticates themselves. Create the token in the browser profile signed in to the account that has access, scope it to Work Items → Read & write and nothing else, then store it:
az devops login --organization https://dev.azure.com/<org>
Work Items read-write is the whole surface the connector writes: titles, descriptions,
state, area, iteration, tags, comments, parent links. A token that narrow cannot read your
source, touch pipelines or cut releases, which makes a longer expiry a reasonable trade
— scope bounds the damage, expiry bounds the duration. With Full access
the arithmetic reverses. An expired one makes sync return 401, which reads like a
broken configuration, so write the expiry date where the team can see it.
A stored token is per organization. On one machine one org can resolve
through a stored token while another resolves through your Azure sign-in, with no indication
which is in play — measured: with a deliberately invalid
AZURE_DEVOPS_EXT_PAT exported, a query against a sign-in org still succeeded. So
before a first push check az account show and whether a token is stored
or exported: a push under the wrong identity does not fail, it writes into the wrong
organization. And you need to run the login command does not mean you are
logged out — a nonexistent org produces the same message.
connect detects this for you. To ask it yourself, one read-only query does it
— the process decides types.pbi and whether you need a
stateMap:
az boards query --organization https://dev.azure.com/<org> --project <project> \
--wiql "SELECT [System.Id],[System.State],[System.WorkItemType] FROM WorkItems
WHERE [System.TeamProject] = '<project>'" -o json
| If you see | Process | What follows |
|---|---|---|
User Story · New/Active/Resolved/Closed/Removed | Agile | types.pbi: "User Story"; the built-in stateMap defaults already match — leave it out |
Product Backlog Item · New/Approved/Committed/Done | Scrum | types.pbi: "Product Backlog Item"; you must set stateMap — the defaults name Agile states |
A card's column lives in per-team hidden WEF_ fields whose id differs per
board, and the connector refuses to write them: it sets
System.State and lets the team's own column↔state mapping move the card. So
a pushed item lands in the default column for its state, and a column backed by no
state of its own — a review lane, a split Doing/Done — cannot be targeted
at all.
Relatedly, sprint resolves the current iteration through the team's timeframe,
so a long-running workstream overlapping the numbered sprints makes “current”
match more than one. A static iterationPath sidesteps the resolution, and
sync status prints sprint: unresolvable rather than guessing.
meta.ado.conventions carries a board's house shape — a description
skeleton, mandatory markers, a tag vocabulary, a required parent — and a
conformance gate runs before every create, so a refusal reaches you
before the item exists. A board with no conventions block passes saying
nothing was checked: an answer, not a silent success.
requireParent: true means a card must hang under something; where it hangs is
the field map's subject, and declaring neither end is not neutral
— the connector then builds its own branch, putting audit work beside the team's
planning instead of inside it. tagVocabulary restricts what tags an item may carry,
one axis per prefix plus "*" for unprefixed tags, and the empty list means
opposite things on the two: {"*": []} admits any bare tag,
because a wildcard with nothing narrowing it is still a wildcard, while
{"supplier": []} admits no supplier: value.
The provenance tag defaults to audit-plugin, which carries no prefix — so
a board whose vocabulary admits only prefixed tags refuses every item the connector creates,
while the manifest still validates clean, because each block is graded alone. Nothing is wrong
with either; they disagree. A prefixed meta.ado.tag closes it.
The connector line, linked vs unlinked counts, then a row per linked item:
manifest id | title | manifest status | ado id | ado state | drift?
It writes to neither side and takes no lock.
Caches the project's backlog ranks and parent-shaped items. Run it before the
first push: measured, ADO does not enforce its own type hierarchy on a link made
through the API, so the connector checks the tree itself — and without this cache
every parent link reports not verified while the create proceeds anyway.
Verify with /audit:sync status while the plan is still small, then materialize,
then push one phase with --phase: a wrong state map then costs a
few items instead of a whole board. On a fresh install, while every phase is still parked,
there is nothing to push — so configuring the connector first is the
one moment when a misconfiguration cannot create anything.
sync status is the only operation that touches
both sides, and it writes to neither.
Default is bugs. Idempotent, because the manifest side of each link
(ado: {id, url, lastSyncedAt}) is written immediately after each successful
call.
stateMap. One bad item does not abort the run./audit:sync push tasks --phase P0 # one phase, smallest first write
/audit:sync push all # bugs + tasks + phases
Brings assigned ADO bugs, or the current sprint's items, into the manifest.
Never modifies ADO. A candidate whose id already appears in any
manifest ado.id — live tasks, bugs, phases, and parked
proposals — is dropped, so a re-pull imports nothing and parking a proposal
does not make its item eligible again. Both flavours ask before importing, and they are
scoped differently:
pull sprint refuses to import blind. It needs an iteration
and a filter (pull.areaPath or pull.tags), because one sprint
can span several repositories. With no filter it lists the sprint read-only, makes you
pick, and offers to save the filter. If your team already tags the cards it owns, that
tag is the filter:
"pull": { "tags": ["<your-card-tag>"] }.pull bugs is project-wide: every non-closed bug, narrowed
only by areaPath if one is set. Not sprint-scoped.Imported sprint items land as parked proposals, one per PBI with its child tasks — nothing enters the live plan until you materialize it.
An item with no ado field is CREATED; one with ado.id is fetched,
diffed and UPDATED only if something differs. But push does
not match by title or tag: a card somebody made by hand for the same phase
is invisible to it, and pushing creates a second one. Broad dedup exists only in the other
direction, so when the board already holds work matching your plan,
pull first, then push.
What lands where on a push. Every default is overridable in meta.ado. Test
evidence is not on this list and will not be — the ledger stays in the repository.
| Manifest side | ADO field | Configured by |
|---|---|---|
| bug | work item type | types.bug — default Bug |
| task | work item type | types.task — default Task |
| phase | work item type | types.pbi · phaseWorkItems (absent = on) |
bug title · [taskId] title · [phaseId] title | Title | — |
| description + repro + expected/actual | Repro Steps (Bug) / Description | — |
| severity high / med / low | Severity 2 - High / 3 - Medium / 4 - Low | — |
bug status open → New, triaged/in_progress → Active, fixed → Resolved, wontfix → Closed | State | stateMap.bug |
task status pending → New, in_progress → Active, blocked → Active + tag blocked, done → Closed, cancelled → Removed | State | stateMap.task |
| phase status | State | stateMap.phase — phase-item vocabularies differ |
task done + onComplete.remainingWork | RemainingWork | stock processes force-clear it at done → retry state-only, report the skip |
| always | Area · Iteration | areaPath · sprint else iterationPath |
| always | provenance tag | tag — absent = audit-plugin, null = none |
A stateMap value of null means never move state for that
transition: the field is skipped and the team moves that card by hand. The built-in
defaults name Agile-process states, so a Scrum project must set
stateMap — /audit:doctor carries that advisory.
Some phases deliver something a stakeholder recognises; others are “extract this query
into a service”. Only the first kind belongs on a shared backlog, and there is
no per-phase opt-out today: phaseWorkItems is a global on/off,
so the only selectivity is what you name at the call site. Make it a team rule:
/audit:sync push tasks --phase P3 # always name the phase
/audit:sync push all # the command your team does not run
meta.ado.parentWorkItem is the fallback: one work item that
anything undeclared hangs under. Where a sprint spans several Features, a phase declares its
own instead:
"phases": [
{ "id": "P0", "adoParent": { "id": 71157 } }, // this Feature
{ "id": "P3", "adoParent": null }, // nothing, on purpose
{ "id": "P4" } // whatever the fallback says
]
Three states, three answers. Absent falls through to the fallback; an object names the parent; explicit null means this phase hangs under nothing even when a fallback is set — uncategorised on purpose, never an error. The id is an integer; a quoted one is a typo the validator names rather than coerces. The push plan prints the resolved parent per phase with where it came from, so a fallback quietly applying to every phase but one is visible before you confirm.
conventions says what an item must have and can only refuse.
meta.ado.fields is the other half — what this project
supplies, per work item type, merged into the create payload before the gate
grades it:
"fields": {
"Task": { "Microsoft.VSTS.Common.Activity": "Development",
"Microsoft.VSTS.Scheduling.OriginalEstimate": 4 },
"User Story": { "Microsoft.VSTS.Scheduling.StoryPoints": 3 }
}
A template may not name a field the connector already maps, nor a read-only
one, and that is refused when you write it rather than tried — because read-only fields
fail two ways and one looks like success. Measured live: System.BoardColumn
refuses out loud, while System.Parent, System.Id and
System.CreatedBy each create the item, report success, and leave the field unset.
Values are literals, with no substitution language.
The obvious two are “the manifest is ahead” and “the board is ahead”.
On a board several teams write to, the common third is: somebody else moved this card
after we last touched it, and neither side is wrong. The reading names the writer so
you can go and ask that person — local ahead,
external (<who>, <when>), or unknown, which draws no
suggested action because the basis for one is missing.
The comparison is your status translated through stateMap
against the board's. Without a map every row reads
state not compared (no mapped state supplied) — so a whole table saying
nothing is compared is not a clean board, it is a missing map.
The index holds each phase as a stub, and every board link below a phase lives in the shard body — so a plain read of the manifest file finds the bugs and stops, where an assembled read finds every link. It does not error; it under-counts, and the connector line looks plausible. The scripts on this path all load assembled, so the trap is in any count you compute yourself. The board read is honest in the same way: a fetch that could not answer for every id exits 1 and names the ones it has no news about, rather than leaving a table that looks complete.
A local control panel for .claude/audit.config.json and the manifest's
composition levers, with live validation and discovery of the skills and agents available
in this repo and globally. Ephemeral and on-demand; a per-project pidfile keeps it
discoverable and stoppable.
The sections, in the order they appear:
Where a run stands, and what the tests said. The live view; it refreshes from disk while you watch.
Per-phase review model, per-task skills and models — and the Branch card, which is
where a phase's landing place is decided: the branch-name convention, the merge target
(meta.developmentBranch, and per phase parentBranch), the three
meta.merge switches, and a live table of this plan's worktrees with a
Sweep button.
Token spend by phase, task, model, author and time — /audit:usage's
data.
What agents are allowed to do. Expandable full-screen, searchable by name and description.
Materialize and drop, through the same script the CLI calls.
audit.config.json, validated as you type rather than on save;
evidence.dir sits in its Audit trail card.
Theme and presentation, for the panel and the rendered report.
Open a phase and its detail carries two evidence lines, labelled apart:
phase sign-off is the phase's own gate run, and tasks is a roll-up
counting its tasks by verdict. They measure different things over different files, so one badge
for both would claim a measurement nobody made. The task table below them ends in a
tests column, in the same position the report uses.
Badges and markers read exactly as they do in Running phases. A badge with a run behind it is also a button: pressing it opens that run's steps (name, exit, checks, took, outcome) and the basis lines for anything it could not determine. A subject with no recorded run is deliberately not a control, because there is nothing to open.
The freshness fingerprint covers the config, the manifest, every shard, the usage ledger and the evidence directory, so a gate recorded in another window refreshes the badge without a reload. Only the runs the plan points at travel to the browser — earlier attempts stay in the ledger, and the report is where you read them. And where a pointer dangles it says how many ledger files it read and how many lines it could not parse, so Pointer without evidence tells an unwritten evidence directory apart from an unreadable one.
The rule is that every feature has a command and a way through the panel. Where it does not hold today, you look in the terminal:
Board status and drift. The ADO card shows manifest evidence only —
how many tasks, bugs and phases carry a link, and when the last sync was. It never reads the
board, so it cannot show state, parent or sprint drift, and the ADO parent cell
shows what you declared: a phase whose card somebody dragged under a different epic
by hand reads exactly like one that agrees. /audit:sync status knows the
difference.
Pulling from the board. No panel path at all — both flavours ask you which items to import, and the panel's manifest writer is restricted to composition levers.
Pruning the plan-gate feed. The panel server can do it; the Plan
gate card has no button yet, so /audit:logs prune is the way.
Running anything. The panel configures runs; it never starts one, and it never records a gate.
Everywhere else, panel and CLI are one implementation: materialize, drop and revive
run the same script from either side, and so does the config validator. The gaps above are
missing paths, not second implementations, which is why a change made in the panel shows up in
/audit:status immediately.
Until this release the panel ran git for reading only. Sweep is the exception, and
it goes through the same path as saving the config rather than beside it: the
loopback and token guards, the manifest write lock — so a running
/audit blocks it — a dry run whose rows fill the ordinary confirmation
dialog, and a hash-chained journal entry. The button never assembles a git command of its
own; it calls the same planner the CLI's sweep calls, so both sides refuse for
the same four reasons and neither can be the lenient one. There is no
--force: a dirty worktree is named and left.
Every change to the plan and the config appends one hash-chained row to a journal beside the manifest. It is meant to be committed — that is what makes it evidence rather than a log — and that single fact decides everything else about what a row may hold.
Because the file ships, no row names a machine. A command is stored as
commandSha256 + commandBytes + program, so you can still
ask “was it this command?” by hashing your candidate while the arguments
— where paths, hostnames and tokens live — are not there. cwd is
repo-relative, . at the root, or <outside-repo>; unresolvable,
empty and outside all land on that token, because a redactor that guesses is a home directory in
a file that ships. There is no actor.host at all. And the file name is the
session id or a random per-checkout token rather than the machine's, which mattered most of all:
the chain is seeded from that base name, so a name committed there could never be corrected
without breaking verify on every clone that already had the file.
The journal anchors the evidence ledger rather than duplicating it: an exit code and a duration are things the plugin observed about the machine, not fields of the plan.
| Action | Says | Written when |
|---|---|---|
test.evidence.recorded |
a gate run was written to the evidence record: runId, and the task or
phase it was about |
its subject is the evidence file, so it is true the moment the row lands, whatever happens to the plan afterwards |
task.testEvidence / phase.testEvidence |
a testEvidence pointer moved, naming from and to |
only after the pointer actually lands. A pointer refused by another live session leaves the row above standing and no row here, because the chain must never assert a transition that did not happen |
audit.state.committed |
an audit-state commit was made for work no task commit will carry | a task went blocked, an infrastructure stop, a red sign-off gate, or
/audit:resume's opening sweep |
Each action has exactly one writer — never append one by hand, because
two writers means duplicate rows and a doctor that can no longer trust the count. These three
are written in process rather than by the journal-writes hook, because the
hook watches edit tools and these writers use os.replace and
git commit.
Editing, deleting or reordering a row breaks the chain and verify names the row.
What it cannot stop is a forger who rewrites the whole file and recomputes every hash forward
— there is no secret key, and nowhere on a user's machine to keep one the same user
cannot read. It is a smoke detector, not a vault. The other half of what it proves is
out-of-band drift: a document that changed with no row to explain it, which
comes back as a warning rather than a finding and is usually the interesting line. History is
never rewritten to tidy any of this up — the chain runs through those bytes, so a row
can be superseded by a later one, never corrected.
The command digest is unsalted on purpose — hashing your candidate is how you check a guess, a shipped salt would be published with the plugin, and a per-machine salt would be readable by the same user it hid things from. So a short command from an obvious vocabulary is recoverable. This buys friction and data minimisation; it is not anonymisation and must not be sold as it.
Verifies the chain across every journal file, live and archived, reporting drift separately from breakage. It also grades the pointers against the ledger both ways — a pointer naming a run this checkout does not have, and a recorded run the plan has not caught up to:
[OK ] evidence 7 pointer(s) in the plan, 9 recorded run(s), and every
pointer names a run this checkout holds
Both are advisory, because a pointer refused by a live lock is a designed state and the
repair is one --reconcile. Asked of what is committed, the same
question is a breach instead:
/audit:status --gate --fail-on invariant-breach.
| Command | Arguments | Touches | Panel path |
|---|---|---|---|
/audit:status | [--gate] [--fail-on …] [--phase ID] [--json] [--color …] | read-only | Overview — the live view of the same state |
/audit:doctor | [--deep] [--json] [--color …] | read-only | none — CLI only |
/audit:report | [--out-dir <dir>] [--share] | read-only | Export report, from any tab |
/audit:usage | [--by phase| [--phase] [--author] [--area] [--since 7d] [--format md|ascii] [--json] [--backfill] | read-only | Usage |
/audit:guide | <question> | read-only | Help drawer |
/audit:sync status | — | read-only | none — the ADO card shows manifest counts, never the board |
/audit:sync connect | — | reads ADO manifest | none — the card edits meta.ado once connected |
/audit:sync parents | — | reads ADO manifest | partial — the cached candidates fill the parent picker |
/audit:phase | <phaseId> [--dry-run] · add · retarget · priority · cancel | manifest lock | none — the panel configures runs, never starts one |
/audit:next | [--dry-run] | manifest | none — same reason |
/audit:run | <taskId> [--dry-run] | manifest | none — same reason |
/audit:resume | — | manifest | none — same reason |
/audit:review | <phaseId> | manifest | none — same reason |
/audit:task | add · scope · move · cancel | manifest lock | partial — Plan & models edits levers, not structure |
/audit:bug | add "<title>" · list [all|<status>] · fix <bugId> [--phase] · close <bugId> [wontfix] | manifest | none — bugs are listed, not edited |
/audit:propose | list [all] · materialize <PROP-id>| · drop · revive | manifest lock | Proposals — same script from both sides |
/audit:worktree | <list|add|remove|sweep> [phaseId] [--path DIR] [--force] · sweep: [--apply] [--remove-worktrees] [--delete-branches] [--prune] [--json] | git worktrees | Branch card — the worktree table and its Sweep button |
/audit:layout | <sharded|single-file> [--dry-run] [--renumber] [--force] | manifest lock | none — CLI only |
/audit:panel | [stop|status] [--port <n>] | manifest | n/a — it is the panel |
/audit:sync push | [bugs|tasks|all] [--task | --phase] | ADO manifest | none — CLI only, and it writes to somebody's board |
/audit:sync pull | [bugs|sprint] | manifest | none — interactive by construction |
/audit:logs | prune [--older-than DAYS] [--dry-run] [--json] | logs feed | endpoint only — the server can prune, the Plan gate card has no button yet |
/audit:migrate | [--dry-run] [--renumber] [--force] | manifest lock | none — the legacy spelling of layout sharded |
/audit:init | [scope/goals] | manifest | none — CLI only |
Nothing under scripts/governance/ has a slash command —
run-test-gate.py, commit-task-work.py,
verify-invariants.py, audit-journal.py and
audit-lock.py. The pipeline invokes them; running one yourself means repairing
something a run left behind. There is no /audit:run-gate verb — it is not
built — so a gate you record by hand is a script invocation:
# record a run the pipeline did not
python3 "$CLAUDE_PLUGIN_ROOT/scripts/governance/run-test-gate.py" \
<manifest> <phaseId> --task <taskId> --record
# the run landed but the pointer did not: read the ledger against the plan, running no tests
python3 "$CLAUDE_PLUGIN_ROOT/scripts/governance/run-test-gate.py" \
<manifest> <phaseId> --reconcile
Running phases shows both against a real manifest, with what each
prints. For anything else, /audit:guide <question> answers from the
plugin's own documentation with a citation for every claim, and
/audit:status --help prints its condition list from the same tuple the gate
evaluates.