debug

SkillFiles & storage

Investigation-first debugging — gather evidence, form confirmed root-cause hypothesis, hand off to fix mode with diagnosis file. TRIGGER when: user reports a symptom or failing test with Python traceback, or asks to investigate a runtime/CI failure with reproducible evidence; phrases: "debug this failure", "why is X broken", "find the root cause of <error>", "investigate this CI failure". SKIP when: pure config quality issues (use `/foundry:audit`); broad system-wide diagnosis without traceback (use `/foundry:investigate`); user already knows the fix (use `/develop:fix`); non-Python project.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the debug skill

What this skill tells your AI

The instructions your AI receives, as published by borda/ai-rig in plugins/cc_develop/skills/debug/SKILL.md and read by ahel’s review.

Investigation-first debugging. Gather evidence, trace data flow, form confirmed root-cause hypothesis, hand off to fix mode.

NOT for: production incidents without any CI run ID or local traceback (use /foundry:investigate (requires foundry plugin) for triage); .claude/ config issues (use /foundry:audit (requires foundry plugin)); non-Python projects (JS/TS/Go/Rust) — toolchain assumes pytest; use language-native toolchain instead. CI-only failures ARE supported — pass --ci-run <run-id or URL> to use GitHub Actions logs as evidence source.

Issue ID routing note: issue mode selected when --issue flag present, or when argument (after other flags stripped) is a pure run of digits with an optional # prefix (e.g. 123 or #123). No numeric threshold. Pass --issue <N> to force issue mode for any argument.

  • Key boundary: after Steps 1+2 — evidence gathered and pattern analysis complete, before hypothesis gate (Step 3).
  • Preserve: debug mode, CI run ID if set, evidence signals (issue body, test path), tried-hypotheses ledger (candidate causes + verdicts — refuted/ruled-out/open), --keep items.
  • Refresh also after any Step 3 probe that rules out a hypothesis — so post-compact gate does not re-test refuted causes (loop guard).

Agent Resolution

export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
_DEV_SHARED=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_develop}/bin/dev_shared_resolve.py" 2>/dev/null)  # timeout: 5000
[ -z "$_DEV_SHARED" ] && _DEV_SHARED="plugins/cc_develop/skills/_shared"
echo "$_DEV_SHARED" > "${TMPDIR:-/tmp}/dev-shared-${CSID}"  # cold resolve — every later block warm-reads this
# loads: compaction-contract.md
cat "$_DEV_SHARED/agent-resolution.md"

Contains: foundry check + fallback table. If foundry not installed: substitute each foundry:X with general-purpose per table. Agents this skill uses: foundry:sw-engineer, foundry:challenger.

export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
IFS= read -r _DEV_SHARED < "${TMPDIR:-/tmp}/dev-shared-${CSID}" 2>/dev/null || _DEV_SHARED=""  # timeout: 5000
[ -z "$_DEV_SHARED" ] && _DEV_SHARED="plugins/cc_develop/skills/_shared"
cat "$_DEV_SHARED/task-hygiene.md"

Project Detection

export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
IFS= read -r _DEV_SHARED < "${TMPDIR:-/tmp}/dev-shared-${CSID}" 2>/dev/null || _DEV_SHARED=""  # timeout: 5000
[ -z "$_DEV_SHARED" ] && _DEV_SHARED="plugins/cc_develop/skills/_shared"
cat "$_DEV_SHARED/runner-detection.md"

Sets $TEST_CMD (full suite) and $PYTEST_CMD (pytest flags). Run at skill start.

Language preflight gate: detect project language; adjust test runner accordingly.

# timeout: 5000
LANG_HINT="python"
if [ ! -f "pyproject.toml" ] && [ ! -f "setup.py" ] && [ ! -f "setup.cfg" ] && [ ! -f "Pipfile" ]; then
    if [ -f "package.json" ]; then LANG_HINT="node"
    elif [ -f "go.mod" ]; then LANG_HINT="go"
    elif [ -f "Cargo.toml" ]; then LANG_HINT="rust"
    fi
fi

If LANG_HINT not python: invoke AskUserQuestion — "Non-Python project detected ($LANG_HINT). Toolchain assumes pytest. How to proceed?" · (a) Abort — use language-native runner · (b) Continue — repo also has Python sources. On Abort: stop.

Checkpoint: debug = investigation only — no code changes. .plans/active/debug_<slug>.md (written in Step 4) serves as implicit session state. No .developments/ checkpoint needed.

Flag parsing

Parse flags into actual shell variables (not prose) so downstream blocks see correct values:

export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
KEEP_ITEMS=""
if [[ "$ARGUMENTS" =~ --keep[[:space:]]\"([^\"]+)\" ]]; then
    KEEP_ITEMS="${BASH_REMATCH[1]}"
fi
echo "$KEEP_ITEMS" > "${TMPDIR:-/tmp}/dev-debug-keep-items-${CSID}"
rm -f .temp/state/skill-contract.md ${TMPDIR:-/tmp}/dev-debug-hypotheses-${CSID}  # timeout: 5000
# timeout: 10000
python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_develop}/bin/dev_parse_args.py" \
    --skill debug --write-files "$ARGUMENTS"
# URL normalization + log fetching: §URL Normalization in ci-log-extract.md

Worktree isolation

loads: worktree-isolation.md

When --worktree set, run the investigation in an isolated git worktree so reproduction attempts (repro scripts, temp edits) can never mutate the main sources — before codemap detection or Step 1.

# timeout: 5000
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
IFS= read -r WORKTREE_ENABLED < "${TMPDIR:-/tmp}/dev-debug-worktree-${CSID}" 2>/dev/null; [ "$WORKTREE_ENABLED" = "true" ] || WORKTREE_ENABLED=false
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
IFS= read -r _DEV_SHARED < "${TMPDIR:-/tmp}/dev-shared-${CSID}" 2>/dev/null || _DEV_SHARED=""  # timeout: 5000
[ -z "$_DEV_SHARED" ] && _DEV_SHARED="plugins/cc_develop/skills/_shared"
cat "$_DEV_SHARED/worktree-isolation.md"

WORKTREE_ENABLED=true → follow §Enter (base off HEAD, EnterWorktree(path=…)). Read-only skill — obey §Deliverable: the diagnosis file is written to the main tree ($_ORIG_ROOT) at Step 4 so /develop:fix can read it. Else skip — run in main tree.

Codemap resolveCODEMAP_RAW already written to ${TMPDIR:-/tmp}/dev-debug-codemap-${CSID} (per-skill) and ${TMPDIR:-/tmp}/dev-codemap-raw-${CSID} (legacy) by flag-parsing block above (via dev_parse_args.py --skill debug --write-files). Read per-skill path, then normalize via codemap_resolve.py:

# timeout: 5000
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
# skill-specific paths (dev-debug-codemap-*) avoid stale value from a prior feature --codemap run
CODEMAP_ENABLED=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_develop}/bin/dev_codemap_gate.py" debug) || exit 1

loads: codemap-gates.md

export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
IFS= read -r _DEV_SHARED < "${TMPDIR:-/tmp}/dev-shared-${CSID}" 2>/dev/null || _DEV_SHARED=""  # timeout: 5000
[ -z "$_DEV_SHARED" ] && _DEV_SHARED="plugins/cc_develop/skills/_shared"
cat "$_DEV_SHARED/codemap-gates.md"

Follow Gate A and Gate B.

Downstream blocks read back: IFS= read -r CHALLENGE_ENABLED < "${TMPDIR:-/tmp}/dev-challenge-enabled-${CSID}" 2>/dev/null || CHALLENGE_ENABLED=true, IFS= read -r CHALLENGE_FORCED < "${TMPDIR:-/tmp}/dev-challenge-forced-${CSID}" 2>/dev/null || CHALLENGE_FORCED=false, IFS= read -r TEAM_MODE < "${TMPDIR:-/tmp}/dev-team-mode-${CSID}" 2>/dev/null || TEAM_MODE=false, IFS= read -r CI_RUN_ID < "${TMPDIR:-/tmp}/dev-ci-run-id-${CSID}" 2>/dev/null || CI_RUN_ID="".

export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
IFS= read -r _DEV_SHARED < "${TMPDIR:-/tmp}/dev-shared-${CSID}" 2>/dev/null || _DEV_SHARED=""  # timeout: 5000
[ -z "$_DEV_SHARED" ] && _DEV_SHARED="plugins/cc_develop/skills/_shared"
cat "$_DEV_SHARED/ci-log-extract.md"

Follow §URL Normalization to set CI_RUN_ID. If CI_RUN_ID set, follow §Log Fetching and §Log Parsing to set CI_LOG_EVIDENCE; use it as evidence source in Step 1 instead of local pytest.

Unsupported flag check — after ALL supported flags extracted (including --issue and --keep from blocks above), scan $ARGUMENTS for remaining --<token> tokens not in supported list. Do NOT include --issue or --keep in "unknown" set — both are consumed by the mode-detect and keep-parse blocks above. Supported: --no-challenge, --challenge, --team, --worktree, --ci-run, --issue, --repo, --codemap, --no-codemap, --keep. If truly unknown token found: print ! Unknown flag(s): `--<token>`. then invoke AskUserQuestion — (a) Abort (stop, re-invoke with correct flags) · (b) Continue ignoring (skip unknown flags, proceed). On Abort: stop.

Mode selection — debug runs in one of two mutually-exclusive modes; set explicitly before any Step:

# timeout: 5000
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
# strip flags first — "123 --no-challenge" would fail integer detection otherwise
ARGUMENTS_FOR_MODE_DETECT=$(echo "$ARGUMENTS" | sed -E 's/--no-challenge|--challenge|--team|--worktree|--ci-run[= ]?[^ ]+|--issue|--repo[= ]?[^ ]+|--no-codemap|--codemap|--keep +"[^"]+"//g' | xargs)
if [[ " $ARGUMENTS " == *" --issue "* ]] || [[ "$ARGUMENTS_FOR_MODE_DETECT" =~ ^#?[0-9]+$ ]]; then
    DEBUG_MODE="issue"
else
    DEBUG_MODE="symptom"
fi
echo "$DEBUG_MODE" > ${TMPDIR:-/tmp}/dev-debug-mode-${CSID}

Subsequent steps branch by DEBUG_MODE:

  • Issue mode: Step 1 fetches issue body and extracts test path before invoking pytest; skip symptom-text pytest block. Stop after Step 4 (handoff) — do not run symptom-text branches.
  • Symptom mode: Step 1 skips issue fetch; uses free-text symptom directly. Skip issue-mode pytest block entirely.

If TEAM_MODE=true — execute team investigation now in place of standard Steps 1-2. After team synthesis completes, run Steps 3-4 inline (hypothesis gate + handoff to fix) on winning hypothesis — do not return to standard Steps 1-2. Authoritative reading: team mode replaces Steps 1-2 (parallel hypothesis investigation supplants serial evidence gathering); Steps 3-4 still execute (inline within this block, not by looping back to standard workflow):

  1. export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"; IFS= read -r _DEV_SHARED < "${TMPDIR:-/tmp}/dev-shared-${CSID}" 2>/dev/null || _DEV_SHARED=""; [ -z "$_DEV_SHARED" ] && _DEV_SHARED="plugins/cc_develop/skills/_shared"; cat "$_DEV_SHARED/preflight-helpers.md" §Team Spawn Template. Confirm [ROLE_PHRASE] = symptom text (from $ARGUMENTS stripped of flags), [FILE_SLUG] = debug-hypothesis.
  2. Run project detection (export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"; IFS= read -r _DEV_SHARED < "${TMPDIR:-/tmp}/dev-shared-${CSID}" 2>/dev/null || _DEV_SHARED=""; [ -z "$_DEV_SHARED" ] && _DEV_SHARED="plugins/cc_develop/skills/_shared"; cat "$_DEV_SHARED/runner-detection.md") to set $TEST_CMD and $PYTEST_CMD.
  3. Compute TS=$(date -u +%Y-%m-%dT%H-%M-%SZ) and mkdir -p ".temp/develop/$TS". Spawn 2-3 foundry:sw-engineer agents (model=opus) in parallel — each investigating one independent root-cause hypothesis. Use Team Spawn Template from preflight-helpers: replace [ROLE_PHRASE] with symptom, [FILE_SLUG] with debug-hypothesis, assign each agent a distinct hypothesis number N. Each agent writes full output to .temp/develop/$TS/debug-hypothesis-N-$TS.md (run-dir timestamp, matching preflight-helpers §Team Spawn Template) and returns compact JSON {"status":"done","file":"<path>","findings":N,"confidence":0.N,"summary":"<one-line description of hypothesis>"}.
  4. Coordination: lead broadcasts {symptom: <description>, traceback: <key lines>} to teammates before spawning. After all return, facilitate cross-challenge between competing analyses. Convergence rule: select hypothesis with most direct evidence (observable in code or logs); if truly tied, invoke AskUserQuestion presenting top 2 competing hypotheses.
  5. Synthesis trace (lead, inline — no spawn): after individual teammate reports, lead reads all teammate findings from .temp/develop/$TS/debug-hypothesis-*.md (2-3 files already on disk) and produces the unified cross-cutting trace map itself — entry point, modules crossed, state mutations, invariant violations across hypotheses. Write to .temp/develop/$TS/debug-trace-synthesis.md. A dedicated synthesis agent costs ~120,851 tok of fixed overhead for a read-and-merge the lead performs inline in fix's equivalent step — spawn nothing here.
  6. Lead synthesises consensus root cause from the trace map + competing hypotheses. Run Steps 3-4 of standard workflow (hypothesis gate + hand off to fix) on winning hypothesis — execute those steps inline here; do not loop back through Steps 1-2. Step 3 gate in team mode: if convergence reached by synthesis agent (all hypotheses point to same root cause with high confidence), present converged hypothesis without a new user confirmation prompt — state "Team converged on root cause (no ambiguity)" and proceed directly to Step 4 handoff. Only invoke AskUserQuestion at Step 3 if competing hypotheses remain or convergence declared by default (tied evidence).

Health monitoring (CLAUDE.md §6): for each spawned agent, use a per-agent sentinel keyed on loop counter $N (not literal N). Loop over agent indices in actual bash:

# timeout: 5000
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
for N in 1 2 3; do
    touch "${TMPDIR:-/tmp}/debug-team-check-${N}-${CSID}"
done

Poll each independently every 5 min via find .temp/develop/$TS -name "debug-hypothesis-${N}*" -newer ${TMPDIR:-/tmp}/debug-team-check-${N}-${CSID} | wc -l where $N is actual agent index in loop variable — the -name scope is load-bearing: a directory-wide find marks every agent alive whenever any sibling writes, collapsing exactly the per-agent isolation these sentinels exist for. Poll only the indices actually spawned (2-hypothesis run → poll N=1,2 only; the third touched sentinel is a harmless unused file). A single shared sentinel collapses health isolation — stalled agent N=2 cannot be distinguished from active agent N=1. Hard cutoff 15 min no-file-activity per agent; mark timed-out agents with ⏱ in synthesis.

Step 1: Understand the symptom

Collect all signals before forming any hypothesis.

Structural context (codemap-py — only if CODEMAP_ENABLED=true): if index available, run before codebase exploration to pre-load blast-radius context for failing module:

# timeout: 10000
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
IFS= read -r CODEMAP_ENABLED < "${TMPDIR:-/tmp}/dev-debug-codemap-enabled-${CSID}" 2>/dev/null || CODEMAP_ENABLED="false"
if [ "$CODEMAP_ENABLED" = "true" ]; then
    codemap-py query central --top 5 2>/dev/null
fi

After reading traceback or $ARGUMENTS, derive TARGET_MODULE: strip src/, .py suffix, replace / with . (e.g. src/mypackage/auth.pymypackage.auth); capture the failing function name too, if known, as FAILING_FN. TARGET_MODULE is a substitution token — resolve into the shell variable before the block below. Do NOT execute with literal <TARGET_MODULE> — bash would interpret < as stdin redirect:

# resolve TARGET_MODULE/FAILING_FN first, e.g. TARGET_MODULE=mypackage.auth; FAILING_FN=validate
# timeout: 10000
if [ -z "$TARGET_MODULE" ]; then
    echo "⚠ TARGET_MODULE not resolved — skipping codemap rdeps/fn-blast query"
else
    codemap-py query rdeps "$TARGET_MODULE" 2>/dev/null
    [ -n "$FAILING_FN" ] && codemap-py query fn-blast "$TARGET_MODULE::$FAILING_FN" 2>/dev/null  # v3 index only
fi

If codemap-py results returned: prepend ## Structural Context (codemap-py) block to foundry:sw-engineer spawn prompt (Step 1). Callers of failing module = likely affected paths to verify after fix. fn-blast shows transitive callers — high-depth callers are regression risk.

Issue-number mode first — if $ARGUMENTS is issue number, fetch issue body and extract test path BEFORE invoking pytest:

# timeout: 6000
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
# wrapper: cross-repo branch from dev-upstream, persists body to dev-issue-body-${CSID} for next block (avoids re-running gh)
python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_develop}/bin/dev_issue_fetch_wrap.py" debug "$ARGUMENTS"
# timeout: 5000
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
# grep file directly — `read -r` would capture only the first line of a multi-line issue body
TEST_PATH=$(grep -oE '(tests?/[^[:space:]]+\.py|test_[^[:space:]]+\.py)' "${TMPDIR:-/tmp}/dev-issue-body-${CSID}" 2>/dev/null | head -1)
if [ -z "$TEST_PATH" ]; then
  echo "→ No test file found in issue; running full test suite"
elif [ ! -f "$TEST_PATH" ]; then
  echo "⚠ test path from issue not found on disk: $TEST_PATH — running full suite"
  TEST_PATH=""
fi
echo "$TEST_PATH" > "${TMPDIR:-/tmp}/dev-debug-test-path-${CSID}"  # persist — three later blocks consume it

Run pytest with extracted path (empty $TEST_PATH → full suite). $TEST_PATH stays unquoted so an empty value collapses to no argument rather than an empty one:

# timeout: 600000
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
IFS= read -r PYTEST_CMD < "${TMPDIR:-/tmp}/dev-pytest-cmd-${CSID}" 2>/dev/null || PYTEST_CMD=""
IFS= read -r TEST_PATH  < "${TMPDIR:-/tmp}/dev-debug-test-path-${CSID}" 2>/dev/null || TEST_PATH=""
if [ -z "$PYTEST_CMD" ]; then
    echo "! PYTEST_CMD unresolved — re-run §Project Detection (runner-detection.md); an empty command would exit 127 and be misread as a reproduced bug"
else
    $PYTEST_CMD --tb=long ${TEST_PATH} -v 2>&1 | tail -60
    GATE_EXIT=${PIPESTATUS[0]}
    echo "$GATE_EXIT" > "${TMPDIR:-/tmp}/dev-gate-exit-${CSID}"
    if [ "$GATE_EXIT" -ne 0 ]; then
        echo "Bug reproduced — tests fail. Proceed to fix."
    else
        echo "Tests pass — bug may not be reproducible via pytest; check symptom directly."
    fi
fi
# timeout: 3000
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
IFS= read -r TEST_PATH < "${TMPDIR:-/tmp}/dev-debug-test-path-${CSID}" 2>/dev/null || TEST_PATH=""
git log --oneline -20
COMMIT_COUNT=$(git rev-list --count HEAD 2>/dev/null || echo 1)
LOOKBACK=$(( COMMIT_COUNT < 5 ? COMMIT_COUNT : 5 ))
if [ "$LOOKBACK" -gt 1 ]; then
    # empty pathspec is fatal (exit 128) — omit `--` clause for full-repo diff
    if [ -n "$TEST_PATH" ]; then
        git diff "HEAD~${LOOKBACK}..HEAD" -- "$TEST_PATH"
    else
        git diff "HEAD~${LOOKBACK}..HEAD"
    fi
fi

Cross-repo adaptation (when REPO_NAME set) — issue from different codebase. After fetching issue:

  1. Extract bug's root cause intent — what invariant violated, not just described symptoms (which may reference upstream structure or code paths)
  2. Search LOCAL codebase for equivalent failure site — grep for related symbols; code paths may differ from upstream due to divergence
  3. Treat upstream issue as debugging context, not as a map — trace actual failure in local code

Symptom-text mode — if $ARGUMENTS is free-text, skip issue fetch + extraction; locate failing test path from symptom directly. <test_path> is a substitution token — resolve into shell variable $TEST_PATH first (via Grep against symptom keywords or heuristic file search), then use $TEST_PATH in pytest call. Do NOT execute with literal <test_path> string — bash would interpret < as stdin redirect from a file named test_path:

# timeout: 600000
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
IFS= read -r PYTEST_CMD < "${TMPDIR:-/tmp}/dev-pytest-cmd-${CSID}" 2>/dev/null || PYTEST_CMD=""
TEST_PATH=""   # REPLACE with the resolved failing test path, e.g. $(grep -rlE '<symptom keyword>' tests/ --include='*.py' | head -1); empty → full suite
echo "$TEST_PATH" > "${TMPDIR:-/tmp}/dev-debug-test-path-${CSID}"  # persist — later blocks consume it
if [ -z "$PYTEST_CMD" ]; then
    echo "! PYTEST_CMD unresolved — re-run §Project Detection (runner-detection.md); an empty command would exit 127 and be misread as a reproduced bug"
else
    $PYTEST_CMD --tb=long ${TEST_PATH} -v 2>&1 | tail -60
    GATE_EXIT=${PIPESTATUS[0]}
    echo "$GATE_EXIT" > "${TMPDIR:-/tmp}/dev-gate-exit-${CSID}"
    if [ "$GATE_EXIT" -ne 0 ]; then
        echo "Bug reproduced — tests fail. Proceed to fix."
    else
        echo "Tests pass — bug may not be reproducible via pytest; check symptom directly."
    fi
fi

Claim-validation gate — before debugging, validate that user's expectation is itself correct. A bug report always contains an implicit or explicit claim: "X should behave like Y". Claim may be wrong — misread docs, misunderstood API contract, incorrect formula, outdated assumption. Fixing a correct implementation to match a wrong expectation wastes effort and introduces regressions.

Classify claim type and validate accordingly:

Claim typeExampleValidation approach
Numeric / metric result"IoU should be 0.5 but returns 0.3"Verify formula from authoritative source; compute expected value independently
API contract"function should return list but returns generator"Read docstring, type hints, and docs — not current implementation
Algorithm correctness"sorting is wrong — element 3 should come before element 1"Trace comparison logic against documented sort key or invariant
Behavioral invariant"adding item twice should raise, not silently dedupe"Check README, docs, or published contract — not assumed behavior
Cross-version assumption"this worked in v1, now broken"Check changelog/release notes for intentional breaking change
Domain-specific formulaML metric, statistical estimator, signal processingSpawn research:scientist (requires research plugin); pass metric name, formula used, claimed expected value; ask: "Is the claimed expected value correct per authoritative definition?"

Resolution rules:

  1. Claim verifiable from docs/type hints/tests → read source now; confirm before proceeding
  2. Claim verifiable by quick computation → run inline script to compute expected value independently
  3. Domain-specific claim requiring literature → spawn research:scientist; if plugin absent flag: ⚠ Expected value unverified — treating as assumption
  4. Claim contradicts docs/contract → it is a documentation misunderstanding, not a bug; surface this to user before any code change
  5. Gate: do not form root-cause hypothesis until claimed expectation confirmed or explicitly flagged as unverified; wrong expectation → wrong fix

Use Grep (pattern: failing symbol, class, or error keyword) to trace call path from entry point to failure site. Path hint: use src/ if exists, else search from project root (.).

Spawn foundry:sw-engineer agent to map execution path and produce:

  • Entry point to failure: which modules does call cross?
  • What state mutated along the way?
  • What invariant violated at failure point?
  • Any recent commit touching this path (from git log output)

Scope gate: if root cause spans 3+ modules, flag complexity smell. Use AskUserQuestion to present scope concern before proceeding, with options: "Narrow scope (Recommended)" / "Proceed anyway".

Present agent's analysis summary before proceeding.

Flaky-test branch — if symptom is intermittent (passes alone, fails in full suite): run binary-search isolation. <failing-test-node-id> is a substitution token — before executing this block, resolve failing test node ID from $ARGUMENTS or from prior pytest output (captured in a shell variable, e.g. FAILING_TEST_NODE=tests/foo.py::test_bar), then substitute literal node ID into command. Do NOT execute with literal <failing-test-node-id> string — bash would interpret < as stdin redirect:

# resolve FAILING_TEST_NODE first (bash reads literal <...> as redirect):
# FAILING_TEST_NODE=$(echo "$ARGUMENTS" | grep -oE 'tests?/[^[:space:]]+::test_[^[:space:]]+' | head -1)
if [ -z "$FAILING_TEST_NODE" ]; then
    echo "⚠ FAILING_TEST_NODE not resolved — cannot run polluter isolation; surface failing test node ID first"
else
    python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_develop}/bin/find-polluter.py" "$FAILING_TEST_NODE"  # timeout: 60000
fi

Output names polluting upstream test. find-polluter.py ships in this plugin's own bin/ (kept identical to foundry's canonical by propagate_shared.py), so this works on a develop-only install. Run only when CI shows non-deterministic failure pattern.

Step 2: Pattern analysis

Find nearest similar working code path, compare exhaustively:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
27
Forks
4
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
debug-borda
Source
github.com/borda/ai-rig