Provide concise project / work item status and run Worklog helpers to augment results...
EXECUTE immediately when invoked via /skill:audit. Do NOT ask permission or offer alternatives.
Provide a concise, human-friendly summary of project status or a specific work item. Exposes a canonical runner for automated use and a structured markdown report format consumed by orchestrators.
[A-Z]{2}-[A-Z0-9]+; found ā item-level (step 3), not found ā project-level (step 2).wl next --json, wl in_progress --json, wl list --stage in_review --json, wl list --priority critical --status open --json, wl recent --number 10 --json ("status", "audit" queries).wl show <id> --children --json.Implemented as a lightweight entry guard in cmd_issue (SA-0MSL1Z1WU005O5IY). Before the status lifecycle runs, the runner checks the item's current status:
in_progress at entry + no --force ā abort with exit 1: Error: Refusing to audit <id>: the work item is already in_progress (a concurrent audit or implementation owns it). Pass --force to bypass this pre-flight guard. No report is produced, nothing is persisted, and the pre-audit status/stage are untouched (the guard runs before the runner sets in_progress). This prevents two audits of the same item racing ā both would set in_progress, run the pipeline, and the last writer would win on persist + status transition. The concurrency semaphore (../shared/process_semaphore.py) caps pi subprocesses host-wide but cannot see per-item claims.--force ā bypass the guard and proceed (also bypasses the freshness gate).--force.The runner manages the item's status/stage during execution to prevent concurrent audits and leave it consistent with the verdict (via try/finally, guaranteed even on failure). Invariant (SA-0MTFTFUIH000UWM9): actively worked => in_progress. No wl mutation (description/comment/child creation) while status: open ā guard with StatusLifecycle.require_claimed(<id>) / ensure_claimed(<id>) on any in-session resume path:
cmd_issue() start; set in_progress. A completed/done item is already terminal and cannot accept the in-progress claim (the worklog rejects in-progress/done as an incompatible status/stage pair), so the claim is skipped for it ā the verdict-driven terminal lifecycle below keeps/restores done (SA-0MUWKGW9W004QXQZ).Ready to close: Yes ā completed/in_review (keep done if terminal); No ā open/plan_complete. A top-level item (no parent) with a Yes verdict also gets needsProducerReview: true (via --needs-producer-review yes) so the release gate (closeWorkItemsAfterRelease) blocks until the producer reviews it; child items (with a parent) are not flagged ā their parent's review covers the subtree (SA-0MSSVKYEW008PJ9H).--audit-children cascade (AUDIT_CASCADE_AUDIT marker in the child subprocess env) and the item was already in_review/done at audit start, a No verdict restores the pre-audit status/stage (assignee cleared) instead of demoting to open/plan_complete. A parent-triggered re-screen must never silently push completed/in-review work back into the actionable queue or cascade blocked states through the dependency chain. An explicit, independent audit of the same item keeps the normal No ā open/plan_complete demotion, and a cascade re-audit of an already-pre-review child still demotes (genuine blocking is preserved).Yes verdict advances even when AC evidence was fallback-tainted (e.g. a read-only test skip with a variance note) ā a fallback on some ACs does not invalidate an explicit model Yes (WL-0MSN7XAUS008WOPQ). The fallback flag only forces the restore path for a No verdict (an infra-fallback No is not an explicit model assessment).blocked + in_review), the runner coerces the stage to a valid default (plan_complete) while preserving the captured status, so the item is never left unrecoverable (SA-0MUIVCJLW000RUC1). Only an explicit No moves to open ā a transient timeout never demotes an in_review item. If a completed Yes run is ever restored (script failure during the run), a visible warning is printed ā never a silent divergence.wl update the runner reads back the item via wl show <id> --json and confirms the status/stage actually changed to the expected values (completed/in_review, open/plan_complete, or the restored pre-audit state). A wl update that exits 0 without applying (silently swallowed) is retried up to 3 times with short delays; if still unverified the runner prints a loud diagnostic, best-effort restores the captured pre-audit state, and exits non-zero ā a passing audit can never leave an item stranded in its pre-audit stage silently.{issue_id, original_status, original_stage} to a state file (.audit_state_<id>.json in $AUDIT_ABSTATE_FILE, else the explicit --worklog-dir, else /tmp) immediately before the in_progress claim, and registers explicit SIGTERM/SIGINT handlers that restore the pre-audit status, remove the state file, and re-signal (Python's default SIGTERM disposition skips finally blocks; only SIGINT raises KeyboardInterrupt). The handlers are deactivated once the terminal lifecycle is applied, so a late signal can never undo a legitimate completed/in_review advance. SIGKILL cannot be caught: the parent (check-audit-gate.js / refresh-audits.js via attemptAuditRemediation) checks for a leftover state file after the child exits and restores the item itself. On a normal exit the finally block removes the state file so the parent takes no action. An item is therefore never left in in_progress by any abort path.--do-not-persist doesn't affect the lifecycle; status-update failures are retried and verified, then surfaced loudly (never silently caught).--do-not-persist, a Ready to close: Yes verdict STILL persists the audit freshness signal ā auditedAt/auditResult (not the full rawOutput) ā via the atomic wl audit-set --ready-to-close yes path (updatedAt = auditedAt, WL-0MT8KTE3E001Q1D9) applied as the last write after the terminal transition. The content fingerprint is forwarded so isAuditFresh stays true across later updatedAt churn; the runner exits non-zero if the refresh fails. A No/partial/failure verdict never bumps auditedAt (fail-closed). Omit the flag to persist the full report.Ready to close: Yes transition has been verified, the runner walks the item's full recursive subtree and sets every non-terminal, non-deleted descendant to completed/done, appending one explanatory comment per child naming the authorising parent and the audit timestamp. The cascade is authorised only by the passing verdict (never by status/stage alone: a No, restore, or fallback run performs no cascade), so the existing scope guard (SA-0MU2OY1N9000XL2H) is unaffected. It is idempotent (already-terminal and deleted descendants are skipped ā no duplicate comments), suppressed under --do-not-persist, and non-fatal (a per-child wl failure is logged and does not abort the audit; the next passing audit completes the cascade). Consequently an audit-approved parent arrives at the ship close step with a fully-terminal subtree and is closed without a --force collateral refusal. Audits recorded outside the runner (manual wl audit-set approval, which bypasses this helper) are covered by the ship close step's fallback cascade (SA-0MUR7Y3BJ004FGPP recurrence) ā see release-process.md.A parent audit evaluates its own closure against the child-exemption state
captured at audit start, not against the child stages as they stand after
the parent run. Before _phase_children runs, the runner snapshots every
child whose stage is in_review or whose status/stage is
completed/done into ctx.snapshot_exempt_children (_capture_exempt_children_snapshot).
The parent's closure decision (_assemble_issue_report markdown path,
_build_issue_json JSON path, and the _has_phase1_blocking_issues Phase 2
gate) all consult _child_is_exempt(child, snapshot_exempt), so a child that
was exempt when the audit began remains exempt even if a parent-triggered
re-screen demotes it.
This closes the failure mode where a parent audit over changed children
re-screened them, persisted Ready to close: No child audits, demoted three
children (F2 ā open/plan_complete, F3/F6 ā blocked/plan_complete),
and then blocked its own otherwise-complete closure (all parent ACs met) ā
because the children were no longer exempt after the demotion.
Contract:
idea/intake_complete/plan_complete) at audit start is not in the
snapshot and still blocks; an explicit, independent audit of an in_review
child still demotes it (AC3).Yes and the exempt child's lifecycle is not
mutated by the side-effect re-screen._child_is_exempt helper so the
markdown report and the --json payload can never drift.in_review/done exemption, inherited_pass pass-through,
deleted-child handling, content-fingerprint freshness/reuse, and
--audit-children opt-in semantics are unchanged.Without audit_runner.py (always --json; ORIG_STATUS = pre-audit status):
wl update <id> --status in_progress --json
# Yes (top-level, no parent): wl update <id> --status completed --stage in_review --needs-producer-review yes --json
# Yes (child item): wl update <id> --status completed --stage in_review --json
# No: wl update <id> --status open --stage plan_complete --json
# Fail: wl update <id> --status "$ORIG_STATUS" --assignee "" --json
An audit targets the project root that owns the work item (LP-0MSQ32HNR007AI6B, revised by SA-0MSRLECW2001AA15). Before any phase runs ā and before any pi/model call ā the runner resolves the launch context and the per-audit target root:
--worklog-dir takes precedence: its parent is the expected project).
The resolved owner becomes the per-audit target root, read by every
scope-sensitive consumer (git-derived content, code-quality scan,
remediation loop, repo-index fallback, debug-log directory, and the F3
suite-execution cwd). Launching from a different, determinable cwd
is no longer fatal: the run proceeds against the owning project and
emits a one-line stderr warning naming the launch and owning roots.
Only undeterminable ownership (no --worklog-dir, unknown prefix,
no sibling match) aborts with Error: Undeterminable project scope: ...
and a non-zero exit ā zero pi calls, no status lifecycle, no persisted
report. The warning channel is stderr only, so it never perturbs the
persisted report or the --json output (SA-0MSRLECW2001AA15).Error: Audit scope error: ... and a non-zero exit
instead of emitting misleading "unmet" verdicts. The check also fails
closed when the item's resolved touched-file set is non-empty and NONE
of those files appear in the manifest ā so a manifest that references
the repo but omits the item's own committed files can never be accepted
(SA-0MUJNZ5RN0078B5M, SA-0MUKCOW1I001MJ7O).wl audit-set rc!=0, e.g. "Work item not found") aborts the run with a
non-zero exit ā a parent report whose child audits never landed is
misleading. PERSIST_CONTENT_INVALID (fallback notice persisted) stays
a warning; the child audit is usable.A mis-scoped audit is indistinguishable from a failed audit, so an
undeterminable launch MUST fail fast (seconds, no pi calls) rather than
waste model time. To launch with zero ambiguity, cd into the owning project
root (a worktree of the owning project counts as owning) or pass
--worklog-dir <owning>/.worklog.
Git scope follows the worklog (SA-0MSLLGDW00098UCC). The runner's
git-derived content ā the file-scope manifest (changed-file list + repo
index), HEAD attestations, working-tree hashes, and --green-run evidence
ā resolves against the worklog-derived owning project root
(--worklog-dir parent, else prefix-to-sibling scan), not the launch cwd.
Launching from any cwd, the git-derived content reflects the audited
project's repository, and an undeterminable owning root aborts with
Error: Undeterminable project scope: ... before any phase runs (never a
silent fallback to the launch cwd's repo). Launching from inside the
owning project ā or a worktree of it (same git repository) ā keeps git
resolving to that checkout, so worktree-only changes and the worktree
branch HEAD stay correct. The remaining TARGET_PROJECT_ROOT consumers
(code-quality scan, remediation loop, _repo_index fallback, debug-log
path, and the F3 suite-execution cwd) now read the per-audit target
root (SA-0MULGDABC001T11L), so a foreign-cwd launch never scopes them to
the launch directory. The module-level TARGET_PROJECT_ROOT is retained
only as the cwd-derived fallback for direct/unit callers.
Audits can run for hours; the launch ā monitor ā abort contract is in docs/dev/audit-skill-reference.md. Summary:
~/.audit_debug/<project>/; 180-min budget.wait_for_job.py, then kill -0 <pid> + tail -50 <log> (markers: Phase 1 passed: running Phase 2 deep code analysis..., Skipping Phase 2 deep analysis: effort=... risk=..., Per-call timing:); confirm log growth.pkill -TERM -P ā kill ā -KILL); restore pre-audit status/stage only if the runner didn't complete its lifecycle (never demote in_reviewāopen); append failure notice (progress summary: elapsed time, last phase marker, trigger) and report the outcome to the operator (run id, log path, trigger, restored status/stage). Never fabricate a report or override a completed verdict.The runner also protects individual Pi calls in-process, so the external monitor is a backstop rather than the primary abort mechanism (LP-0MSQ32S2M001EA74):
AUDIT_CHILD_SCREEN_TIMEOUT env or --child-screen-timeout flag). A screen that exceeds its budget returns a clean timeout verdict (_timeout marker + timeout evidence) ā never a full 1800 s burn. Phase 2 calls (parent + child deep analysis) and parent Phase-1 screens keep the 1800 s budget.AUDIT_STALL_TIMEOUT seconds (default 600 = 10 min) is aborted in-process inside _call_pi (kill + drain) with a _timeout verdict and stall evidence, instead of waiting out the remaining per-call budget. The external stale-log abort (ā„10 min) remains as a backstop.Phase 1 parent screening, Phase 1 child screening (<id>), Phase 2 deep analysis, Phase 2 child deep analysis, Phase 2 batched deep analysis) via _phase_label(context) (SA-0MT6EZUS9004FJ9T AC4) ā the persisted report states which phase timed out rather than a bare "Manual audit required."/llama/local/status ā available_slots/total_slots; AUDIT_SLOT_STATUS_URL, which overrides verbatim, otherwise the endpoint is derived from AUDIT_PROXY_BASE_URL so it shares the proxy host; short 1 s timeout, fail-open). The ceiling is min(free-slots, configured_max) with a floor of 1; when the slot query fails the runner degrades to the configured static ceiling (AUDIT_MAX_CHILD_CONCURRENCY > AUDIT_PARALLELISM > 2).Child audits in the main LLM slot (SA-0MT2XRGEU0009QRE): config gate between two child-audit execution modes ā env var AUDIT_CHILD_IN_MAIN_SLOT and CLI flag --child-in-main-slot (flag wins), default true (in-main-slot mode):
In-main-slot mode (default): child Phase-1 AC-review screens and Phase-2 child deep analysis run in the main LLM slot ā no new pi subprocess session is spawned per child. The runner emits structured [AUDIT_IN_MAIN_SLOT_WORK] work items (the exact screen/deep prompt the invoking agent session should run inline) and a [AUDIT_IN_MAIN_SLOT_COMPACT] /compact instruction after each child audit before continuing ā the session compacts so context stays within budget while auditing many children. Child verdicts are left pending (partial, "main-session review") until the main session performs the child audit and persists it; re-running the parent audit then reuses the fresh child audit with zero pi calls (existing child-verdict reuse). In-main-slot mode is sequential (one main slot): children run one at a time with a /compact between them; Phase-2 batch mode (--batch-phase2) is not applicable in this mode.
Separate-process mode (gate false): the unchanged historical path ā a pi subprocess session per child for Phase-1 screens and Phase-2 deep analysis, with the slot-aware concurrency ceiling above. Set AUDIT_CHILD_IN_MAIN_SLOT=false or pass --no-child-in-main-slot to restore it when the main-slot mode misbehaves.
Separate-process child output parsing (WL-0MU0FHSEP003PCDY): each child subprocess runs pi -p --mode json; its stdout is parsed by extract_pi_text with a recovery pass (extract_pi_text_with_fallback in scripts/pi_utils.py) that tolerates alternative stream shapes (e.g. a string-encoded assistantMessageEvent) before ever declaring the output unparseable. When a child response cannot be parsed into a verdict array, the runner issues exactly one bounded JSON re-ask (verdict_reask) ā scoped to the child Phase-1 screen budget for child screens ā and only on re-ask failure falls back to partial verdicts. That fallback is explicit, never silent: the AC evidence names the failure phase (Phase 1 child screening / Phase 2 child deep analysis), includes a truncated head+tail of the raw subprocess output when captured, and the child_ac_fallback debug-log entry records phase and raw_stdout_truncated (via --debug-log) so a genuine format mismatch is diagnosable without demoting a valid model response to partial (no false cascade to plan_complete / parent Ready to close: No).
Manual verification (WL-0MU0FHSEP003PCDY): to observe the separate-process child parse/retry/diagnostic behaviour end-to-end, run a parent audit with --no-child-in-main-slot --do-not-persist against a work item with at least one unaudited child, with AUDIT_CHILD_SCREEN_TIMEOUT set low (e.g. 120) so a screen that cannot answer fails fast instead of stalling: debug_log entries of the form child_ac_fallback carry phase=Phase 1 child screening plus raw_stdout_truncated; a healthy run records no such entry and the child's ACs persist met. The recovery path is unit-verifiable without a model: python3 -c "from scripts.pi_utils import extract_pi_text_with_fallback as f; print(f('{\"type\": \"message_update\", \"assistantMessageEvent\": \"{\\\"type\\\": \\\"text_end\\\", \\\"content\\\": \\\"[{\\\"index\\\":0,\\\"verdict\\\":\\\"met\\\"}]\\\"}\"}'))" prints the verdict JSON (string-encoded assistant event) that the primary extractor skips.
Child-audit execution contract (SA-0MU32TE120012T49): in-main-slot mode never presents a pending child as a normal result. The report's Ready to close: header is immediately followed by an explicit Child screens pending: N (main-slot) ā <ids> gate, the summary repeats it, and Ready to close: Yes is impossible while any child screen is pending. A resumable signal (.worklog/audit-checkpoints/<id>.pending-screens.json) records the outstanding child ids and is cleared automatically once none remain; perform each emitted [AUDIT_IN_MAIN_SLOT_WORK] screen, persist the child audit, then re-run the parent to complete exactly those children without re-running completed phases.
Unserviced in-main-slot assessment (SA-0MVAWFUU0004VW45 / SA-0MVAWFYZE0040SQ3): a child whose [AUDIT_IN_MAIN_SLOT_WORK] screens were emitted by a previous run and never serviced ā its ac_results still carry the partial + main-session review pending marker AND the previous run's <id>.pending-screens.json signal still lists it ā must not re-block the parent's readiness decision on a re-run. The parent reads the stale pending-screens signal once and treats each such child as: inherited when a previously STORED child verdict exists (reused_from / child_audit_ready ā the child-verdict reuse markers), so the stale partial ACs no longer count against closure; otherwise not-assessed ā its partial ACs still block (Ready to close: No), because the child was never judged rather than passed. Both the markdown report (Child assessment reasons: [<reason>] <id>, ā¦) and the JSON payload (child_reasons, not_assessed_children) carry a machine-parseable diagnostic per child, using reason codes unserviced_in_main_slot (stale pending marker present) and not_assessed (genuinely not-ready child with no stored verdict). Interactive runs are unchanged: a child whose screens the invoking session did service carries a fresh stored verdict with no pending marker and is classified assessed (no diagnostic; ready path unchanged), and screens freshly emitted in the current run (no stale signal) still force Ready to close: No via the child-audit execution gate above.
Short-circuits item-level audits when a recent, valid audit exists. Full behavior in docs/dev/audit-skill-reference.md:
git log --all --grep=<id> --name-only), commit hashes recorded in the item's comments, and the description's Key Files. Each touched path contributes its committed state (blob hash at HEAD, else latest touching commit) plus its narrowed working-tree state (git status --porcelain -- <paths> + git diff --name-only HEAD -- <paths>); unchanged ā existing report. A change to any touched file (committed or uncommitted), or to the description/Key Files, invalidates; an unrelated commit or working-tree change elsewhere no longer does (SA-0MSKB6US1009CNHT, SA-0MSL1YXG7004F2BZ). Comment-consistency contract: _compute_content_fingerprint is the single hashing function and always resolves the item's comments itself when the caller does not supply them, so every call site ā the item's own audit and freshness check, the parent child-reuse gate, the read-only check-freshness probe, the remediation re-hash, and child-audit persistence ā contributes the identical touched-file set and therefore the identical fingerprint (SA-0MVAWFYKT002CQ3L). Call sites that already hold the comments pass them through; the rest fetch them via wl show.auditedAt vs updatedAt + 60s).Skipping: audit still fresh ā Ready to close: Yes (audited <ISO timestamp>)), exit 0, no lifecycle/persistence.--force bypasses. Config: AUDIT_FRESHNESS_BUFFER_SECONDS = 60.ProjectSettings/**, *.meta) are always invalidating when touched ā no ignore-list is applied.Before starting a full audit (or re-audit) of an item, always run the coordination check ā this prevents redundant re-audits that repeat work another session already did at the same HEAD (the 2026-08-12 release wasted ~1h re-auditing SA-0MSOK041Z0027964, which another session had already completed + audited at the same HEAD):
wl audit-show <id> --json # existing audit + auditedAt + rawOutput verdict
wl comment list <id> --json # recent session activity on this item
Then decide:
completed/in_review with a fresh
audit (content fingerprint unchanged ā the runner's freshness gate would
short-circuit anyway) unless the work actually changed (a new commit or
an uncommitted edit touching one of the item's files, or an edited
description/AC/Key Files, invalidates the fingerprint and makes the stored
audit stale; unrelated repo commits no longer do ā SA-0MSPZDALB000S18P).--force is the
only way to bypass it and must be justified (stale fingerprint, changed
code, or an explicit operator request).wl audit-show shows a fresh audit with verdict Ready to close: Yes
at the current HEAD, treat the item as audited ā do not launch a new run.Full behavior: docs/dev/audit-skill-reference.md.
[READ-ONLY AUDIT]; persistence [PERSIST-AUDIT].confident-false-positive, the pipeline MAY apply a MINIMAL, surgical config fix to silence it: per-file-ignores entries for the flagged file+rule pairs only, in ruff.toml or the pyproject.toml [tool.ruff] section (created if absent), committed locally (no push) with the work item referenced; the content fingerprint is re-hashed after each commit and the code-quality scan re-run (fix=False, same changed-file scope ā the pipeline is never restarted). Capped at 3 config-fix iterations per audit run (AUDIT_REMEDIATION_MAX_ITERATIONS); a finding persisting past the cap stays blocking genuine annotated "remediation loop exhausted". uncertain and non-blocking (medium/low) findings never enter the loop. Each applied config fix is tracked by a chore work item linking the finding + commit sha, and each medium/low confident-false-positive finding gets a tracking chore (no commit link, annotated "candidate false positive ā producer decision required") ā see SA-0MST01PQQ009T0CI. The exception applies ONLY when the model classifies the finding confident-false-positive AND the no-breakage verification (T3) is green; it never closes or deletes work items and never pushes. Full documentation in the audit skill reference (D1).chore work item to track a false-positive config fix or a medium/low confident-false-positive finding (the ONLY relaxation of the no-create rule ā SA-0MST01PQQ009T0CI). Chore creation is fail-safe: a wl create failure never reverts the remediation commit; the finding stays blocking genuine and the failure is recorded in the report. uncertain and genuine findings never get a work item.wl commands outside the authorized flow.--debug-log appends raw Pi output to JSONL.Phase 1 (merge gate + linters + children stage + surface AC pass)
ā Decision Gate: blocking? ā "partial", skip Phase 2
ā (no blockers)
Phase 2 (model verifies code against each AC)
in_review/done), (3) surface AC assessment. Blocking: critical/high findings (unless screened as a confident false positive), or any non-deleted child not in in_review/done that was not exempt at audit start (the child-exemption snapshot, SA-0MUJAPC680078396 ā see Status Lifecycle).dev branch. The item's integration evidence is resolved GENERICALLY from the item itself (owning repo via the worklog prefix-to-sibling scan; commits from the item's description/comments; the wl-<id>-* feature branch via git for-each-ref) ā never a hardcoded commit or repo. Each candidate is verified with git merge-base --is-ancestor <commit> origin/dev; the command + result are recorded in the report's ## Merge Gate Evidence (Phase 1) section. When the work is NOT merged the gate integrates it into the owning repo's dev (fetch dev, fresh worktree at origin/dev, merge/cherry-pick the item's branch/commits, build, run the project test suite, push dev ā NEVER main), then re-verifies the pushed commit is an ancestor of origin/dev. When integration cannot complete (missing commit, conflicts, build/test failure, push failure, any blocker) the audit FAILS CLOSED: "Ready to close: No" + --needs-producer-review yes, and the pipeline never proceeds past Phase 1 with unmerged work (AC3). Fail-open exceptions (never blockers): an item with NO resolvable evidence (docs/admin item, no feature branch, no referenced commits) and an owning repo with NO dev baseline (neither origin/dev nor a local dev ā no dev target to be missing from). Items whose work is already in dev, and repos lacking any dev baseline, simply record the evidence and proceed.origin/dev, but ALL git-derived audit content (HEAD sha, file-scope manifest, changed files, working-tree hash, green-run attestation) resolves against the local checkout at launch. When the item's verified commits ARE on origin/dev but the local HEAD does NOT contain them ā a stale checkout (e.g. main dev behind origin/dev) ā the audit would run against a tree MISSING the delivered work and report misleading unmet/partial verdicts. In that case the gate now aborts before any Phase 1 screening with a loud, actionable error (non-zero exit; never a persisted verdict/report): "audit base is STALE ⦠run git fetch origin && git pull origin dev in the owning project's main checkout, or launch the audit from a checkout containing the delivered commit". The abort is independent of --force and does NOT demote the item lifecycle (the pre-audit status/stage is restored ā the ITEM is fine; the checkout is stale). The guard also covers the integration path (in-progress audits from a stale main checkout: after the gate integrates+pushes, the local base still lacks the work ā abort with the same remediation). Fail-open: a candidate not on origin/dev (context citation, unresolvable sha) is skipped; a git error verifying presence in HEAD treats the commit as present; an item with no resolvable evidence is untouched._screen_ruff_findings in audit_runner.py, context false-positive-screen, child-screen timeout budget) into genuine / confident-false-positive / uncertain with per-finding written justifications, surfaced in the report (#### False-positive screen table) and _build_issue_json (code_quality.false_positive_screen). Caution-first: a finding missing from the batch, unparseable output, provider error, timeout, concurrency-limit marker, or pi failure defaults EVERY finding to uncertain (never confident-false-positive) and marks infra-failure provenance (ac_fallback_used) so a failed screen restores the pre-audit state instead of demoting. Only confident-false-positive critical/high findings stop blocking closure; uncertain findings stay blocking annotated candidate false positive ā producer decision required. Medium/low confident-false-positives are classified and reported but never flagged remediable (remediation is blocking-severity only ā F2/T2 scope). The screen is skipped entirely (zero Pi calls) when the scan yields no ruff findings; non-ruff findings are never sent to it.evidence as a structured JSON object ({file, line, note}) instead of the requested path/file:line string. All evidence consumers normalize through the shared _evidence_text() helper (dict/list ā json.dumps, other scalars ā str()), and Phase 2 merge sites normalize before writing into ac_results ā so gap mapping, file-scope refs, infra-marker detection, and report assembly never crash or silently miss on non-string evidence. Verdict semantics and conservative fail-closed gap mapping are unchanged.phase2_deep, child phase2_child, batch phase2_batch) instruct the model to cite at most N file:line references per criterion, minimum 1 ā a prompt-level bound that shortens evidence-JSON generation (the dominant Phase 2 cost) without changing the model or verdict semantics. Default N is 5; resolve via --max-citations-per-ac N (highest) or the hardcoded default. Invalid values (0/negative/non-int) fail closed to the default with a warning. Parsed evidence/verdicts are never mutated ā the canonical report format is preserved. Trade-off: fewer citations per AC shortens deep analysis but narrows evidence breadth; the ā„1 file:line floor keeps every verdict substantiated.effort ā {Extra Small, Small} and risk = Low, Phase 2 deep analysis is skipped ā Phase 1 verdicts stand unchanged (met remains met) and the report/evidence records the skip reason. The rule applies tree-wide: the parent and every child in the cascade are evaluated independently against the criterion. Fail-closed: missing/unknown effort or risk ā Phase 2 runs as usual (never skip on absent data). No override flag or env var forces deep analysis for a qualifying node ā the skip is unconditional.met/adjusted, (2) all active children in_review/done (empty stage excluded) or snapshot-exempt at audit start (SA-0MUJAPC680078396), (3) no critical/high findings.IMPORTANT: Release process constraints are NOT audit concerns. The ONE exception is the Phase 1 merge gate (SA-0MT456M27001LRTL) above: it verifies the audited item's work is integrated into its owning repo's
devand fails the audit closed when integration cannot complete. Every other deployment/release criterion stays out of scope.
Phase 1 parent + child AC screening runs on the resolved model by default; a distinct phase-1 model may be supplied programmatically (direct callers) via cmd_issue(..., phase1_model=...) / _AuditContext.phase1_model, in which case Phase 1 screens on it while Phase 2 deep analysis keeps the resolved model. There is no config-file or CLI flag for this (the config system was removed in SA-0MUQ0I0YY000JCB5):
DEFAULT_MODEL, Local Proxy/plan) ā behaviour is byte-for-byte identical to a single-model audit.docs/dev/audit-phase2-measured-report.md). Per-call Per-call timing: stderr lines remain the observability surface; each line appends model=<name> so the serving model is visible per call.partial diagnostics. With no distinct phase-1 model the retry is a no-op. An infra failure on the fast attempt that succeeds on the full-model retry keeps the conservative ac_fallback_used provenance (restore-not-demote).With --model-source, a metadata line goes after Ready to close: in issue/child reports (project reports NOT modified):
Model: <model> (provider: <source>) (e.g. Model: Local Proxy/plan (provider: local), Model: gpt-4 (provider: remote))Model: manual (no provider)<concise 2-4 sentence summary>
| # | Criterion | Verdict | Evidence |
|---|---|---|---|
| 1 | <text> |
met/unmet/partial/adjusted | <file:line ā note> |
If no ACs found: "No acceptance criteria defined."
Only when at least one criterion is adjusted:
| # | Source | Criterion | Justification |
|---|---|---|---|
| 1 | <id> |
<text> |
<reason> |
<child-title> (<child-id>) ā <status>/<stage>| # | Criterion | Verdict | Evidence |
|---|---|---|---|
| 1 | <text> |
met/unmet/partial/adjusted | <file:line> |
No children ā "No children."
Automatically added by the runner; do NOT construct manually (empty: No code quality issues found.).
| # | Severity | File | Line | Message | Linter | Code |
|---|
Synonym for "Acceptance Criteria"; Acceptance Criteria is canonical.
$(skill_path audit)/scripts/audit_runner.py ā audit_runner.py issue <id> / audit_runner.py project / audit_runner.py batch; flags: --do-not-persist, --timeout, --parent-timeout, --batch-phase2, --child-in-main-slot / --no-child-in-main-slot, --max-concurrency N, --green-run (SHA|HEAD), --run-tests, --no-execute, --audit-children, --max-child-audits N, --max-citations-per-ac N, --pi-bin, --model-source, --debug-log, --json, --force, --worklog-dir DIR, --checkpoint-dir DIR, --no-checkpoint, --batch-drain / --no-batch-drain (issue), --max-items N / --timeout S (batch).$(skill_path audit)/scripts/persist_audit.py ā persist from stdin, file, or CLI string; cwd-independent ā the worklog store is auto-resolved from the work-item id prefix (prefix-to-sibling scan, cwd-chain fallback) when --worklog-dir is omitted, so it persists to the item's own store from any cwd (SA-0MSKQERKH002IBLG).Flag semantics and env-var overrides (timeouts, concurrency, retry, green-run, test-cache auto-verification, --run-tests, batch/parallel Phase 2, tools-enabled invocation, bounded scanning, debug logs, file-scope manifest, child verdict reuse, phase-1/2 performance, phase checkpoints) are fully documented in docs/dev/audit-skill-reference.md. Execution-dependent ACs can also be verified via the test skill (/skill:test).
Phase checkpoints (SA-0MT6EZUS9004FJ9T): audit_runner.py issue persists per-phase partial results (phase1_parent ā phase1_children ā phase2) to a JSON checkpoint keyed by issue id + git HEAD sha, so an audit killed by the parent Pi timeout resumes from the furthest completed phase instead of re-running the whole pipeline. Resume is automatic on the next issue run (same HEAD) ā completed phases are skipped (e.g. a completed parent screening SKIPS the Phase 1 Pi call), the resume + interrupted phase are reported on stderr, and the checkpoint is cleared after a successful run. Stale checkpoints from a different HEAD or a --force run are never reused. Config: --checkpoint-dir DIR / AUDIT_CHECKPOINT_DIR (default <owning>/.worklog/audit-checkpoints); --no-checkpoint disables it entirely. Checkpointing is best-effort and OFF for audit project; failures degrade silently without affecting the verdict or exit code.
Batch drain (SA-0MTG5TP5Z008QBL5): the runner can process MULTIPLE queued work items per dispatch window instead of one item at a time. Production: the dispatcher enqueues pending work items as work-item ids (their priorities) into the audit-batch priority queue, or simply runs audit_runner.py batch [--max-items N] [--timeout S] ā the batch subcommand PRIMES the queue from the in_review backlog (oldest-first, critical > high > medium > low, capped at N) when it is empty, then drains it. The drain loop dequeues in strict priority + FIFO order, audits each item via the full single-item pipeline (including persist + verified lifecycle), releases the concurrency slot between items (the previous item's pi calls fully complete before the next is dequeued), and stops when the queue is empty, --max-items N (default 5, env AUDIT_BATCH_MAX_ITEMS) is reached, or the wall-clock budget elapses (default 1800s / 30 min, env AUDIT_BATCH_TIMEOUT). After a successful issue audit the runner ALSO auto-drains when the batch queue holds >= 2 pending items (AUDIT_BATCH_DRAIN=0 or --no-batch-drain opts out; --batch-drain forces on). Each window logs an observability line ā batch_start, batch_end, items_processed, queue_remaining, items_included ā to stderr. The batch queue (audit-batch) is separate from the per-launch admission queue (audit), so draining never steals a live launch's admission ticket (SA-0MTG5RYH8005RQNM).
Automatic full-suite verification (SA-0MSIU5HFI0024D7W / SA-0MSJELL44009XYIL): the runner auto-verifies execution-dependent ACs from a green cached full-suite run (read-only query_cached(), never executes the suite). The audit accepts gate-produced evidence within its own 24h acceptance window (AUDIT_CACHE_TTL_SECONDS): the dev-push pre-push hook and the ship release gate run the full suite at the exact pushed/merged git state and cache it ā the audit consumes that evidence read-only instead of re-running it, even when the item was pushed more than 2h earlier or the audited HEAD has since moved (SA-0MVECQUGF0064U81). Failed runs are NOT extended: a non-zero-exit entry still expires after the short 5-min TTL (SA-0MSJELL44009XYIL). Cache entries are scope-filtered (SA-0MT6BYQHB008DOGC): only entries whose metadata records scope == "full" count as full-suite evidence ā a changed-scope cache entry (a partial, file-scoped run) is classified as a MISS and never satisfies a "full test suite passes" AC; when only a changed-scope entry exists the runner auto-executes the full suite (F3) or reports partial with a clear diagnostic. Suite commands are repo-aware ā only node suite dirs that exist under the target repo are required (tests/node, tests/cli, tests/unit; missing dirs skipped), so any layout can auto-verify. When verification fails, the runner prints a clear diagnostic distinguishing a cache miss (run /skill:test / run_tests.py --force once at HEAD to populate the cache, then re-audit) from a non-zero cached run (suite is red ā fix or attest with --green-run HEAD); execution-dependent ACs stay partial. Failed runs get a short 5-min cache TTL so transient infra failures are not re-served as current results.
Default auto-execution on cache miss (F3, SA-0MSTN5KRF0097TVP): when the read-only cache cannot satisfy the evidence (a cache miss ā no cached green full-suite run at HEAD and no --green-run attestation), the audit now AUTO-EXECUTES the repo's actual suite via the test skill (run_tests.py / full_suite_commands), triages any failures per the test skill, and refreshes the per-repo cache so subsequent audits auto-verify read-only. A green executed run injects the TEST-SKILL GREEN RUN evidence block (execution-dependent ACs MAY be marked met); a red run is fail-open ā no block, execution-dependent ACs stay partial with failure evidence, and the audit continues (never blocks). Operators can opt out with --no-execute / AUDIT_NO_EXECUTE=1 to proceed fail-open partial without executing the suite; --run-tests remains the explicit override that executes on ANY non-green state.
Nested-suite recursion guard (SA-0MUG47DYG006TV40): when an outer test-suite run already owns the checkout, the F3 auto-execution stands down with a clear diagnostic rather than re-entering the suite. The runner reuses the existing LIVE_REPO_GUARD_ACTIVE marker (set by run_tests.py while it owns the checkout and consumed by live_repo_guard.py to make nested pytest/run_tests.py stand down); _suite_run_in_progress() consults it before executing, and audit_runner() propagates it into every child-audit subprocess environment. This bounds the child-audit cascade: a child audit spawned by an audit test cannot itself trigger a full-suite execution, so the recursion cannot recurse (the per-run child cap, --max-child-audits / AUDIT_MAX_CHILD_AUDITS, remains and bounds breadth within one process). Stand-down is fail-open: execution-dependent ACs stay partial with a documented reason and never produce a degraded "met" verdict. Standalone production audits (no outer suite run) keep F3 behaviour unchanged. --run-tests is the explicit operator override and still executes. The F3 execution is also paced (SA-0MUJK94QN0015925): it routes its runner through run_tests.paced_runner(), so a real execution acquires the shared "test" semaphore (TEST_MAX_CONCURRENCY / TEST_LOCK_TIMEOUT) before spawning ā audit-triggered runs cannot saturate the host alongside run_tests.py / implement-gate runs. On saturation the audit fails open with a clear notice. Audit tests that exercise the child-cascade path must mock the child-audit subprocess (subprocess.run) so no real audit_runner.py issue child is spawned ā see test-writing guidelines. Full contract: docs/dev/audit-skill-reference.md.
Never-block guarantee (F4, SA-0MSTN8CWM003AAU9): the audit NEVER exits with a hard block solely because it cannot run tests ā no cache, no test runner, no configured suite commands, execution impossible. The old pre-flight hard gate (SA-0MSQ72BVV0011SRU) was removed; every such case degrades to a fail-open partial verdict with a clear diagnostic. Verification order for execution-dependent ACs: (1) read-only cache (query_cached(), green within the audit's 24h acceptance window ā AUTO-VERIFIED GREEN RUN) ā (2) auto-execute on a cache miss via the test skill (green ā TEST-SKILL GREEN RUN; red ā partial + triaged test-failure items) ā (3) partial with documented reason (red/error/empty cache states, --no-execute, or an unresolvable suite command set).
Per-project suite extension (F2, SA-0MSTMYE79006NA61): a .pi/test-config.json file at the repo root overrides convention detection ā {"suiteCommands": ["..."], "timeoutPerCommand": 600} ā so a bespoke suite (e.g. a monorepo package command) is executed exactly as configured. Resolution order: extension file > npm-test convention (npm --silent test) > pytest (only when the repo declares a pytest suite) + node suite dirs (tests/{unit,node,cli} that exist).
Context reduction + isolation (SA-0MSISKM8F004NW1U, SA-0MUEIOGT2005IR7F): every _call_pi runs with --no-context-files --no-skills --no-extensions in both tool-enabled and tool-less modes. Audit prompts are fully self-contained ā they carry the read-only mandate, JSON output format, FILE SCOPE manifest, SCANNING block, and criteria ā so the duplicated global+project AGENTS.md load and the skills section are dropped from each session's static context (an 88% reduction, ~23x margin under the 10K-token bound; prompts must never depend on AGENTS.md, skill descriptions, or extension-provided behaviour ā that is an invariant of this skill). --no-extensions additionally keeps interactive/UI extensions (compaction mirroring, status line, ContextHub recovery) out of audit sessions: after a session replacement a stale ExtensionContext makes their async callbacks throw outside pi's try/catch and crash the pi process, which surfaces as a spurious provider_error: Connection error. and blocks the audit/ship gate. Per-call timing + verification script + recorded AC2/AC3 evidence: docs/dev/audit-skill-reference.md, evidence/.
Session-id traceability (SA-0MSNYMKV7005P0H9, SA-0MSNYWMJJ002CIJ7): every pi subprocess invocation carries a descriptive --session-id so sessions can be traced back to the work item being audited. _call_pi() builds audit-{issue_id}-{context}-{uuid8} (8-hex-char UUID, fresh per invocation; colons in context values like child:SA-XXX are replaced with underscores so the id passes assertValidSessionId), or audit-project-{uuid8} for project-level audits (the repeated project context is deduped); audit_pr.py's run_audit_in_worktree() builds audit-pr-{wl_id}-{uuid8} but only when dry_run=False. Sessions appear under ~/.pi/agent/sessions/ keyed by the descriptive id instead of a random UUID. After each completed issue/project run the runner prunes stale audit--prefixed sessions older than a retention period (default 112 days; --session-retention-days flag or AUDIT_SESSION_RETENTION_DAYS env var, flag wins; session directory via --session-dir or PI_CODING_AGENT_SESSION_DIR) ā only audit- sessions are ever touched. See docs/dev/audit-skill-reference.md (Session-id traceability & retention).
audit_runner.py) is the CANONICAL audit path ā its verdict is authoritative and MUST NOT be overridden later.--force or clear directive); never demote a runner "Yes" without fresh, documented evidence; don't run the manual path if the runner already reported.Ready to close: header and canonical sections (pipeline sections above are normative).effort ā {Extra Small, Small} and risk = Low skip Phase 2 ā Phase 1 verdicts stand unchanged, evidence notes the skip, and the rule is fail-closed (missing/unknown values ā deep analysis runs). Applies independently to the parent and every child in the cascade; unconditional (no override flag/env).in_review/done block Phase 2. AC ambiguity, medium warnings, or preference are NOT valid reasons to skip.met/adjusted, all active children in_review/done (or snapshot-exempt at audit start, SA-0MUJAPC680078396), no critical/high findings. Children in in_review do NOT block closure ā only pre-review stages do.dev and fails closed when integration cannot complete; everything else stays out of scope.$(skill_path audit)/scripts/persist_audit.py with [PERSIST-AUDIT].python3 $(skill_path audit)/scripts/persist_audit.py --issue-id <id> --report "<report>" (or echo-pipe; runner audit_runner.py issue <id> persists and verifies unless --do-not-persist). The persister targets the work-item's own worklog store from any cwd (auto-resolved via the shared prefix-to-sibling scan when --worklog-dir is omitted; an explicit --worklog-dir keeps highest precedence).Readback verification is an invariant: runner reads back via
wl audit-show <id> --json(audit exists,rawOutputnon-empty, content references the ID) or exits non-zero. With--do-not-persistthe readback of the full report is intentionally skipped (norawOutput); instead, onReady to close: Yesthe runner persists ONLY the freshness signal (auditedAt/auditResultviawl audit-setwithout--raw-output) so Herdr/DOWNTIME see the item as freshly audited, and exits non-zero if that refresh fails.rawOutputstays null. The dry-run refresh is applied after the terminal status/stage transition, soauditedAt == updatedAtholds for an item already inin_review.
wl audit-show <id> --json must show success=true, audit not null, rawOutput non-empty with Ready to close: marker.Yes ā "Audit passed. The item is ready for release."; otherwise ā "Work item is not ready to close (see above), would you like me to address the gaps in the audit?"Critical:
persist_audit.py/wl audit-setmay return success without storing ā always verify withwl audit-show.
persist_audit.pyinternally callswl audit-setthenwl update --audit-textwithout--stage. Adding--stagewould cause worklog'sdb.update()to bumpupdatedAt, potentially invalidating freshness checks ifupdatedAtadvances pastauditedAt + 60 s(SA-0MTHC710X003ORZM). The status/stage transition is applied separately by the runner's_apply_terminal_lifecycleafter persistence completes.- Ordering contract (SA-0MTHC710X003ORZM): any
wl comment addthat records an audit result MUST be ordered beforepersist_audit.py/wl audit-set. The persisted audit report itself is the record of the audit ā no separate post-persist audit-result comment is needed. A comment added afterwl audit-setbumpsworkItem.updatedAtviatouchWorkItemUpdatedAt(), which can pushupdatedAtpastauditedAt + 60 sand makeisAuditFreshreturn stale (ā³) for a just-persisted passed audit. If a post-persist comment is unavoidable, re-verify freshness via the content-fingerprint gate; the 30 s persistence-write tolerance on the legacy time gate (SA-0MSI3XH34001LLU4, SA-0MTHC710X003ORZM) covers the runner's own writes only.
wl/git commands outside the authorized flow; use --debug-log for debugging.python3 $(skill_path audit)/scripts/audit_runner.py issue SA-123 # audit + persist
python3 $(skill_path audit)/scripts/audit_runner.py issue SA-123 --do-not-persist # dry run (full report not stored; a Yes verdict still refreshes auditedAt/auditResult)
python3 $(skill_path audit)/scripts/audit_runner.py issue SA-123 --force # in-progress item (bypasses pre-flight guard + freshness)
On runner failure (non-zero exit, timeout, exception), the report is wrapped with an ā Script Execution Failure: <script_name> ā <reason> banner above and below (informational, no state changes; Python module scripts.failure_notice from the shared skills-root scripts/ dir ā note it lives one level above the audit scripts, unlike the $(skill_path audit)/scripts/... runner/persister; JSON key script_failure).
persist_audit.py / wl audit-set returns success without storing ā always verify with wl audit-show --json.wl unavailable/invalid JSON ā report the error, do not claim success._extract_json_array) is documented in docs/dev/audit-skill-reference.md.The audit's persisted machine-verified report (Ready to close: header,
## Summary, ## Acceptance Criteria Status) is unchanged and remains
the authoritative audit record. Only the agent's end-of-session summary
reconciles with the canonical template ā render it as the last step:
python3 $(skill_path report)/scripts/render_report.py <work-item-id> \
--skill-name audit \
--headline "<1-3 sentence headline summary>" \
--ac "<AC# description>|<verification metric>|met" \
--ac "<...>|<...>|unmet" \
[--producer-actions "<actions for the producer, or omit for 'None needed'>"] \
[--notes "<freeform context/caveats/assumptions>"] \
[--next-action <review|plan|implement|...>]
The script prints the rendered report to stdout ā paste it verbatim into
your final response, so the operator sees the report itself (not just the
tool call), then close with: <work-item-id>: <one-line summary>. Do NOT
re-summarize the report in a different format ā the report is the summary. When the session ends in a terminal state with no open questions for the operator, end your final response with </end_session> on its own line as the very last line after the summary; if the session ends with questions for the operator, do not emit the marker.