Imported from josephyooo/audit-loop (
address/SKILL.md). Install upstream withnpx skills add josephyooo/audit-loop --skill address. Copyright stays with the author.
Address Audit Findings
Fix code issues identified by an external audit. Operates in two modes:
- Manual mode: user pastes findings or provides a file path. Parse, triage, fix, report.
- Loop mode: activated by
ADDRESS:trigger phrases from the audit agent via tmux. Reads GitHub issues, fixes in batched PRs, coordinates review with the audit agent.
Mode Detection
- If input starts with
ADDRESS:→ loop mode (see Loop Protocol below) - Otherwise → manual mode (see Manual Protocol below)
Loop Protocol
Triggered by the audit agent via tmux send-keys. All GitHub operations use gh CLI.
Sending Triggers — CRITICAL RULES
Every tmux send-keys that sends a trigger phrase to the other agent MUST follow all three rules below. Violating any one of them breaks the loop silently.
- The quoted string MUST end with a trailing space. Example:
"AUDIT: review PR #4 "— note the space before the closing quote. This is required because Copilot CLI (and some other agents) autocomplete on#<number>; without the trailing space, pressing Enter selects an autocomplete suggestion instead of submitting your literal text. Do NOT strip or "clean up" this trailing space — it is load-bearing. - Enter MUST be sent in a separate bash call. Not chained with
&∨. Two distinct bash tool invocations. - Do not substitute
C-mforEnter— they behave differently in some agent terminals.
Correct pattern (two separate bash calls):
tmux send-keys -t audit "AUDIT: review PR #4 "
tmux send-keys -t audit Enter
Every send-keys snippet below follows this pattern. Preserve it exactly.
- First trigger to the audit agent MUST include the skill prefix. Read
audit_prefixfrom state.json. The very firsttmux send-keys -t auditin the cycle must prepend{audit_prefix}auditto the trigger phrase (e.g.$/audit AUDIT: review PR #4for Codex, or/audit AUDIT: review PR #4for Claude). After the first trigger is sent, subsequent triggers use bareAUDIT:phrases — the skill is already loaded. To track this, after sending the first prefixed trigger, set"audit_skill_loaded": truein state.json. Before each trigger, check this flag — iffalseor missing, use the prefix.
Receive Protocol — CRITICAL
On any input, check whether it matches a known trigger phrase (ADDRESS: begin, ADDRESS: continue, ADDRESS: revise PR #N, ADDRESS: poll-async). If it does, you MUST execute the corresponding handler below exactly as written — run the gh commands, create branches, commit code, open PRs, update state.json, and send tmux triggers to the audit agent. Do NOT just discuss your plan in chat. Every handler MUST end with a tmux trigger to the audit agent (except poll-async, which only triggers audit when a job actually completes or fails).
On session start or after any interruption, check .audit-loop/state.json. If a cycle is in progress and phase is address-fixing, resume:
- If
current_pris set, check whether that PR has been reviewed. If not, re-sendAUDIT: review PR #Nto the audit agent. If it has reviews, process the latest review as if receiving the appropriate trigger. - If
current_pris null, treat it asADDRESS: begin(start/continue grouping issues).
Important: needs-human on one PR does NOT halt the cycle. The agent proceeds to the next batch. Only the capped PR is deferred.
Step 0: LIB_ROOT bootstrap (every trigger, before anything else)
The timeout check and every handler invokes bash "$LIB_ROOT/<name>.sh". $LIB_ROOT must be set before the timeout check runs, so this block runs FIRST on every trigger — even before reading state.json — because session restarts lose the env var.
# Prefer the cached value from state.json when available (survives across triggers).
LIB_ROOT=""
if [ -f .audit-loop/state.json ]; then
LIB_ROOT="$(jq -r '.lib_root // empty' .audit-loop/state.json)"
[ -d "$LIB_ROOT" ] || LIB_ROOT=""
fi
# Fall back to symlink discovery if state.json doesn't have it yet.
if [ -z "$LIB_ROOT" ]; then
for skill_name in address audit; do
skill_link="$HOME/.claude/skills/$skill_name"
[ -e "$skill_link" ] || continue
resolved="$(readlink -f "$skill_link" 2>/dev/null || true)"
[ -n "$resolved" ] && [ -d "$resolved" ] || continue
candidate="$(dirname "$resolved")/lib"
if [ -d "$candidate" ]; then
LIB_ROOT="$candidate"
break
fi
done
fi
[ -n "$LIB_ROOT" ] && [ -d "$LIB_ROOT" ] || {
echo "FATAL: lib/ not found via .audit-loop/state.json or ~/.claude/skills/{address,audit}. Tell me where audit-loop is checked out." >&2
exit 1
}
export LIB_ROOT
Once state.json exists, cache the resolved path so future triggers don't re-walk the symlinks:
bash "$LIB_ROOT/state_update.sh" address lib_root "\"$LIB_ROOT\""
Re-export LIB_ROOT in every subsequent bash call — CRITICAL. The Claude Code Bash tool runs each invocation in a fresh shell; env vars do NOT persist across calls. Once lib_root is in state.json, every bash call that invokes a helper must re-read it inline. Use this prologue at the top of every such call:
export LIB_ROOT="$(jq -r .lib_root .audit-loop/state.json)"
[ -d "$LIB_ROOT" ] || { echo "FATAL: lib_root in state.json is invalid: '$LIB_ROOT'" >&2; exit 1; }
Then invoke the helper as bash "$LIB_ROOT/<name>.sh" .... Skipping this prologue (e.g. relying on an export LIB_ROOT=... from an earlier bash call) will silently resolve "$LIB_ROOT/state_update.sh" to /state_update.sh and produce confusing "No such file or directory" errors.
State File
All loop state lives in .audit-loop/state.json (in the repo root, created by the /audit skill or by state_init.sh address on first run).
All state writes go through bash "$LIB_ROOT/state_update.sh" address <field> <json_value> [...]. The helper refreshes last_heartbeat_time and heartbeat_actor on every call and writes atomically (mktemp + same-FS mv). Inline jq … > tmp && mv tmp state.json is forbidden — it bypasses the heartbeat invariant the watchdog depends on.
Additional State Fields
Beyond the base fields (cycle_id, phase, current_pr, current_batch, revision_round, batches_created, last_trigger, last_trigger_time), state.json also tracks:
last_heartbeat_time: most recent in-flight heartbeat in UTC ISO8601 format. Initialized bystate_init.sh.heartbeat_actor:auditoraddress, whichever agent most recently refreshedlast_heartbeat_time.awaiting_clarification: boolean —truewhen the address agent posted a clarifying question and is blocked waiting for a response. Defaultfalse.revision_history: array of{round, pr, commit, summary}entries — The address agent writes one after each revision push. ThecommitSHA enables the audit agent's convergence detection.address_prefix/audit_prefix: skill prefix for each agent's harness (/for Claude/Copilot/Gemini,$for Codex). Set by whichever agent initializes state.json.address_skill_loaded/audit_skill_loaded: booleans tracking whether the skill has been activated via a prefixed trigger.lib_root: absolute path tolib/(cached after first symlink resolution; see Step 0).
Helper library
Multi-line shell logic lives in lib/*.sh files alongside this SKILL. The scripts used in this SKILL:
state_init.sh address— first-time.audit-loop/state.jsoncreationstate_update.sh address <field> <value> [...]— every subsequent state writeneeds_human_apply.sh pr <num> <reason>— label-first (load-bearing watchdog signal), comment best-effortdefer_kind_execute.sh <issue> <reason>— idempotent: skips if needs-human already present; otherwise label-edit first (atomic add-needs-human/remove-audit-loop in one gh call), comment best-effortclose_referenced_issues.sh <pr>— close all closing-keyword-referenced issues post-mergeslurm_qos_precheck.sh <command_file>— refuse (exit 4) if the user is already at the per-user slot cap for the command's--qos. Exit 0 = OK to submit, exit 4 = retry next cycle, exit 2 = bad args. Call this BEFORE opening the placeholder PR so a cap-blocked submission doesn't leave a stray async-pending PR behind.slurm_submit.sh <issue> <pr> <branch> <command_file>— dispatch akind:executeissue whose## Commandblock starts withsbatchorsallocinto the async pipeline (returns immediately with the slurm job ID)abandon_async_pr.sh <pr> <issue> <reason>— clean up a placeholder PR whose slurm dispatch failed: defers the issue, labels the PR needs-human, strips async-pending. One named operation so the three-step cleanup can't drift apart on refactor.slurm_check.sh— poll every entry inpending_async_jobs; TSV<issue>\t<pr>\t<job_id>\t<state>\t<log>per lineslurm_complete.sh <job_id>— verify expected artifacts, commit + push, dropasync-pendinglabel, triggerAUDIT: review PR #Nslurm_fail.sh <job_id> <state>— comment failure tail to PR, labeltests-failing, dropasync-pending, trigger reviewsetup_async_cron.sh [<minutes>]— install user crontab entry that pokes the address agent withADDRESS: poll-asyncperiodically (one-shot setup;--uninstall/--statusto manage)
Async polling mechanism — CRITICAL when pending_async_jobs is non-empty
pending_async_jobs entries only finalize when something fires ADDRESS: poll-async. If nothing does, async PRs sit forever with async-pending and the cycle never completes. Pick one mechanism per environment, in this preference order:
Tier 1 — Harness-native scheduler (preferred). If your Claude Code session has access to CronCreate (recurring wake-ups) or Monitor (event-driven on a long-running command), use it. The scheduler fires inside the agent's REPL, so it doesn't depend on OS-level crontab — works on NERSC login nodes, restricted HPC, anywhere the agent runs.
- Monitor on
sacct(best for slurm): drivesADDRESS: poll-asyncfrom actual state changes, not a clock. Set up after the first async submission of the cycle:
Each new line wakes the agent. Cancel via the tool's stop mechanism when the cycle ends.Monitor: bash -c 'while true; do sacct -u $USER -X --noheader -o JobID,State --starttime now-1day | sort -u; sleep 60; done' prompt: "ADDRESS: poll-async" - CronCreate: simpler but clock-driven. Set
*/10 * * * *for 10-min polling.
Cancel viaprompt: "ADDRESS: poll-async"CronDelete <id>when the cycle ends.
Tier 2 — setup_async_cron.sh (when OS crontab is available). For environments with user crontab (typical workstations, dev containers, non-restricted Linux). One-shot install:
bash "$LIB_ROOT/setup_async_cron.sh" 10 # poll every 10 minutes
Uninstall at cycle end via bash "$LIB_ROOT/setup_async_cron.sh" --uninstall.
Tier 3 — Manual. If neither of the above is available, document in the cycle's final report that the user must run ADDRESS: poll-async themselves. Acceptable for short cycles or single-job submissions; brittle for multi-batch overnight runs.
Decision rule for the agent: immediately after the first successful slurm_submit.sh invocation of the cycle, set up tier 1 if available, else attempt tier 2 (and notice the exit code — setup_async_cron.sh will fail on NERSC login nodes), else fall back to tier 3 and tell the user explicitly. Never silently leave pending_async_jobs un-polled.
Timeout Check
On receiving any trigger (after Step 0), read last_heartbeat_time from state.json. If it is missing, fall back to last_trigger_time. Also read awaiting_clarification.
If awaiting_clarification is true:
- If
last_trigger_time< 2 hours ago: skip normal timeout. Print: "Cycle is blocked on clarification — waiting for response on PR #N." - If
last_trigger_time>= 2 hours ago: clarification was never answered. Readcurrent_prand guard against null (a prior partial failure may have cleared it while leavingawaiting_clarification=true— without the guard, the literal string "null" gets passed toneeds_human_apply.shand exits 2, leaving the flag stuck and re-firing the same branch every trigger; round-9 finding #9):
Move on to the next batch.cp=$(jq -r '.current_pr // empty' .audit-loop/state.json) if [ -n "$cp" ]; then bash "$LIB_ROOT/needs_human_apply.sh" pr "$cp" "clarification not answered within 2 hours" else echo "WARN: awaiting_clarification was stuck true with current_pr=null; clearing flag without labeling (no PR to label)" >&2 fi bash "$LIB_ROOT/state_update.sh" address awaiting_clarification 'false' current_pr 'null'
If awaiting_clarification is false and more than 30 minutes have elapsed:
for pr in $(gh pr list --label audit-loop --state open --json number -q '.[].number'); do
bash "$LIB_ROOT/needs_human_apply.sh" pr "$pr" "paired agent stalled (no heartbeat for 30+ min)"
done
needs_human_apply.sh applies the needs-human label FIRST (load-bearing — the watchdog and cap-detection logic key off the label), then posts the explanatory comment best-effort. A comment-only failure produces a stderr warning but exits 0; a label failure exits 1 so the caller can retry without silently bypassing the cap.
Print: "WARNING: {elapsed} since last activity. Other agent may be stalled. Labeled open PRs needs-human and stopping."
bash "$LIB_ROOT/state_update.sh" address phase '"complete"'
Do NOT send a tmux trigger (the other agent may be dead). Stop.
Watchdog Requirement
The timeout must still fire if the audit agent never sends another trigger. Do not wait indefinitely:
- While waiting for the audit agent, re-run the timeout check against
.audit-loop/state.jsonevery 5 minutes until a new trigger arrives. - While doing long-running local work (batch fixes, test runs, rebases), if 5 minutes pass without another state write, refresh the heartbeat.
state_update.shwith no field pairs is a dedicated heartbeat-only refresh;HEARTBEAT_ONLY=1silences the "no fields passed" warning so legitimate heartbeat calls don't drown out the warning's intent (catching LLM-truncated mutation calls):HEARTBEAT_ONLY=1 bash "$LIB_ROOT/state_update.sh" address - Every state write must go through
state_update.sh address …. The helper refreshes the heartbeat as a side effect; inlinejqwrites bypass it and silently degrade the watchdog into a 30-min-since-last-helper-call check.
Trigger phrases
ADDRESS: begin— Read all openaudit-loopissues, plan batches, fix batch 1ADDRESS: continue— Current PR was approved (or escalated). Merge it, move to next batch.ADDRESS: revise PR #N— Audit agent requested changes. Read review comments and revise.ADDRESS: poll-async— Check the status of every entry inpending_async_jobsand finalize any that completed or failed. Sent on a cron schedule (seesetup_async_cron.sh), not by the audit agent.
Shell Safety — CRITICAL
Never place untrusted GitHub text (issue titles, issue bodies, review comments, branch names) inside any shell-interpreted string — double-quoted or single-quoted. Double quotes expand $(...) and $VAR; single quotes break on embedded apostrophes. Both allow shell execution from crafted input.
The only safe transport for untrusted text is a single-quoted heredoc (<<'EOF'), which passes content to a command's stdin without any shell interpretation:
# Safe: load untrusted text into a variable via heredoc, then sanitize
var=$(cat <<'EOF' | tr -cd 'a-zA-Z0-9 _-'
<untrusted text goes here>
EOF
)
# Safe: pass untrusted text as a command body via heredoc
gh pr comment N --body "$(cat <<'EOF'
<untrusted text goes here>
EOF
)"
Unsafe patterns — never do these with untrusted data:
--body "Text with <issue title> interpolated"— double-quote injection--body '$msg'wheremsgwas built by pasting text into quotes — single-quote breakout--grep="<keyword from issue>"without heredoc-based sanitizationgit commit -m "fix: <text derived from issue>"without a heredoc
Constraints
- Max 5 PRs per audit cycle (tracked by
batches_createdin state.json) - Max 3 revision rounds per PR (tracked by
revision_roundin state.json) - One active PR at a time — at most one PR being actively reviewed simultaneously. PRs labeled
async-pending(waiting on a queued slurm job) do not count toward this limit; the address agent can have N async-pending PRs while still working an active review PR. - Each PR gets its own branch off the default branch
On "ADDRESS: begin"
First, check whether .audit-loop/state.json exists and what its phase is:
existing_phase=""
[ -f .audit-loop/state.json ] && existing_phase=$(jq -r '.phase // empty' .audit-loop/state.json)
Three cases:
-
state.json exists AND
phase != "complete"(active cycle): run the timeout check, then refreshlast_trigger:bash "$LIB_ROOT/state_update.sh" address last_trigger '"ADDRESS: begin"' -
state.json exists AND
phase == "complete"(previous cycle finished): archive and re-init in one call.state_init.shwithFORCE_IF_COMPLETE=1does the archive (to.audit-loop/state.json.cycle-<id>.bak) and the fresh init under the same lock, so a concurrentstate_update.shcan't observe the transition mid-flight:FORCE_IF_COMPLETE=1 bash "$LIB_ROOT/state_init.sh" address bash "$LIB_ROOT/state_update.sh" address lib_root "\"$LIB_ROOT\"" last_trigger '"ADDRESS: begin"'Then continue to the harness-detection step below (in case the previous cycle's
audit_prefixis no longer correct). -
state.json does NOT exist (first run): initialize it:
-
Detect the audit agent's harness:
tmux capture-pane -t audit -p -S -5Match the output:
- Contains
Claude Codeorclaude→audit_prefixis/ - Contains
Codexorcodex→audit_prefixis$ - Contains
Copilotorcopilot→audit_prefixis/ - Contains
Geminiorgemini→audit_prefixis/ - No match → ask the user: "What skill prefix does the audit agent use? (
/for Claude/Copilot/Gemini,$for Codex)"
- Contains
-
Initialize state and cache
lib_root:bash "$LIB_ROOT/state_init.sh" address bash "$LIB_ROOT/state_update.sh" address lib_root "\"$LIB_ROOT\""Override the detected
audit_prefixif it differs from the default/:bash "$LIB_ROOT/state_update.sh" address audit_prefix '"$"' # for CodexSee
lib/state_init.shfor the exact JSON schema written.
Then proceed:
Before any gh command in ADDRESS: begin, validate GitHub auth:
if ! gh auth status >/dev/null 2>&1; then
echo "ERROR: GitHub authentication failed. Run 'gh auth login' and retry."
exit 1
fi
-
Identify the default branch:
gh repo view --json defaultBranchRef -q '.defaultBranchRef.name' -
Deadlock recovery: Before grouping new issues, check for open
audit-loopPRs without reviews:gh pr list --label audit-loop --state open --json number,reviews \ --jq '.[] | select(.reviews | length == 0) | .number'If found, treat that PR as the current batch:
bash "$LIB_ROOT/state_update.sh" address current_pr '<number>'Then send the review trigger:
tmux send-keys -t audit "AUDIT: review PR #<number> "Then in a separate bash call (preserve the trailing space above; do NOT chain with && or ;):
tmux send-keys -t audit EnterThen wait. Do NOT create new batches.
Also check state.json: if
current_pris set and that PR is still open with no reviews, do the same. -
Check for orphaned in-progress issues from an interrupted batch:
gh issue list --label in-progress --label audit-loop --state open --json number,titleIf found, check whether their audit branch exists:
git ls-remote --heads origin | grep 'refs/heads/audit/'- If a branch exists with a PR: resume by sending the review trigger for the existing PR.
- If a branch exists with commits but no PR: delete the orphaned branch (
git push origin --delete audit/<slug>), removein-progresslabels, and include those issues in new batching. - If no branch exists: just remove
in-progresslabels and regroup normally.
-
Fetch all open audit issues:
gh issue list --label audit-loop --state open --json number,title,labels,body --limit 100Then sweep previously-blocked issues for unblock-eligibility. Any issue labeled
blocked-on-othermay now have all its declared deps closed. For each:gh issue list --label blocked-on-other --state open --json number,body --limit 100Parse
Depends on #Nfrom each body. If every declared dep is now CLOSED, restoreaudit-loopand removeblocked-on-other:gh issue edit <number> --add-label "audit-loop" --remove-label "blocked-on-other"Then re-fetch step 4's list so the newly-unblocked issues are included in this cycle's planning.
-
If no issues found, signal completion immediately (see "Signal completion" below).
-
Validate each issue before planning work. For each issue, check:
-
Is the scope clear enough to implement? (not vague like "improve performance")
-
Are the acceptance criteria concrete and verifiable?
-
Does it conflict with or duplicate another open issue?
-
Are the referenced files/APIs real? (quick check —
lsorgrep) -
Does it declare a dependency on another open audit-loop issue? Scan the body for
Depends on #N(the format/issuesemits) orDepends on:followed by issue references. For each cited dep, check whether it's still open:gh issue view N --json state --jq '.state'If ANY declared dep is OPEN, this issue is blocked. Comment, mark
blocked-on-other, removeaudit-loop, and skip (do not add to a batch):gh issue comment <number> --body "$(cat <<'EOF' Blocked by an open dependency in this cycle. Will be re-eligible when the dependency closes; the address agent will restore the `audit-loop` label on the next cycle if so. EOF )" gh issue edit <number> --remove-label "audit-loop" --add-label "blocked-on-other"This convention prevents the silent-drop pattern (cycle 33 dropped #723/#724 because their dep #714 was queued; they retained
audit-loopand re-entered the batch on the nextADDRESS: beginwith no record of why they couldn't progress). -
Does it carry
kind:execute? If yes, look at the first word of the issue's## Commandblock and pick ONE of three paths:a. Async slurm path. First word is
sbatchorsallocAND you are running on a system with Slurm (login node, cluster). The job goes through the async pipeline: a placeholder PR is opened with theasync-pendinglabel,slurm_submit.shqueues the job and returns immediately, and a cron-drivenADDRESS: poll-asynctrigger finalizes the PR when the job completes. See "Fix a batch (subroutine)" step 4a-execute below for the actual submission code. You can have multiple async-pending PRs concurrently; they don't count against the "one active PR" constraint.b. In-session inline path. First word is anything else (a non-slurm shell command, a
maketarget, apython script.py, etc.) AND you have access to the required infrastructure / credentials / runtime in this session. Run the command inline and commit artifacts as a normalkind:executePR (see step 4a-execute below).c. Defer to human path. None of the above — multi-hour interactive runs that require a human at the terminal, jobs requiring datasets/credentials you don't have, jobs that need a non-Slurm scheduler you can't reach. Use
defer_kind_execute.sh(idempotent on retry; label-first +needs-human / -audit-loop in one gh edit; reason is a positional arg, not a placeholder):bash "$LIB_ROOT/defer_kind_execute.sh" <number> "missing dataset access on this login node"(Replace the quoted reason with the actual reason. Keep it one line, no shell metacharacters.) Move on. NEVER substitute a code/tooling change for an unfulfilled
kind:executeissue, and NEVER writeCloses #<number>for akind:executeissue in a PR that does not actually produce the listed artifacts.
If an issue fails validation, comment explaining what's unclear and remove the
audit-looplabel so it drops out of the batch:gh issue comment <number> --body "$(cat <<'EOF' Issue needs clarification before work can begin: <what's unclear> EOF )" gh issue edit <number> --remove-label "audit-loop"Continue with the remaining valid issues.
-
-
If no valid issues remain after validation, signal completion immediately (see "Signal completion" below).
-
Group valid issues into batches by relatedness:
- Same file or subsystem/directory → same batch
- Same category (e.g., all
cat:securityinput validation) → same batch - Unrelated issues → separate batches
- Order batches by severity (critical/high first)
- Target 2-5 issues per batch. Single-issue batches are fine for complex fixes.
-
Print the batch plan:
## Batch Plan Batch 1: #12, #13, #15 (auth input validation) Batch 2: #14, #17 (cache race conditions) Batch 3: #16 (deprecated dependency) -
Fix batch 1 (see "Fix a batch" below).
On "ADDRESS: continue"
First run the timeout check.
bash "$LIB_ROOT/state_update.sh" address last_trigger '"ADDRESS: continue"' revision_round '0'
-
Merge the current PR if one is open and approved. Refuse to merge PRs labeled
tests-failing— if the label is present, print a warning and skip the merge (labelneeds-humaninstead).Approval signal — CRITICAL. Treat the PR as approved if EITHER condition holds:
- The PR has a review with state
APPROVED(the normal path):gh pr view <number> --json reviews --jq '[.reviews[] | select(.state == "APPROVED")] | length > 0' - OR any review/comment body contains the literal line
## audit-loop verdict: APPROVED(the fallback path used when audit hits self-approval rejection on shared-identity accounts):gh pr view <number> --json reviews,comments \ --jq '[(.reviews[].body // empty), (.comments[].body // empty)] | map(select(. | contains("## audit-loop verdict: APPROVED"))) | length > 0'
Combine both checks; if either is true, proceed to merge. If neither, the PR has not been approved — do NOT merge. Re-send
AUDIT: review PR #<number>and wait. Do not invent your own approval criteria.# Check for tests-failing label gh pr view <number> --json labels --jq '.labels[].name' | grep -q 'tests-failing'If no blocking label:
gh pr merge <number> --squash --delete-branchIf merge fails due to conflicts, rebase:
git fetch origin && git rebase origin/<default-branch> git push --force-with-leaseThen retry merge. If still failing, defer to human, do NOT close any issues, and move on to step 3:
bash "$LIB_ROOT/needs_human_apply.sh" pr <number> "merge conflict not resolvable by address agent" - The PR has a review with state
-
If the PR is merged, explicitly close the issues referenced by closing-keyword in the PR body (squash merges may not propagate closing keywords). The helper handles the merge-state check, body extraction, and closure loop:
bash "$LIB_ROOT/close_referenced_issues.sh" <number>close_referenced_issues.shis a no-op if the PR is not MERGED, and useslib/extract_closes.shinternally — which matches the narrow historical regex (closes?\s+#N). Broadening toFixes,Resolves, and multi-issue lists is a separate behavior change documented as a follow-up. -
Check the batch cap — read
batches_createdfrom state.json. If >= 5, signal completion. -
Check for remaining open issues:
gh issue list --label audit-loop --state open --json number,title,labels,body --limit 100If none remain, signal completion. Otherwise, fix the next batch.
On "ADDRESS: revise PR #N"
First run the timeout check.
bash "$LIB_ROOT/state_update.sh" address last_trigger '"ADDRESS: revise PR #N"'
-
Read
revision_roundfrom state.json. If >= 3, do NOT revise. Instead:bash "$LIB_ROOT/needs_human_apply.sh" pr N "3 revision rounds reached without convergence" bash "$LIB_ROOT/state_update.sh" address current_pr 'null' revision_round '0'Print: "3 revision rounds reached. Labeled
needs-human, waiting for next batch." Then wait forADDRESS: continue. -
Read review comments:
review_body=$(gh pr view N --json reviews --jq '(.reviews[-1].body // empty)') repo=$(gh repo view --json nameWithOwner -q '.nameWithOwner') gh api "repos/$repo/pulls/N/comments" --jq '.[] | select(.pull_request_review_id) | {path, body, line}'If
review_bodyis empty, the audit review has not been posted yet (or was deleted). Re-sendAUDIT: review PR #N, do not check out the branch, and wait for the next trigger instead of parsing empty feedback. -
Check out the PR branch:
gh pr checkout N -
Parse review comments into explicit tasks before editing.
For each review comment, write:
File: <path:line> Requested change: <one sentence, in your own words> My plan: <what you'll do>If you cannot confidently interpret a comment (ambiguous feedback), do NOT guess. Post a clarifying comment on the PR:
gh pr comment N --body "$(cat <<'CLAREOF' Clarification needed: [quote the ambiguous feedback]. Do you mean [interpretation A] or [interpretation B]? CLAREOF )"Set the clarification flag (keeps
phaseasreviewing). The state write also setslast_triggerso the 2-hour clarification timer measures from the clarification post — not from whenever the original revise trigger was received (the agent may have spent significant time parsing/running tests before asking, which would otherwise eat into the 2h window). Do not make any code changes. Still send the review trigger so the audit agent can answer:bash "$LIB_ROOT/state_update.sh" address awaiting_clarification 'true' last_trigger '"ADDRESS: clarification-posted PR #N"' tmux send-keys -t audit "AUDIT: review PR #N "Then in a separate bash call (preserve the trailing space above; do NOT chain with && or ;):
tmux send-keys -t audit EnterThen stop and wait.
Resuming from clarification: On any input while
awaiting_clarificationis true, check the PR for new comments after yours:gh pr view N --json comments --jq '.comments[-1].body'If a response exists:
bash "$LIB_ROOT/state_update.sh" address awaiting_clarification 'false'Then proceed with the revision using the clarified feedback.
Scope constraint: Only modify files and lines cited in the review comments. If you notice an unrelated issue during revision, file a new GitHub issue — do not fix it in this PR.
-
Address each parsed review task with targeted fixes.
-
Discover and run the project's linter/formatter (same discovery as "Fix a batch" step 5). Fix auto-fixable errors before committing.
-
If tests/lint fail:
gh pr edit N --add-label "tests-failing"Note the failure in the PR body. Attempt to fix the test failure once. If unable to fix, proceed — the audit agent will see the label and request changes.
If tests pass and the PR previously had
tests-failing:gh pr edit N --remove-label "tests-failing" -
Before updating the PR body or writing revision artifacts, derive the next revision number:
next_revision_round=$((revision_round + 1)) -
Update the revision marker in the PR body. Read current body, find or insert:
<!-- audit-revision: N -->Update N to
next_revision_round. -
Stage and commit. Apply the same secret file exclusion as "Fix a batch" step 7 — never stage
.env, credentials, keys, etc. Use explicit file paths.
git add <specific files> && git commit -m "address review feedback (round <next_revision_round>) on PR #N"
git push
-
Post a revision summary comment on the PR so the audit agent (and humans) can see the reasoning:
gh pr comment N --body "$(cat <<'REVEOF' ## Revision Round <next_revision_round> ### Analysis <copy the parsed review tasks from step 4 here — File, Requested change, My plan for each> ### Changes <brief description of what was changed and why> ### Test plan <how to verify the revision fixes the review feedback> REVEOF )" -
Persist
next_revision_roundand append arevision_historyentry. The address agent owns this counter — only update it when pushing an actual code change, not on clarification. Build the history entry as JSON inline sostate_update.shcan append it atomically with the round/phase change:commit_sha=$(git rev-parse HEAD) summary='<one-line description>' # plain text; quote-safe # Idempotency guard (round-9 finding #3): if the most recent history # entry already records this commit, the handler is being re-entered # after a partial failure (state write succeeded but tmux send-keys # didn't). Skip the append so revision_history doesn't double-count # the same commit, which would mislead the audit-side convergence # check. last_commit=$(jq -r '.revision_history[-1].commit // empty' .audit-loop/state.json) if [ "$last_commit" = "$commit_sha" ]; then echo "INFO: revision_history already records commit $commit_sha; skipping duplicate append (restart-recovery)" >&2 else history_entry=$(jq -nc \ --argjson round "$next_revision_round" \ --argjson pr N \ --arg commit "$commit_sha" \ --arg summary "$summary" \ '{round: $round, pr: $pr, commit: $commit, summary: $summary}') new_history=$(jq --argjson e "$history_entry" '.revision_history + [$e]' .audit-loop/state.json) bash "$LIB_ROOT/state_update.sh" address \ revision_round "$next_revision_round" \ phase '"reviewing"' \ revision_history "$new_history" fiThen trigger the audit agent:
tmux send-keys -t audit "AUDIT: review PR #N "Then in a separate bash call (preserve the trailing space above; do NOT chain with && or ;):
tmux send-keys -t audit Enter
On "ADDRESS: poll-async"
Sent on a cron schedule (see setup_async_cron.sh) — NOT by the audit agent. Checks every queued slurm job and finalizes any that have completed or failed.
First run the timeout check, then refresh the heartbeat so the watchdog knows the cycle is alive even while all current work is waiting on the queue:
HEARTBEAT_ONLY=1 bash "$LIB_ROOT/state_update.sh" address
Read pending jobs and dispatch by Slurm state:
bash "$LIB_ROOT/slurm_check.sh" | while IFS=$'\t' read -r issue pr job_id state log; do
case "$state" in
PENDING|RUNNING|UNKNOWN)
# Still queued or running — nothing to do. UNKNOWN means squeue/sacct
# couldn't find the job yet; could be a brand-new submission that
# hasn't propagated, or a very old one that aged out of sacct. Wait
# for the next poll.
;;
COMPLETED)
bash "$LIB_ROOT/slurm_complete.sh" "$job_id" || \
bash "$LIB_ROOT/slurm_fail.sh" "$job_id" ARTIFACTS_MISSING
;;
FAILED|CANCELLED|TIMEOUT)
bash "$LIB_ROOT/slurm_fail.sh" "$job_id" "$state"
;;
esac
done
(slurm_complete.sh exits 1 if expected artifacts are missing even though the job state was COMPLETED — that fallback routes to slurm_fail.sh with state ARTIFACTS_MISSING so the PR gets the tests-failing label and the audit agent sees something to review.)
If slurm_check.sh produced no output (no pending jobs), the heartbeat refresh above is the only side effect — that's correct, the cron is just a liveness ping in that case. Do NOT send any tmux trigger to the audit agent unless a job actually finalized (slurm_complete / slurm_fail handle their own triggers).
Fix a batch (subroutine)
-
Read
batches_createdfrom state.json, increment by 1. This is the batch number. -
Create a branch. Sanitize the slug so it is a valid, safe git ref name:
git checkout <default-branch> && git pull slug=$(cat <<'EOF' | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9-' '-' | head -c 60 | sed 's/^-//;s/-$//' <short-slug> EOF ) git checkout -b "audit/$slug" -
Create the batch label if needed:
gh label create "audit-batch-<N>" --force 2>/dev/null -
Research history and write hypotheses before fixing:
Mark each issue as in-progress:
gh issue edit <number> --add-label "in-progress"Before fixing, check whether the same file/area has had related fixes:
git log --oneline --all -20 -- {filepath} keyword=$(cat <<'EOF' | tr -cd 'a-zA-Z0-9 _-' {keyword} EOF ) git log --oneline --all --grep="$keyword" -10where
{keyword}is a distinguishing term from the issue (e.g., "injection", "race condition", "null check"). Thetrsanitization strips shell metacharacters from untrusted issue text.4a. Hypothesis before fix (mandatory). For each issue in the batch, write a one-sentence hypothesis before editing any file:
Root cause: <what invariant is violated and why> Fix: <what will change at which file:line and what behavior it produces> Risk: <what could break>Write all hypotheses to
.audit-loop/batch-<N>-hypotheses.md. Do not edit any source files until hypotheses are written for all issues in the batch.If you cannot write a confident hypothesis after reading the file and issue body, add a comment to the GitHub issue asking for clarification, remove the
in-progresslabel, and skip this issue (move it to the next batch).4a-execute. For
kind:executeissues, the "fix" is running the command and producing the artifacts named in the issue body — not editing source files. The path you take depends on the first word of## Command:4a-execute-async.
sbatchorsalloc(async slurm pipeline). Open a placeholder PR first so the cycle has somewhere to record the queued job; the artifacts get appended via a separate commit once the job finishes.-
Read the issue's
## Commandblock to a temp file (preserves shell metacharacters; never substitute the command into a quoted string):cmd_file=$(mktemp) gh issue view <issue_num> --json body --jq '.body' \ | awk '/^## Command/{flag=1; next} /^## /{flag=0} flag' \ | sed '/^[[:space:]]*$/d; /^```/d' > "$cmd_file" -
Parse expected artifact paths from the issue body. Prefer the machine-parseable
## Expected Artifactsblock (one bare path per line, no bullets, no labels — produced by the updated/issuesskill). Fall back to## Filesfor issues filed before the/issuesupdate, accepting some risk of false-negatives if the block contains prose:body=$(gh issue view <issue_num> --json body --jq '.body') expected_artifacts=$(printf '%s' "$body" \ | awk '/^## Expected Artifacts/{flag=1; next} /^## /{flag=0} flag' \ | sed '/^[[:space:]]*$/d; /^```/d; s/^[[:space:]]*//') if [ -z "$expected_artifacts" ]; then # Backward-compat: try ## Files. Strip leading bullets/whitespace; the # block may contain labels like "Script: foo.sh" which will fail the # `[ -e ]` check in slurm_complete.sh — that's the trade-off for # supporting pre-Expected-Artifacts issues. expected_artifacts=$(printf '%s' "$body" \ | awk '/^## Files/{flag=1; next} /^## /{flag=0} flag' \ | sed '/^[[:space:]]*$/d; /^```/d; s/^- *//; s/^[[:space:]]*//') fi if [ -z "$expected_artifacts" ]; then echo "ERROR: issue #<issue_num> has neither ## Expected Artifacts nor ## Files block; cannot verify async slurm output. Defer to human." >&2 bash "$LIB_ROOT/defer_kind_execute.sh" <issue_num> "kind:execute issue is missing ## Expected Artifacts block required by the async slurm pipeline" # Skip this issue; continue with the next in the batch fiIf empty after both checks, defer the issue rather than open a PR that can never complete.
Also parse an optional
## Completion Sentinelblock — a single regex (grep -E syntax) the wrapper writes to its log on successful completion. Only relevant forsalloc(sbatch reaches sacct COMPLETED naturally); harmless for sbatch. Empty/missing block = no early-complete detection, allocation runs to walltime:completion_sentinel=$(printf '%s' "$body" \ | awk '/^## Completion Sentinel/{flag=1; next} /^## /{flag=0} flag' \ | sed '/^[[:space:]]*$/d; /^```/d; s/^[[:space:]]*//' \ | head -1)Take only the first non-empty line — multi-line sentinels are not supported. The wrapper-side contract (artifact-write ordering + sentinel-as-last-output) is documented in
issues/SKILL.md. -
QoS slot precheck (before opening the placeholder PR). If the command specifies a
--qos, count the user's in-flight jobs at that QoS and refuse if the cap is reached. Doing this BEFOREgh pr createmeans a cap-blocked issue doesn't leave a strayasync-pendingPR for someone to clean up later:bash "$LIB_ROOT/slurm_qos_precheck.sh" "$cmd_file" rc=$? case "$rc" in 0) ;; # OK to proceed 4) # Slot cap reached. Leave the issue audit-loop-labeled so it # retries next cycle (do NOT defer-to-human; the cap is # transient). Skip this issue and move on to the next in the # batch. echo "INFO: skipping issue #<issue_num> this cycle: QoS slot cap reached" >&2 continue ;; *) # Bad args or other hard failure. Treat as undeferrable — # defer to human and move on. bash "$LIB_ROOT/defer_kind_execute.sh" <issue_num> "slurm_qos_precheck.sh exited $rc: see stderr" continue ;; esac -
Create the branch + placeholder commit + PR. The placeholder commit references the issue so the eventual artifact commit has a clean history:
slug="run-issue-<issue_num>" git checkout <default-branch> && git pull git checkout -b "audit/$slug" git commit --allow-empty -m "queue slurm job for issue #<issue_num>" git push -u origin "audit/$slug" pr_number=$(gh pr create \ --title "audit: run issue #<issue_num> (async slurm)" \ --body "## Async slurm job
-
Issue: #<issue_num> Expected Artifacts: $(printf -- '- %s\n' $expected_artifacts)
This PR will be updated with the artifact commit once the slurm job finishes. Audit review is skipped while the `async-pending` label is set."
--label "audit-loop" --label "async-pending" --json number --jq '.number')
5. Submit via `slurm_submit.sh`. The helper dispatches sbatch/salloc, captures the job ID, appends to `pending_async_jobs`, and returns immediately. Run it in the foreground (do NOT background — the script's blocking is the signal that something is wrong). Pass `COMPLETION_SENTINEL` only if the issue had a `## Completion Sentinel` block: bash
EXPECTED_ARTIFACTS="$expected_artifacts"
COMPLETION_SENTINEL="$completion_sentinel"
bash "$LIB_ROOT/slurm_submit.sh" <issue_num> "$pr_number" "audit/$slug" "$cmd_file"
```
**On non-zero exit from `slurm_submit.sh` — CRITICAL:** the issue's `## Command` is not safe to run via the async pipeline. Common causes: salloc timeout on a non-instant-grant QoS (exit 1), QoS guard rejection (exit 2), or cgroup check failure (exit 1 after login-node leak detection).
You **MUST NOT**:
- Rewrite the issue's `## Command` to use a different scheduler or QoS
- Author a wrapper script (e.g. `scripts/run_issue_<N>_sbatch.sh`) and re-invoke `slurm_submit.sh` against it
- Re-invoke `slurm_submit.sh` with any modified arguments
You **MUST**:
- Capture the stderr tail from the failed call
- Call `abandon_async_pr.sh` to clean up the placeholder PR in one operation — defers the issue, labels the PR `needs-human`, strips the speculative `async-pending` label:
```bash
bash "$LIB_ROOT/abandon_async_pr.sh" "$pr_number" <issue_num> "slurm_submit.sh exited non-zero: <one-line stderr summary>"
```
- Remove the entry from `pending_async_jobs` (the helper may or may not have appended depending on where it failed; safer to dedupe):
```bash
new_list=$(jq --argjson pr "$pr_number" '[.pending_async_jobs[] | select(.pr_number != $pr)]' .audit-loop/state.json)
bash "$LIB_ROOT/state_update.sh" address pending_async_jobs "$new_list"
```
- Move on to the next batch. Do NOT send `AUDIT: review` for this PR.
This rule exists because `slurm_submit.sh`'s exit codes are the only signal that the submitted command will not actually run as advertised. A silent retry under a different scheduler returns instantly with a fake-looking success (sbatch returns a job ID even when the job sits PD'd for hours), violating the `kind:execute` contract.
6. Mark the issue in-progress and update batch state. Do not send AUDIT: review — the audit agent should not review until artifacts land. Move on to the next batch (or signal completion if this was the last):
bash gh issue edit <issue_num> --remove-label "in-progress" --add-label "fix-submitted" bash "$LIB_ROOT/state_update.sh" address \ current_batch "$batch_number" \ batches_created "$new_batches_created"
Note: current_pr is NOT set to this PR — there's no active review. The next active PR (from a non-async batch, or from a subsequent ADDRESS: poll-async finalizing one of these) will set current_pr then.
4a-execute-inline. Non-slurm in-session command. The original kind:execute flow:
- Run each shell command in the issue's
## Acceptance Criteriablock yourself and verify it exits 0 / matches the expected output. - Commit ONLY the produced artifacts (and any incidental log/metadata files the command writes into the repo). Do not bundle unrelated code changes.
- If the command requires editing the codebase to make it run (e.g. flag plumbing, env-var hygiene), that change belongs in a separate
kind:changeissue/PR that this execute-issue depends on — not silently in this PR. If you discover such a prerequisite mid-run, stop, file the change issue, label the execute issue blocked-on that issue, and exit the batch. - You MUST NOT swap a
kind:executeissue for a tooling/refactor PR. If the run cannot complete in this session, defer viadefer_kind_execute.shas in step 6 validation. Do not writeCloses #<execute-issue>in a PR whose diff does not contain the produced artifacts.
4b. Fix each issue:
If the issue is a reopened recurrence (the audit agent added a "recurrence detected" comment), the previous fix was insufficient. Identify the root cause and apply a systemic fix (validation middleware, shared helper, lint rule, regression test) rather than patching the same symptom again. Note the prior fix commit in the PR body.
When multiple issues in the batch share a root cause (e.g., 3 unsanitized inputs in different handlers), prefer a single systemic fix over N individual patches. For example: add input validation middleware instead of sanitizing each handler separately.
Scope ceiling: A systemic fix may touch at most 3 files not directly referenced in the batch's issues. If fixing the root cause properly requires more, open a separate issue titled "Systemic: " with your proposed fix plan, label it audit-loop + severity:high + the matching cat: label, and apply a scoped partial fix to the immediate symptoms instead.
For each issue:
- Read the referenced file(s) — don't fix blind
- Understand full context before changing anything
- Apply targeted fixes — systemic when the issue recurs, minimal when it's new
- If the issue's suggested fix is sound, follow it; if it's a band-aid for a recurring problem, go deeper
-
Discover and run the project's linter/formatter before committing:
Makefile→ check for targets:lint,check,fmt,formatpackage.json→ checkscriptsfor:lint,format,check,typecheckCargo.toml→ runcargo clippyandcargo fmt --checkpyproject.toml/setup.cfg→ runruff check .orblack --check .orflake8.golangci.yml→ rungolangci-lint run- Go files without golangci → run
go vet ./...
If a linter reports auto-fixable errors, fix them before committing. If unfixable errors remain, note them in the PR body and add the
tests-failinglabel after creating the PR. -
If tests fail after fixing:
- Attempt to fix the test failure once
- If unable, label the PR
tests-failingafter creating it (step 8)
-
Stage and commit. Never stage files that likely contain secrets (
.env,.env.*,credentials.json,*.pem,*.key,*.p12,*.pfx,id_rsa*,*.secret). If any are among the changed files, exclude them and print a warning. Always use explicit file paths — nevergit add -Aorgit add ..git add <specific files> git commit -m "$(cat <<'EOF' fix: <batch summary> Closes #<issue1>, closes #<issue2> EOF )" -
Update documentation. If the fix changes behavior, configuration, or APIs, update the relevant docs (README, inline comments, config examples, usage instructions). Don't leave stale docs behind.
-
Self-review before pushing. Run
git diff HEAD~1and verify:- Each change addresses the root cause hypothesis, not just the symptom
- No files were modified that aren't referenced in the batch's issues (exception: shared helpers for a systemic fix, up to the 3-file scope ceiling)
- No debug output, commented-out code, or TODOs were introduced
- Documentation affected by the changes was updated
If the diff contains out-of-scope changes, revert them with
git checkout HEAD~1 -- <file>and amend the commit. -
Push and create PR. Sanitize the title to strip shell metacharacters:
git push -u origin "audit/$slug"
pr_title=$(cat <<'EOF' | tr -d '`$"' | tr -d "'" | head -c 70
audit: <batch summary>
EOF
)
gh pr create \
--title "$pr_title" \
--body "$(cat <<'PREOF'
## Audit Fixes (Batch <N>)
<!-- audit-revision: 0 -->
Closes #<issue1>: <title>
Closes #<issue2>: <title>
## Root Cause Analysis
<copy the hypotheses from .audit-loop/batch-N-hypotheses.md here — root cause, fix, risk for each issue>
## Changes
<brief description of what was changed and why>
## Test plan
<how to verify the fixes>
PREOF
)" \
--label "audit-loop" --label "audit-batch-<N>"
If tests are failing, also add --label "tests-failing".
If gh pr create fails because a PR already exists for this branch (crash recovery scenario), retrieve the existing PR instead:
existing=$(gh pr view --json number -q '.number' 2>/dev/null)
Use that PR number. Do not attempt to create a duplicate.
-
Update issue labels — mark all issues in this batch as fix-submitted:
for issue in <issue_numbers>; do gh issue edit "$issue" --remove-label "in-progress" --add-label "fix-submitted" done -
Persist batch state — single atomic write:
bash "$LIB_ROOT/state_update.sh" address \ current_pr "$pr_number" \ current_batch "$batch_number" \ batches_created "$new_batches_created" \ revision_round '0' \ revision_history '[]' \ phase '"reviewing"' -
Notify audit agent:
tmux send-keys -t audit "AUDIT: review PR #<number> "
Then in a separate bash call (preserve the trailing space above; do NOT chain with && or ;):
tmux send-keys -t audit Enter
Signal completion
bash "$LIB_ROOT/state_update.sh" address phase '"complete"' last_trigger '"ADDRESS: complete"'
tmux send-keys -t audit "AUDIT: complete "
Then in a separate bash call (preserve the trailing space above; do NOT chain with && or ;):
tmux send-keys -t audit Enter
Print:
## Audit Fix Complete
Batches completed: <N>
Issues fixed: <N>
Issues remaining: <N> (open with audit-loop label)
PRs needing human review: <N> (labeled needs-human)
Loop error handling
- If
tmux send-keys -t auditfails: stop, tell the user to start the audit agent in a tmux session namedaudit. - If
ghcommands fail: retry once, then stop with error. - If merge conflicts can't be resolved: label
needs-human, sendADDRESS: continuebehavior (move to next batch). - If tests fail after fixing: label
tests-failing, note in PR body, proceed with review request (the audit agent will catch it).
Manual Protocol
For direct use without the audit loop. User pastes findings or provides a file path.
Input Handling
- File path as argument (
/address path/to/findings.md) — read the file - No argument / inline (
/address) — the user's message contains the findings. If empty, ask once: "Paste or describe the audit findings." - Directory (
/address dir/) — glob for*.mdor*.txt
Parse findings into a structured list. Each finding should capture (when available):
- ID — original identifier or generate sequential (F-1, F-2, ...)
- Severity — critical / high / medium / low / info
- Category — security, correctness, performance, maintainability, style, dependency
- File + location — file path and line range
- Description — what the issue is
- Suggestion — what the audit tool recommended (if any)
Step 1: Parse & Inventory
Print a compact table:
## Audit Inventory ({n} findings)
| # | Sev | Cat | File | Summary |
|-----|--------|--------------|-------------------|------------------------|
| F-1 | high | security | src/auth.go:42 | SQL injection in login |
| F-2 | medium | correctness | lib/parse.rs:118 | Off-by-one in boundary |
If more than 15 findings, group by category first.
Step 2: Cross-Reference History
For each finding, check if it's new or recurring:
-
Git log — check file history and grep for related keywords:
git log --oneline --all -20 -- {filepath} git log --oneline --all --grep="{keyword}" -10 -
Git blame — check age of flagged lines:
git blame -L {start},{end} {filepath} -
Memory check — if your agent has a persistent memory/notes system, scan it for past audit references.
-
Classify: New, Recurring (note prior commit), Known (documented tech debt), Regression (was fixed, reappeared — flag prominently).
Step 3: Triage
## Triage
### Fix now ({n})
- F-1: SQL injection in login (high/security, NEW)
### Needs discussion ({n})
- F-3: Deprecated dependency (major version bump required)
### Skip ({n})
- F-7: Style nit (no functional impact)
Auto-classify:
- Fix now: severity >= high, OR regression, OR security >= medium
- Needs discussion: requires architectural change or major dependency update
- Skip: info-level, pure style nits
Ask: "Proceed with 'Fix now' items? Any from 'Needs discussion' to include or exclude?"
If the user indicated blanket approval, proceed without waiting.
Step 4: Fix
- Read relevant file(s) for full context
- For recurring or regression findings, identify the root cause — apply a systemic fix (shared helper, middleware, lint rule, regression test) rather than patching the same symptom. Note the prior fix commit in the report.
- When multiple findings share a root cause, prefer a single systemic fix over N individual patches
- For new, isolated findings, apply minimal, targeted fixes via Edit
- Follow audit suggestion if sound; otherwise use judgment and note divergence
- Run test/lint commands if discoverable
Step 5: Report
## Audit Resolution
**Source:** {tool name + date}
**Findings:** {total} | Fixed: {n} | Skipped: {n} | Deferred: {n}
### Fixed
| # | File | What changed | Status |
|-----|-----------------|-------------------------------------|------------|
| F-1 | src/auth.go:42 | Parameterized query | New |
| F-5 | lib/cache.rs:30 | Added mutex guard | Regression |
### Regressions detected
- F-5: previously fixed in `abc1234`, reintroduced in `def5678`. Consider adding a test.
### Recurring patterns
{Root cause analysis if multiple findings share a pattern.}
### Deferred
- F-3: Deprecated dependency — requires major version bump. Schedule separately.
### Skipped
- F-7: Style nit — no functional impact
Step 6: Memory (optional)
If the audit revealed a recurring pattern or systemic issue, offer to save to memory. Don't save routine findings.