/sg-code-audit — Parallel Codebase Audit
Dispatch parallel AI agents to audit every file in your repo. Each agent reviews a non-overlapping
zone and produces structured JSON. Audits are report-only by default; source mutation requires the
explicit --fix flag. Results appear in the /sg-visual-review dashboard under a "Code Audit" tab.
Invocations
| Command | Behavior |
|---|---|
/sg-code-audit |
Standard mode — 10 agents, 1 round, report only |
/sg-code-audit quick |
5 agents, 1 round, surface scan only |
/sg-code-audit deep |
15 agents, 2 rounds (surface + depth) |
/sg-code-audit paranoid |
20 agents, 3 rounds (surface + depth + edge cases) |
/sg-code-audit --focus=path/ |
Restrict audit scope to a directory |
/sg-code-audit --report-only |
Explicit spelling of the report-only default |
/sg-code-audit --fix |
Opt in to tier-gated source fixes |
/sg-code-audit --diff=main |
Audit only files changed since main + their importers |
/sg-code-audit --all |
Force full codebase audit (skip scope question) |
/sg-code-audit --model=opus |
Use opus for all rounds (maximum depth) |
/sg-code-audit --monitor |
Start/attach the real-time monitor dashboard without asking |
/sg-code-audit deep --model=opus --focus=src/ --fix |
Flags combine freely (mode, model, focus, scope, fix) |
Diff scope covers committed changes only. --diff uses the three-dot merge-base diff (git diff {ref}...HEAD), which sees committed work only — commit or stash uncommitted/staged changes before running in diff mode.
Sandbox note: report-only avoids source writes, but agent dispatch, Git worktrees, GitHub hooks, and local monitor POSTs may still need permissions. See ../../docs/sandbox.md.
Reference files
| File | Contents |
|---|---|
references/agent-prompt.md |
Zone-agent prompt template, severity calibration, category taxonomy, self-validation, round 2+ context blocks |
references/verification.md |
Post-merge validation commands, flow-tracer template, Phase 5.6/5.7 protocol, constitutional pre-filter |
references/output-schema.md |
Canonical output schemas, TOON format, normalization maps, lifecycle/accepted-risks procedures, risk score, route derivation, terminal summary |
references/monitor.md |
Phase 0 monitor setup, all monitor POST payloads |
references/checklists.md |
Round focus descriptions + language-specific checklists |
AUDITABLE extensions (shared list)
One list drives Phase 2 stack detection, both Phase 3 zone-discovery find commands, and the Phase 1 diff filter:
py, ts, tsx, js, jsx, go, rs, java, kt, html, css, sh, bash, zsh
Infra files (Dockerfile*, docker-compose*, *.yaml, *.yml, *.toml, Makefile, CI configs) are additionally kept by the diff filter and always form the dedicated infra zone (Phase 3).
Phase 0 — Monitor Setup
Detect or start the review server for real-time audit monitoring. Optional — if the user declines or the server can't start, the audit proceeds normally.
results_diris alwaysvisual-tests/_results/— runmkdir -p visual-tests/_resultsfirst.- Check ports 8888/8889/8890 for an existing server whose
/healthresponse reports the sameresults_dir; attach if found. - If none found: default monitor OFF. Start a server only if
--monitorwas passed, or if mode isdeep/paranoid(estimated >15 min) and the user answers yes to "Monitor progress in a dashboard?". - Store
monitor_active(boolean) andmonitor_urlas working variables.
Load references/monitor.md § "Phase 0 — Monitor Setup" for the full health-check, bootstrap, and server-start procedure. All monitor POSTs in later phases use the payloads in references/monitor.md § "POST payloads".
Phase 1 — Parse Arguments
Parse the user's input into working values: mode, focus, fix_mode, model, and scope.
Extract the first positional argument. Match against
quick,standard,deep,paranoid. Default:standard.Extract
--focus=<path>. If present, store asfocus_path. If not, scope is the entire repo.Parse mutation flags. Default:
fix_mode = false.--report-onlyis an explicit spelling of that default; only--fixsetsfix_mode = true. If both flags are present, printCannot use --fix and --report-only together.and stop before creating agents or worktrees. Also check for--monitor— it forces the Phase 0 monitor on without asking.Check for
--model=<model>. Values:sonnet,opus,auto. Default:auto.auto(default): the balanced profile — audit agents run on the strongest available model (opus) in every round, and verification agents (Phase 5.7) run on a fast verification model (sonnet).sonnet: audit agents use sonnet (fallback when the opus weekly quota is saturated).opus: audit agents use opus — the explicit spelling of theautoaudit profile.
NEVER use haiku. Haiku is permanently banned for audit agents — it fails under load (529) and produces inferior results. If a user passes
--model=haiku, printhaiku is banned for audits — using sonnet instead.and proceed with sonnet.Regardless of model, always append the mandatory anti-self-censoring instruction to every agent prompt — verbatim text in
references/agent-prompt.md§ "Orchestrator notes (do NOT paste into the agent prompt)".Parse scope flags:
--all→scope_mode = "full".--diff=<ref>→scope_mode = "diff",scope_ref = <ref>.- If BOTH are present: error. Print
Cannot use --all and --diff together.and stop. - If neither is present, set
scope_mode = "interactive".
If
scope_mode == "interactive": a. Detect base reference:current_branch=$(git rev-parse --abbrev-ref HEAD) if [ "$current_branch" != "main" ] && [ "$current_branch" != "master" ]; then if git show-ref --verify --quiet refs/heads/main; then base=$(git merge-base HEAD main) elif git show-ref --verify --quiet refs/heads/master; then base=$(git merge-base HEAD master) else base="HEAD~1" fi else base="HEAD~1" fib. Run
git diff --name-only {base}...HEAD(three-dot — committed changes only) to get changed files. c. Iffocus_pathis set, filter the changed files to that subtree before asking.--diffand--focusboth apply. d. If the diff is NOT empty, ask the user: "I detected {N} files changed since{base}. What scope? 1. Only what changed — {N} files + importers ({est} min) 2. Full codebase — {total_file_count} files ({est} min) 3. Different base". Estimates: diff ≈ ceil(diff_files / 30) min; full ≈ ceil(total_files / 200) × round_count min. Pick 1 →scope_mode = "diff",scope_ref = {base}; pick 2 →"full"; pick 3 → ask for a ref, then"diff"with that ref. e. If the diff IS empty, get the last commit (git log --oneline -1) and ask: "No diff vs{base}. Audit the last commit{sha}: {message}? 1. Last commit — {N} files 2. Full codebase 3. Different base". Pick 1 →scope_mode = "diff",scope_ref = "HEAD~1"; pick 2 →"full"; pick 3 → ask for ref.If
scope_mode == "diff": a. Get changed files:git diff --name-only {scope_ref}...HEAD→ store asdiff_files[]. This three-dot diff covers committed changes only; ifgit status --porcelainshows uncommitted or staged work, warn the user to commit or stash it first — it is invisible to the diff. b. Iffocus_pathis set, filterdiff_files[]to that subtree before import expansion. c. Filter out binary files (images, fonts, compiled assets). Keep only AUDITABLE extensions (shared list above) plus infra files (*.yaml,*.yml,Dockerfile*). d. For each changed source file, find direct importers (1 level):grep -rl "from.*['\"].*{relative_path_without_ext}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.py" --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=venv --exclude-dir=.venv --exclude-dir=dist --exclude-dir=build . grep -rl "require(.*{relative_path_without_ext}" --include="*.js" --include="*.ts" --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=venv --exclude-dir=.venv --exclude-dir=dist --exclude-dir=build .Use the relative path (e.g.
hooks/use-dossier), not just the filename stem, to reduce false matches. Deduplicate results. e. Combine:scope_files = diff_files + importer_files(deduplicated). f. Ifimporter_filescount > 3xdiff_filescount, warn:{N} files modified. Import expansion found {M} importers (noisy). Run on modified files only, or include importers?If the user picks "modified only" →scope_files = diff_files. g. Print:{diff_count} files modified + {importer_count} importers = {total} files to audith. Storescope_files,diff_files, andimporter_filesfor zone discovery.Focus-path filtering: Apply
focus_pathfiltering once, immediately after collectingscope_files(7b for diff scope, or at the start of Phase 3 Step 1 for full scope). Do not re-filter in later steps.Look up mode parameters:
| Mode | Max Agents | Rounds | Description |
|---|---|---|---|
quick |
5 | 1 | Surface scan — known patterns, lint-like |
standard |
10 | 1 | Standard audit — known patterns with broader coverage |
deep |
15 | 2 | Surface + runtime behavior analysis |
paranoid |
20 | 3 | Surface + behavior + edge cases and security |
Auto-adjust agent count: the table gives maximums. Scale to file count: agent_count = min(mode_max_agents, ceil(total_file_count / 7)) — a 34-file project in standard gets 5 agents, not 10. This prevents agents with 2-3 files each from producing shallow results.
- Store working variables:
agent_count,round_count,focus_path,fix_mode,scope_mode,scope_ref,scope_files,diff_files,importer_files. - Determine
results_dir: alwaysvisual-tests/_results/at the repo root — runmkdir -p visual-tests/_results/runsfirst. The finalaudit-results.jsonstays at the canonical root. Intermediate artifacts never do. - Record
base_sha = git rev-parse HEADonce. Createrun_id = audit-{UTC YYYYMMDD-HHMMSSmmm}-{first 8 chars of base_sha}; if that directory already exists, append the first unused numeric suffix (-2,-3, ...). Then createrun_dir = {results_dir}/runs/{run_id}. Createrun_dir, setdispatch_log_path = {run_dir}/dispatch.json, and persist the initial run record described inreferences/output-schema.md§ "Run-scoped dispatch record". Before dispatch, move any pre-existing root-levelzone-*.jsonandcross-zone-*.jsoninto{results_dir}/history/intermediate-{UTC timestamp}/without overwriting an existing archive. Record the moved paths in the dispatch log. These are legacy artifacts: never delete them silently and never aggregate them. - Before dispatch, load
{results_dir}/audit-results.jsonintoprevious_runwhen valid and copy it to{results_dir}/history/audit-{previous timestamp, colons replaced by dashes}.json(createhistory/first and never overwrite an existing archive). This early snapshot feeds lifecycle and the cross-zone tracer. An absent or malformed previous result setsprevious_run = nullwith a warning; it never blocks the new run. - If
fix_modeis true, rungit status --porcelainnow. Any output aborts before worktree creation:--fix requires a clean working tree; commit or stash existing changes first.A report-only audit may continue and must not create source commits. - Print:
Code audit: {mode} mode ({agent_count} agents, {round_count} round(s)){", model: " + model_strategy}{", focus: " + focus_path if set}{", fix enabled" if fix_mode else ", report-only"}{", scope: diff vs " + scope_ref + " (" + total_in_scope + " files)" if scope_mode == "diff"} — run {run_id} - Compute prompt hash: after Phase 2 (when checklists are known), compute a SHA256 hash of the prompt template + activated checklists + learnings audit_hints. Store as
prompt_hashand include inaudit-results.json.sg-improvereads this to detect prompt changes and flag baseline discontinuity inlearnings.yamlsession_history.
Phase 2 — Detect Stack
Scan the repository root (or focus_path if set) to identify languages and frameworks. Activate
only the relevant checklists from references/checklists.md. Every Glob below excludes .git,
dependency/vendor directories, virtual environments, and generated dist, build, and .next
trees. The checks cover every AUDITABLE extension (shared list above):
- Python: Glob
**/*.py— if matches > 0 AND (**/requirements.txtOR**/pyproject.tomlOR**/setup.py): activate Python checklist - TypeScript: Glob
**/*.tsOR**/*.tsx— if matches > 0 AND**/package.json: activate TypeScript/React checklist - Infra: Glob
**/Dockerfile*OR**/docker-compose*: activate Infrastructure checklist - Next.js: Glob
**/next.config.*: activate Next.js checklist - Go: Glob
**/*.go: activate Go checklist - Rust: Glob
**/*.rs: activate Rust checklist - JVM: Glob
**/*.javaOR**/*.kt: activate JVM checklist - HTML/CSS/JS (vanilla): Glob
**/*.html,**/*.css,**/*.js,**/*.jsx— if HTML matches > 0 AND none of the framework-specific indicators above (nonext.config.*, nopackage.jsonwith React/Vue/Angular, no*.pywith Flask/FastAPI): activate HTML/CSS/JS checklist. Covers static sites, Hugo/Jekyll output, and vanilla JS projects. - Shell: Glob
**/*.sh,**/*.bash, or**/*.zsh— activate the Shell checklist exactly once. Detect the shebang of each file before applying dialect-specific rules.
After detection, read CLAUDE.md from the repository root if it exists; store its contents (truncated to 3000 characters) for injection into agent prompts. Store detected_languages = ["python", "typescript", ...] and the activated checklist text blocks.
Print: Detected: {detected_languages joined by ", "}. CLAUDE.md: {"found" if exists else "not found"}.
Phase 3 — Discover Zones
Split the codebase into non-overlapping zones, one per agent. Zones must not share files — each source file belongs to exactly one zone.
Step 0 — previously skipped zones: if {results_dir}/_skipped_zones.json exists from a previous audit, read it, prioritize those zones (first in the dispatch queue) with max_files reduced 30% from the previous run (to avoid the same overflow), print Found {N} zones skipped in previous audit — prioritizing them with smaller sizes, then delete the file (it is recreated if zones fail again).
If scope_mode == "diff": zone discovery operates on scope_files instead of the full repo. Simplified strategy:
- Group
scope_filesby parent directory (first 2 path segments, e.g.src/routes/) - Each group becomes a zone candidate
- Group <=30 files → 1 zone; >30 files → split by subdirectory (same rules as full mode)
- Merge groups with <5 files into their nearest neighbor (longest common path prefix; if none shared, into the group with the fewest files)
- Cap to
agent_count(same merge/split logic as full mode)
Print: Scoped zone discovery: {zone_count} zones from {file_count} files (diff mode)
If scope_mode == "full": use the directory-based algorithm below.
Step 1: Count files per directory
Run with Bash (the -name list is exactly the AUDITABLE extension list):
find {repo_root_or_focus_path} \( -name '*.py' -o -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' -o -name '*.go' -o -name '*.rs' -o -name '*.java' -o -name '*.kt' -o -name '*.html' -o -name '*.css' -o -name '*.sh' -o -name '*.bash' -o -name '*.zsh' \) -not -path '*/node_modules/*' -not -path '*/vendor/*' -not -path '*/.git/*' -not -path '*/venv/*' -not -path '*/__pycache__/*' -not -path '*/.next/*' -not -path '*/dist/*' -not -path '*/build/*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn
This produces lines like 42 ./src/routes. IMPORTANT: use sort (not sort -u) before uniq -c so duplicate directory paths are counted correctly — with sort -u every count becomes 1.
Step 1.5: Read learnings (if available)
If {repo_root}/.shipguard/learnings.yaml exists, read it: zone_hints (per-path max_files overrides), audit_hints (patterns to inject into agent prompts), noise_filters (patterns to batch in agent prompts). Print: Loaded {N} zone hints, {M} audit hints, {K} noise filters from .shipguard/learnings.yaml. If absent, skip silently.
Step 2: Apply splitting rules
Use token-weighted thresholds instead of raw file counts: file_weight = max(1, file_line_count / 50); zone_weight = sum(file_weights). Approximate by sampling the first 5 files per directory with wc -l: estimated_zone_weight = file_count × avg_weight.
If a directory path matches a learnings zone_hint, use hint.max_files as the hard cap instead of these defaults:
- estimated_weight <= 40 → 1 zone
- estimated_weight 41-100 → split by immediate subdirectories; re-run the count on children
with the same AUDITABLE extension list and the same generated/vendor exclusions:
find {dir} -maxdepth 2 \( ...same -name list... \) ...same exclusions... | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn. Each child becomes a zone. - estimated_weight > 100 → split by sub-subdirectories (depth 3); each becomes a zone.
- Infra files → always 1 mandatory dedicated zone, even in
quickmode. CollectDockerfile*,docker-compose*,*.yml,*.yaml,.env*,.env.example,Makefile,*.toml(pyproject.toml, Cargo.toml),*.cfg,.github/workflows/*,.gitlab-ci.ymlin the repo root,infra/, ordeploy/. Indeep/paranoidmodes the infra zone gets its own R2 round focused on: env var consistency (referenced in code vs declared in compose), port mapping verification (code defaults vs compose ports), and healthcheck coverage.
Step 3: Merge small zones
Any zone with fewer than 5 files merges into the nearest sibling zone (longest common path prefix).
Step 4: Match zone count to agent count
- Zone count >
agent_count: repeatedly merge the two smallest zones (by file count) until equal. - Zone count <
agent_count: repeatedly split the largest zone in two (by subdirectory boundary) until equal. - Flat directory fallback: a zone with no subdirectories splits by alphabetical file list into two equal halves (handles flat
src/orlib/). - Overshoot guard: if a split would exceed
agent_count, apply the merge step immediately after to bring the total back down.
Step 5: Store zones
[
{"id": "z01", "paths": ["src/routes/", "src/middleware/"], "file_count": 28},
{"id": "z02", "paths": ["src/hooks/", "src/stores/"], "file_count": 22},
{"id": "z03", "paths": ["infra/"], "file_count": 12}
]
Print: Discovered {zone_count} zones ({total_file_count} files total). Dispatching {agent_count} agents.
Phase 3.5 — Monitor: Initialize
Runs once, after zones are known and before the round loop begins. Must NOT be repeated on subsequent rounds — re-POSTing resets all monitor state.
If monitor_active is true, POST audit-start to seed all zone state on the monitor server. Payload and overflow-children notes: references/monitor.md § "audit-start (Phase 3.5)". If the POST fails, set monitor_active = false and continue silently.
Phase 4 — Build Prompts + Dispatch Agents
The core execution phase. For each round (1 to round_count), build prompts and dispatch agents.
Round descriptions
| Round | Focus | Description |
|---|---|---|
| R1 — Surface | Known patterns, lint-like | Silent exceptions, missing guards, dead code, type mismatches, missing cleanup |
| R2 — Depth | Runtime behavior | Race conditions, cross-service integration, auth gaps, resource leaks, SSR issues |
| R3 — Edge Cases | What R1+R2 missed | Logic errors, prompt injection, data corruption, null propagation, off-by-one, performance |
Prompt template
Load references/agent-prompt.md § "Prompt template" and instantiate it per zone with {run_id},
{agent_id}, {zone}, {round_number}, {base_sha}, checklist, learnings, and fix-mode variables.
Agents write to the relative run-scoped path
visual-tests/_results/runs/{run_id}/zone-{zone.id}-r{round_number}-{agent_id}.json inside their
own worktree, and every prompt gets the mandatory anti-self-censoring instruction appended.
Dispatch
For each zone, dispatch an agent:
- Tool: Agent
- prompt: the filled prompt template
- isolation: worktree
- model: per the
--modelflag (Phase 1 step 4):auto/opus→ the strongest available model (opus) for all audit rounds;sonnet→ sonnet. NEVER use haiku — it is permanently banned. - run_in_background: true
Staggered dispatch: do not launch all agents in the same instant. Dispatch in batches of up to 10 agents per message to limit API burst load — this prevents 529 overload errors caused by too many agents requesting context simultaneously. The same batching rule applies to verification agents in Phase 5.7.
Dispatch log: after every state transition atomically rewrite dispatch_log_path using the
run-scoped schema in references/output-schema.md. Each entry contains {run_id, base_sha, zone_id, round, agent_id, status, dispatched_at, retry, artifact}. When a retry is dispatched, mark the
original agent superseded with its reason before adding the retry. A late completion from a
superseded agent is recorded but can never become the accepted artifact.
Note on worktrees: the Agent tool with isolation: worktree automatically creates a temporary
git worktree and branch. The branch name is returned in the agent's result as branch — record it
in the dispatch log for the merge phase. Inject the Phase 1 base_sha into every agent prompt: each
worktree agent verifies its HEAD equals that SHA before working, and the merge phase verifies branch
ancestry from the same immutable base.
Print: Round {round_number}: Dispatched {agent_count} agents (batches of {batch_size}). Waiting for completion...
If monitor_active, POST agent-started after dispatching each agent — payload in references/monitor.md § "agent-started (Phase 4)".
Phase 5 — Collect + Retry
As each background agent completes, process its result.
Zone-JSON transport (authoritative rule)
Zone agents write their JSON to the run-scoped relative path declared in Phase 4. The orchestrator
copies that exact file out of {worktree_path} into {run_dir} as soon as the agent completes —
always before any merge and before git worktree remove. Worktree merges never deliver zone JSONs;
this validated copy is the only transport.
On agent completion
- Read the agent's output text.
- "Prompt is too long" / "context window": the zone was too large. Split
zone.pathsinto two roughly equal groups (by file count), create zones{zone.id}aand{zone.id}b, dispatch two new agents with the same template but narrower scope, and track them. Single-path zone edge case: split the individual files under that path in half alphabetically; if the zone has fewer than 3 files, mark itfailed(cannot split further) and logZone {zone.id} too small to re-split ({N} files) — skipping.Print:Zone {zone.id} context overflow — re-splitting into {zone.id}a and {zone.id}b - Success: resolve the exact artifact path from that
agent_id's dispatch entry and copy it to{run_dir}. Validate JSON shape plus exactrun_id,base_sha,zone_id,round, andagent_id. A mismatch is stale or foreign evidence: reject it, mark the attempt failed, and retry once with the validation error. Only the newest non-superseded successful attempt may setaccepted_artifactfor a zone. Print:Zone {zone.id} complete: {N} bugs found. - Account/session capacity exhausted: detect
session limit,usage limit,quota exceeded,rate limit resets, and equivalent account-capacity messages separately from 529. Set the run status topaused_quota, store any parsed reset time, stop new dispatch immediately, and persist completed/pending/failed/superseded entries. Do not sleep until reset and do not retry the fleet. Print the reset information and how many zones remain. On an explicit conversational resume:- load the newest named paused run and verify current HEAD still equals its
base_sha; otherwise refuse continuation because the audited code changed; - dispatch one minimal read-only capacity probe that must reply exactly
SHIPGUARD_CAPACITY_OK; - if the probe fails, remain paused and launch no zone agents;
- if it succeeds, set status
runningand dispatch only pending zones, preserving completed artifacts and retry counts.
- load the newest named paused run and verify current HEAD still equals its
- API overload (529,
overloaded_error): retain bounded exponential backoff — 30s, then 60s, then 120s. After 3 retries, markfailedand add to_skipped_zones.json. Never classify an account/session quota message as 529. - Any other error: log it, print
Zone {zone.id} failed: {error summary}, add it to_skipped_zones.json, persist the dispatch state, and move on without retry.
If monitor_active, POST agent-update after processing each result (success / overflow / error) — payloads in references/monitor.md § "agent completion (Phase 5)".
Track completion: maintain counters completed, pending, failed. A re-split increments pending by 2 and decrements by 1 (net +1).
When all agents for this round are complete
Prerequisite: Clean working tree check
Run git status --porcelain. If the output is NOT empty, abort the merge phase and warn:
WARNING: Uncommitted changes detected in the working tree.
Commit or stash your changes before merging audit fixes.
Skipping merge phase — audit results are still available in worktree branches.
Do NOT merge. Continue with the zone JSONs already copied into {run_dir}.
Merge worktree branches (fix mode only)
If fix_mode is true AND the working tree is clean, for each completed zone with a worktree branch:
- Ancestry check: verify the branch descends from the dispatch base —
git merge-base {base_sha} {agent.branch}must print{base_sha}. If not, the worktree started from a stale base: skip the merge, logZone {zone.id}: stale base (expected {base_sha}), add the zone toskipped_merges. - Run
git merge {agent.branch} --no-edit(branch name from the dispatch log). - Check the exit code:
- Success (exit 0): record the merge in a
merge_log:{zone_id, branch, merge_sha: $(git rev-parse HEAD)}. Continue. - Conflict: run
git diff --name-only --diff-filter=Uto list conflicting files; logMerge conflict in zone {zone.id}: {conflicting_files}; rungit merge --abort; add the zone toskipped_merges; continue to the next branch.
- Success (exit 0): record the merge in a
IMPORTANT: do NOT use git checkout --theirs or any auto-resolution strategy. A conflict means two zones touched the same file — a zone boundary error the user must resolve manually.
After all merges:
- Verify every completed zone's accepted run-scoped JSON was already copied into
{run_dir}(Phase 5 copies at completion — including zones inskipped_merges). If any is missing and its worktree still exists, copy and validate it now, before cleanup. - Clean up worktrees:
git worktree remove {worktree_path} --force— only after step 1 confirms the zone JSON is safe. - Clean up branches:
git branch -d {agent.branch}for each merged branch, using the branch names returned by the Agent tool and recorded in the dispatch log (skip branches inskipped_merges— the user needs them). Do not glob for branch name patterns — only delete branches this run created. - If
skipped_mergesis not empty, report:Merge conflicts in {N} zones — manual resolution required: - Zone {id}: {conflicting files} These zone branches are preserved for manual merge.
Phase 5.5 — Post-Merge Validation
After all worktree merges complete (or after the clean tree check if no merges happened), validate that the merged code is syntactically correct. Audit fixes can introduce regressions — bad indentation from merge, wrong imports from copy-paste, broken syntax from adjacent edits.
Step 1: Identify modified files
Use the merge_log recorded in Phase 5 (one entry per successful merge). For each entry, run git diff --name-only {merge_sha}^1 {merge_sha}. The union of these lists is the set of files modified by audit fixes, and each file maps directly to the zone whose merge introduced it. Do NOT reconstruct this with HEAD~N arithmetic — skipped merges make the count wrong.
Step 2: Run language-specific syntax checks
For each modified file, run the syntax checks in references/verification.md § "Post-merge validation commands (Phase 5.5)": Python ast.parse per file; npx tsc --noEmit once if tsconfig.json exists; go build ./... once if go.mod exists.
Step 2.5: Targeted functional tests (optional)
In deep and paranoid modes only (skip in quick/standard), run targeted tests covering the modified zones — NOT the full suite. Commands in the same reference section: pytest with a two-candidate lookup per modified file (sibling {dir}/tests/test_{basename}, then repo-root tests/test_{basename}); npx jest --findRelatedTests if package.json has a test script.
If tests fail, do NOT revert — log the failure and set "test_regression": true on the affected zone's results. The fix may be correct while the test needs updating. Print: Targeted tests: {N} test files run, {M} failures
Step 3: Handle failures
If ANY syntax check fails:
- Log:
Post-merge syntax error: {file}:{line} — {error} - Revert the offending merge using its recorded SHA from the
merge_log(the file → zone mapping from Step 1 identifies which merge introduced the broken file):git revert -m 1 {merge_sha} --no-edit. Revert only that zone's merge commit — never neighboring merges. - Mark the zone
fix-revertedin the results - Add it to
_skipped_zones.jsonso the next audit retries it - Continue to Phase 6 — the other zones' fixes are still valid
Print:
Post-merge validation: {N} files checked, {M} errors found
⚠ {file}:{line} — {error_type}: {message}
→ Reverted merge for zone {zone_id}. Fix needs manual review.
If all checks pass: Post-merge validation: {N} files checked — all clean ✓
Step 4: Write _skipped_zones.json
Persist failed zones (context overflow, API overload after 3 retries, merge conflict, syntax error
after merge) to {results_dir}/_skipped_zones.json with run_id and base_sha — format in
references/output-schema.md § "Skipped zones format (_skipped_zones.json)". Quota-paused zones
remain pending in the dispatch record rather than becoming skipped work.
Phase 5.6 — Cross-Zone Flow Validator
After zone agents and post-merge validation complete, dispatch 1-2 flow tracer agents to catch cross-zone integration bugs that isolated zone agents cannot see — they excel at per-file patterns but are blind to mismatches between a frontend caller and a backend callee in different zones.
When to run: always in deep and paranoid modes. Skip in quick. In standard, run only if the detected stack includes both frontend AND backend (e.g., TypeScript + Python).
Load references/verification.md § "Cross-Zone Flow Validator (Phase 5.6)" for the full procedure:
identify critical flows; record the current source_sha; compare the accepted current zone findings with comparable
previous_run.bugs to derive a provisional fixed_since_last_run list; inject both the comparable
history and that provisional list as verification context; dispatch a read-only tracer; then
validate deterministic caller/route evidence and collect only
{run_dir}/cross-zone-r{round_number}-{agent_id}.json for the current run. Phase 6 recomputes the
final lifecycle after accepted tracer findings are included.
If the flow tracer fails, log and continue — cross-zone validation is additive, not blocking.
Phase 5.7 — Finding Confidence Verification
After all zone agents and cross-zone validation complete, first validate every negative claim at any severity, then independently verify critical/high findings. Verification typically eliminates 15-30% of false positives.
Load references/verification.md § "Finding Confidence Verification (Phase 5.7)" for the full protocol:
- Step 1.4 — negative-evidence gate across all severities: missing-construct, missing-test,
unchecked-exit, dead-endpoint, and unreachable-feature claims without full-scope deterministic
searches move to
unverified_bugs. - Step 1.5 — constitutional pre-filter: free deterministic checks (file exists, line in range, ID format
^r\d+-(z\w+|xz)-\d{3}$, severity valid, scope, title/description quality) before spending tokens. - Step 2 — dispatch verification agents (fast verification model, batches of up to 10 per message, cap 50 total).
- Step 3 — score parsing and the keep / downgrade / reject table.
- Step 4 — recompute
summarycounts after filtering. - Step 5 — terminal report format.
Phase 6 — Aggregate + Report
Step 1: Collect all zone JSON files
Read only the accepted_artifact paths listed in the current run's persisted dispatch record, plus
the validated current-run cross-zone artifact. Revalidate run_id and base_sha while loading.
Never glob {results_dir}, never consume a previous run directory, and never rely on merges to
deliver artifacts.
Step 1.5: Normalize + Deduplicate
Apply the severity and category normalization maps from references/output-schema.md § "Normalization maps (Phase 6 Step 1.5)" to every bug.
Deduplication: group bugs by (file, title_normalized) (title lowercased, whitespace collapsed). For duplicates: keep the highest severity, set occurrence_count, discard the rest.
Log: Normalized {N} severity values, {M} category values, deduplicated {D} bugs.
Step 1.6: Finding Lifecycle (cross-run diff)
Compare this run's findings against the Phase 1 previous_run: tag each current bug
"lifecycle": "new" or "persistent" by the {file}::{title_normalized} match key, record
comparable previous bugs that disappeared in fixed_since_last_run, and count out-of-scope previous
bugs as not_rechecked. A previous bug is comparable only if its file was audited this run.
Print: Lifecycle vs {previous timestamp}: {N} new, {P} persistent, {F} fixed since last run, {K} not re-checked (out of scope)
Step 1.7: Apply Accepted Risks
If {repo_root}/.shipguard/accepted-risks.json exists, move current bugs matching non-expired entries into accepted_bugs (excluded from summary counts and the risk score); bugs with expired acceptances stay in bugs flagged "acceptance_expired": true and are called out in the terminal summary. Malformed file or entry → warn with the parse error and skip acceptance entirely — never silently drop findings. Schema and matching rules: references/output-schema.md § "Accepted risks (procedure and file format — Phase 6 Step 1.7)".
Print: Accepted risks: {A} findings moved to accepted{IF E > 0}, {E} acceptance(s) EXPIRED and resurfaced{END IF}
Step 2: Build audit-results.json
Merge all zone results into a single aggregated file following the canonical schema in references/output-schema.md § "Aggregated output: audit-results.json (canonical)". Key contract points:
prompt_hash—sg-improvereads this to detect prompt changes and flag baseline discontinuity.run_idandbase_sha— bind the canonical aggregate to the accepted intermediate evidence.summary.by_fix_tier— recompute exactmechanical,test-first, andhuman-onlycounts from the final activebugsarray after verification, lifecycle, and accepted-risk filtering.impacted_ui_routes: [{route, reason, severity, bug_count}]andimpacted_backend: [{endpoint, reason, severity}]— the bridge consumed by/sg-visual-run --from-auditand/sg-process-check --from-audit.bugs,unverified_bugs,accepted_bugs,fixed_since_last_runarrays; per-bugfix_tier,fix_tier_reason,negative_evidence,verification_score,verified,lifecycle, and (for UI bugs)impacted_routes; cross-zone bugs also carry deterministicflow_evidence.scope_info: all fields in diff mode,{"mode": "full"}otherwise.
Step 3: Derive impacted routes
Split bug impacts into impacted_ui_routes (URL paths that /sg-visual-run --from-audit can test) and impacted_backend (API endpoints, services, infra with no visual test) — this prevents "uncovered route" noise for things that can't have visual tests.
Classify each bug's file: frontend (under src/app/, src/pages/, src/components/, public/) → impacted_ui_routes; backend (Python routes, services, Dockerfiles, config) → impacted_backend.
Map frontend files to routes using the framework-specific strategies in references/output-schema.md § "Deriving impacted routes (Phase 6 Step 3 — framework strategies)" (Next.js App/Pages Router, React Router, static HTML, generic fallback). Never hardcode project-specific paths.
Deduplicate routes: multiple bugs mapping to one route become one entry with the highest severity, a combined reason, and an exact bug_count; add the route to each mapped bug's impacted_routes array. Do not compute route bug counts with substring matching — / must not count every bug; counts come only from explicit route mapping. If no routes can be derived, set impacted_ui_routes to [].
Step 3.5: Compute risk score
Compute the 0-100 diminishing-returns risk_score using references/output-schema.md § "Risk score model (Phase 6 Step 3.5)" (severity base points with geometric decay, capped at 100). Compute it on the bugs array AFTER Step 1.7 — accepted_bugs and unverified_bugs do not contribute. Store as summary.risk_score.
Step 4: Write results
The previous canonical run was archived before dispatch in Phase 1. Write the new
audit-results.json atomically to {results_dir} only after aggregation completes, then mark the
run-scoped dispatch record completed. A paused or incomplete run never overwrites the canonical
aggregate.
Step 4.5: Write TOON compact format
Also write audit-results.toon alongside the JSON, following references/output-schema.md § "TOON compact format (Phase 6 Step 4.5)". TOON is informational (~40% fewer tokens for LLM consumption); the JSON file remains canonical.
Step 5: Print summary
Print the terminal summary using the exact template in references/output-schema.md § "Terminal summary template (Phase 6 Step 5)" — bugs by severity with verification counts, lifecycle line, accepted-risks line (flagging expired acceptances), top categories, files audited/modified, merge conflicts if any, both result paths, and next steps (/sg-process-check --from-audit, /sg-visual-run --from-audit, /sg-visual-review, or /sg-ship to run all lanes + review in one command).
If monitor_active, POST audit-complete — payload in references/monitor.md § "audit-complete (Phase 6)".
Multi-Round Execution
If round_count > 1 (deep or paranoid mode), the audit runs in sequential rounds:
Round loop
for round_number in 1..round_count:
1. Build prompts with round-specific focus (R1, R2, or R3 from
…(truncated)