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.
Agent Resolution
_DEV_SHARED=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/develop}/bin/dev_shared_resolve.py" 2>/dev/null) # timeout: 5000
Read $_DEV_SHARED/agent-resolution.md. Contains: foundry check + fallback table. If foundry not installed: use table to substitute each foundry:X with general-purpose. Agents this skill uses: foundry:sw-engineer, foundry:challenger.
Read $_DEV_SHARED/task-hygiene.md.
Project Detection
Read $_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.
Debug Mode
Argument type detection: if
$ARGUMENTSis positive integer (or prefixed with#, e.g.#123), treat as GitHub issue number and fetch withgh issue view. If text (contains spaces, letters, or special chars), treat as symptom description.
Flag parsing
Parse flags into actual shell variables (not prose) so downstream blocks see correct values:
# timeout: 5000
CHALLENGE_ENABLED=true
TEAM_MODE=false
CI_RUN_ID=""
[[ " $ARGUMENTS " == *" --no-challenge "* ]] && CHALLENGE_ENABLED=false
[[ " $ARGUMENTS " == *" --team "* ]] && TEAM_MODE=true
set -- $ARGUMENTS
while [ $# -gt 0 ]; do
case "$1" in
--ci-run=*) CI_RUN_ID="${1#--ci-run=}" ;;
--ci-run) shift; CI_RUN_ID="${1:-}" ;;
esac
shift
done
# Persist for cross-block access (bash state lost between Bash() calls)
echo "$CHALLENGE_ENABLED" > ${TMPDIR:-/tmp}/dev-challenge-enabled
echo "$TEAM_MODE" > ${TMPDIR:-/tmp}/dev-team-mode
echo "$CI_RUN_ID" > ${TMPDIR:-/tmp}/dev-ci-run-id
# URL normalization and log fetching: see §URL Normalization in ci-log-extract.md below
Downstream blocks read back: CHALLENGE_ENABLED=$(cat ${TMPDIR:-/tmp}/dev-challenge-enabled 2>/dev/null || echo true), TEAM_MODE=$(cat ${TMPDIR:-/tmp}/dev-team-mode 2>/dev/null || echo false), CI_RUN_ID=$(cat ${TMPDIR:-/tmp}/dev-ci-run-id 2>/dev/null || echo "").
Read $_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, scan $ARGUMENTS for remaining --<token> tokens. If any found: print ! Unknown flag(s): \--<token>`. Supported: `--no-challenge`, `--team`, `--ci-run`, `--issue`.then invokeAskUserQuestion` — (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
if [[ " $ARGUMENTS " == *" --issue "* ]] || [[ "$ARGUMENTS" =~ ^#?[0-9]+$ ]]; then
DEBUG_MODE="issue"
else
DEBUG_MODE="symptom"
fi
echo "$DEBUG_MODE" > ${TMPDIR:-/tmp}/dev-debug-mode
Subsequent steps branch by DEBUG_MODE:
- Issue mode: Step 1 fetches issue body and extracts test path before invoking pytest; skip the 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 the issue-mode pytest block entirely.
If TEAM_MODE=true — execute team investigation now and exit; skip standard Steps 1-4:
- Read
$_DEV_SHARED/preflight-helpers.md§Team Spawn Template. Confirm[ROLE_PHRASE]= symptom text (from$ARGUMENTSstripped of flags),[FILE_SLUG]=debug-hypothesis. - Run project detection (read
$_DEV_SHARED/runner-detection.md) to set$TEST_CMDand$PYTEST_CMD. - Compute
TS=$(date -u +%Y-%m-%dT%H-%M-%SZ)andmkdir -p ".temp/develop/$TS". Spawn 2-3foundry:sw-engineeragents (model=opus) in parallel — each investigating one independent root-cause hypothesis. Use the Team Spawn Template from preflight-helpers: replace[ROLE_PHRASE]with the symptom,[FILE_SLUG]withdebug-hypothesis, assign each agent a distinct hypothesis number N. Each agent writes full output to.temp/develop/$TS/debug-hypothesis-N.mdand returns compact JSON{"status":"done","file":"<path>","findings":N,"confidence":0.N,"summary":"<one-line description of hypothesis>"}. - 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, invokeAskUserQuestionpresenting top 2 competing hypotheses. - Synthesis trace agent: spawn one
foundry:sw-engineersynthesis agent after individual teammate reports — read all teammate findings from.temp/develop/$TS/debug-hypothesis-*.md, produce unified cross-cutting trace map (entry point, modules crossed, state mutations, invariant violations across hypotheses). Write to.temp/develop/$TS/debug-trace-synthesis.md. - Lead synthesises consensus root cause from synthesis trace + competing hypotheses. Run Steps 3-4 of standard workflow (hypothesis gate + hand off to fix) on the winning hypothesis — execute those steps inline here; do not loop back through Steps 1-2.
Health monitoring (CLAUDE.md §6): for each spawned agent, create sentinel touch ${TMPDIR:-/tmp}/debug-team-check-N; poll every 5 min via find .temp/develop/$TS -newer ${TMPDIR:-/tmp}/debug-team-check-N -type f | wc -l; hard cutoff 15 min no-file-activity; mark timed-out agents with ⏱ in synthesis.
Step 1: Understand the symptom
Collect all signals before forming any hypothesis.
Issue-number mode first — if $ARGUMENTS is issue number, fetch issue body and extract test path BEFORE invoking pytest:
ISSUE_BODY=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/develop}/bin/issue_fetch.py" "$ARGUMENTS" 2>/dev/null) # timeout: 6000
echo "$ISSUE_BODY"
# Extract a test path (e.g., tests/foo.py or test_foo.py) from the issue body
TEST_PATH=$(echo "$ISSUE_BODY" | grep -oE '(tests?/[^[:space:]]+\.py|test_[^[:space:]]+\.py)' | 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
Run pytest with extracted path (empty $TEST_PATH → full suite):
# Read the full traceback — never just the last line # timeout: 600000
$PYTES