Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Triage night-market failures by symptom (hooks, CI, tests). Use when a check fails unexpectedly. Do not use for routine gates; use night-market-operations.
.claude/skills/athola-night-market-debugging-playbook/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 105% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 105% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 137% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 95% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 82% | 0% |
Match the symptom to a row, run the one discriminating command, apply the known fix. Every row below is a failure this repo has already paid for, with the commit hash that settled it. Do not re-derive a diagnosis that archaeology already produced.
Terms used throughout, defined once:
PostToolUse, Stop, SessionStart). Registered in a plugin's hooks/hooks.json with a command and a timeout.
timeout in seconds. The harnesskills the hook when it expires, before any output is honored.
python3 ... under the machine'ssystem Python (floor: 3.9), NOT the repo's uv-managed 3.12 venv. Third-party packages a plugin declares are not guaranteed present.
Every Makefile target that uses it has a Python fallback.
import x transitively pulls in,including the plugin's __init__.py.
| # | Symptom | Likely cause | Story | |---|---------|--------------|-------| | 1 | PreToolUse hook error / ModuleNotFoundError on every git commit | Unguarded third-party import in a plugin __init__.py reachable from a hook | 45dd77ef, 9bfc0a7a | | 2 | python39-compat is the only failing CI check | A 3.10+/3.11+ construct (datetime.UTC, bare X \| Y union) entered a hook import chain | 18c9340d, PR #511 | | 3 | Hook exits 0 but never does anything | Hook reads CLAUDE_TOOL_* env vars instead of stdin JSON | CHANGELOG 1.9.14 | | 4 | capabilities-sync CI fails | plugin.json registrations drifted from the book reference | capabilities-sync.yml | | 5 | Root pytest raises ImportPathMismatchError | Plugin tests collected from repo root instead of per plugin | conftest.py, pyproject norecursedirs | | 6 | slop-check fails on a PR | Slop score over 3.0 in a docs/ or book/src/ markdown file | slop-check.yml | | 7 | Stop hook produces no verdict at all | Inner subprocess timeout >= registered hook budget | 268cff89 | | 8 | CI broken on a GitHub action or tool pin | Stale or nonexistent pinned version | f81d89a5, 25bf5a9d | | 9 | Scanner reports nothing on input you know is bad | Swallowed exception (except-and-continue) drops files silently | 666171c3, b6de71cf | | 10 | skrills: not found | Missing optional binary (a Python fallback exists) | Makefile validate-skills |
First command (substitute the hook path from the error message):
bashecho '{}' | python3 plugins/gauntlet/hooks/precommit_gate.py; echo "exit=$?"
What the result means: a traceback names the module whose import chain pulls in a package the host interpreter lacks. Exit 0 with no output means the hook is import-safe and the problem is elsewhere (check the hook registration in hooks/hooks.json).
Fix: guard the import at module level or defer it into the function that needs it. The gauntlet incident: precommit_gate.py imported gauntlet.knowledge_store, whose __init__.py eagerly imported modules doing bare import yaml and import anthropic. Guarded in 45dd77ef (#518), deferred in 9bfc0a7a. Add a regression test that blocks the package via a sys.meta_path blocker and re-imports the hook (pattern in plugins/gauntlet/tests/unit/test_challenges.py).
The repo is Python 3.12, but hook scripts and their transitive imports must stay importable under Python 3.9 (.github/workflows/ python39-compat.yml). First command:
bashuv run ruff check --select UP007 --target-version py39 plugins/<plugin>/hooks/ rg -n 'datetime\.UTC|from datetime import UTC' plugins/<plugin>/
What the result means: UP007 hits are bare X | Y union annotations that raise TypeError at import time on 3.9. The rg hits are the datetime.UTC alias (3.11+), which UP007 does not catch. Either one in a hook import chain breaks every hook at once: on PR #511 a single datetime.UTC in leyline.quota_tracker produced three cascade failures (18c9340d).
Fix: use from datetime import timezone with timezone.utc, and typing.Union/Optional or a from __future__ import annotations line for unions. To mirror CI's Gate 2 locally (verified 2026-07-02):
bashuv venv --python 3.9 /tmp/hook39 VIRTUAL_ENV=/tmp/hook39 uv pip install pytest pyyaml cd plugins/abstract /tmp/hook39/bin/python -m pytest tests/hooks --override-ini="addopts="
The addopts override strips per-plugin coverage flags that need packages the bare venv lacks. See also the linter trap below: ruff will fight this fix.
First command:
bashrg -l 'CLAUDE_TOOL_' plugins/*/hooks/ rg -ln 'read_hook_payload' plugins/*/hooks/
What the result means: Claude Code never sets CLAUDE_TOOL_* environment variables. The payload arrives as JSON on stdin. A hook reading only env vars is a silent no-op: it exits 0, CI is green, and nothing downstream ever happens. This starved the [Learning] discussion digests for two months (last digest 2026-04-25) before anyone noticed (CHANGELOG 1.9.14).
Fix: read stdin first via the canonical reader plugins/abstract/hooks/shared/hook_io.py (read_hook_payload, stdin-first with env-var fallback for the test harness). Then verify the hook actually fires: pipe a realistic payload in and check for the side effect rather than the exit code alone.
First command:
bashbash scripts/capabilities-sync-check.sh
What the result means: the script diffs every plugin's .claude-plugin/plugin.json registrations against book/src/reference/capabilities-reference.md and prints the drifted entries. PASSED: All capabilities are in sync means the CI failure was against an older commit. Rebase and rerun.
Fix: run the sanctum sync command with the fix flag:
/sanctum:sync-capabilities --fixFirst command:
bashrg -n 'norecursedirs' pyproject.toml
What the result means: root pytest excludes plugins/* on purpose. Plugins carry duplicate test module names and conftest fixtures, and collecting them from the root collides (documented in conftest.py). If you see this error you ran pytest across plugin boundaries.
Fix: run tests per plugin, never from the root against plugins:
bashcd plugins/imbue && uv run pytest tests/unit/test_deferred_capture.py -x -q make sanctum-test # delegation target, any plugin name works ./scripts/run-plugin-tests.sh --all
First command: read the PR comment the workflow posts (it lists the failing files and scores), then reproduce locally. The score is tier-1 hits x3 plus tier-2 hits x2 plus em dashes, per 100 words, threshold 3.0. Copy the TIER1 and TIER2 regexes from .github/workflows/slop-check.yml rather than retyping the word lists, then:
bashgrep -o '—' docs/<file>.md | wc -l grep -oiE "$TIER1" docs/<file>.md | wc -l
Fix: rewrite per .claude/rules/slop-scan-for-docs.md. Replace em dashes with colons or periods, and replace the flagged vocabulary with plain words. Never de-slop historical CHANGELOG entries.
First command:
bashrg -n '"timeout"' plugins/herald/hooks/hooks.json rg -n 'TIMEOUT' plugins/herald/hooks/double_shot_latte.py
What the result means: if any subprocess or LLM-call timeout inside the hook is greater than or equal to the registered hook budget, the harness kills the whole hook before it can print a decision, and the hook dies without emitting anything. Herald shipped LLM_TIMEOUT_SECONDS = 30 inside a 10-second registered budget (fixed in 268cff89: capped to 8 with startup margin, and the LLM second shot gated to the single ambiguous outcome).
Fix: cap every inner timeout strictly below the registered budget and pin the invariant with a guard test, as in plugins/herald/tests/unit/test_double_shot_latte.py:: test_llm_timeout_fits_within_hook_timeout. Deterministic tests do not exercise optional LLM branches, so the timeout relation must be asserted directly.
First command:
bashpython3 scripts/check_pinned_versions.py
What the result means: the script checks GitHub-sourced pins (CI actions in .github/workflows/*, external rev: hooks in .pre-commit-config.yaml) against upstream and prints stale or held pins with reasons. Two settled incidents: setup-uv@v8 failed because the bare v8 tag does not exist upstream (pinned to v8.2.0, f81d89a5), and bandit 1.9+ dropped Python 3.9 support (held at 1.8.6, 25bf5a9d).
Fix: pin to a full existing tag, and when holding a version back, record the reason where the checker reports it so the hold is visible.
First command: feed the scanner one deliberately malformed file and watch for an ADVISORY finding. Silence is the bug. Then look for the swallow:
bashrg -n 'except' scripts/check_hook_modernization.py
What the result means: an except-and-continue block in a scanner loop drops unparseable files without a trace, so the worst inputs are exactly the ones never reported. Incidents B1-B4 (666171c3, #575, and b6de71cf) covered hook-modernization scanning, strict-mode file drops, and DORA metrics rating malformed tags as Elite.
Fix: Constitution rule 10. Errors are not optional: emit an advisory finding for each skipped file or propagate. Never catch-and-continue without output.
First command:
bashmake validate-skills
What the result means: skrills not available, using Python fallback followed by scripts/check_plugin_hooks.py output is normal operation rather than an error. make analyze-skills falls back to scripts/generate_dependency_map.py. Only build the binary if you need the Rust path:
bashmake skrills-build # needs cargo and the skrills repo at $HOME/skrills # (override with SKRILLS_REPO=/path)
Ruff's pyupgrade rule UP017 auto-rewrites timezone.utc back to the 3.11-only datetime.UTC, silently reverting the py39 fix on the next make lint. This recurred at least three times (18c9340d, b0049fde, 709dafc9) before the durable defense landed: UP017 in root pyproject.toml extend-ignore, per-line suppression comments with a stated reason where needed (Constitution rule 6 requires the reason), and above all an AST-scanning invariant test (plugins/leyline/tests/test_python39_compat.py) that fails CI on any reintroduction. Lesson: when an autofixer keeps reverting your fix, re-applying is attempt N of an infinite loop. Encode the invariant as a test that scans the source.
A green check proves only that the gate's own spec was satisfied. A gate can be quietly configured to check nothing and stay green. The global mirrors-mypy pre-commit hook silently disabled 13 error codes, and typecheck ran --changed instead of --all (fixed in 1.9.12: neutered hook removed, run-plugin-typecheck --all, typecheck.yml gating every PR). Discriminating test for any suspicious gate: introduce one known violation and confirm the gate goes red. If it stays green, the bug is inside the gate itself.
Plugins execute from Claude Code's cache directory rather than the repo checkout, so a CWD-relative path in a hook resolves to nothing. The conserve session-start hook broke exactly this way and was fixed by inlining its JSON utilities (CHANGELOG 1.4.1; the inlined-copy sync notice lives in scripts/shared/json_utils.sh). Rule: hooks resolve paths from ${CLAUDE_PLUGIN_ROOT} or relative to their own script file, never from the working directory.
After two consecutive failed attempts of the same shape (same file, same error class, same tool), do not try a third variation. Switch to a read-only diagnostic: the discriminating command from the matching row above, verbose test output, or printing the actual state. Then report four things: what you believed, what the evidence now says, what you would try differently, and an explicit ask. This discipline comes from the global CLAUDE.md and is wired into plugins/sanctum/commands/fixit.md. It exists to kill the "47 edits to make a test pass" loop.
night-market-operations.
night-market-build-and-env.
the 2026-03-28 unbloat cascade): use night-market-failure-archaeology.
claude-code-plugin-reference.
night-market-change-control.
identified as a new failure mode not in this table.
any fix was attempted.
is stated with evidence.
python3 with astdin JSON payload and the side effect (beyond exit 0) was observed.
invariant test now fails if the bug is reintroduced.
diagnostic and a four-part report was produced.
Compiled 2026-07-02 against repo v1.9.15, branch discussions-fix-1.9.14. All commit hashes verified with git log --oneline -1 <hash>. Facts most likely to drift, with re-verification one-liners:
rg -n '"timeout"' plugins/herald/hooks/hooks.json and rg -n 'LLM_TIMEOUT_SECONDS' plugins/herald/hooks/double_shot_latte.py (10 and 8 as of 2026-07-02).
rg -n 'THRESHOLD|TIER1|TIER2' .github/workflows/slop-check.yml (3.0 as of 2026-07-02).
rg -n 'UP007|--python 3.9' .github/workflows/python39-compat.yml.
rg -n 'UP017' pyproject.toml.rg -n 'def read_hook_payload' plugins/abstract/hooks/shared/hook_io.py.
ls scripts/check_pinned_versions.py scripts/capabilities-sync-check.sh.
rg -n -A 8 'validate-skills:' Makefile.rg -n 'norecursedirs' pyproject.toml.gh pr view 511 and the 18c9340d commit body.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 20,028 | 13,486 | -33% | 1 | 1 | 0% | 3,375 | 6,919 | +105% | 0 | 0 | — |
case-02 | fail→pass | 18,633 | 11,598 | -38% | 1 | 1 | 0% | 3,092 | 6,349 | +105% | 0 | 0 | — |
case-03 | fail→pass | 13,993 | 6,469 | -54% | 1 | 1 | 0% | 2,336 | 5,545 | +137% | 0 | 0 | — |
case-04 | fail→pass | 18,126 | 10,397 | -43% | 1 | 1 | 0% | 3,133 | 6,106 | +95% | 0 | 0 | — |
case-05 | fail→pass | 35,963 | 8,000 | -78% | 1 | 1 | 0% | 3,199 | 5,831 | +82% | 0 | 0 | — |
case-06 | fail→pass | 10,873 | 4,240 | -61% | 1 | 1 | 0% | 1,761 | 5,079 | +188% | 0 | 0 | — |
case-07 | fail→pass | 14,877 | 7,493 | -50% | 1 | 1 | 0% | 2,295 | 5,673 | +147% | 0 | 0 | — |
case-08 | fail→pass | 12,646 | 8,109 | -36% | 1 | 1 | 0% | 2,161 | 5,636 | +161% | 0 | 0 | — |
case-09 | pass→pass | 15,933 | 8,963 | -44% | 1 | 1 | 0% | 2,713 | 5,738 | +112% | 0 | 0 | — |
case-10 | fail→pass | 14,983 | 9,200 | -39% | 1 | 1 | 0% | 2,323 | 5,722 | +146% | 0 | 0 | — |
case-11 | fail→pass | 10,042 | 3,285 | -67% | 1 | 1 | 0% | 1,717 | 4,918 | +186% | 0 | 0 | — |
case-12 | fail→pass | 11,480 | 6,669 | -42% | 1 | 1 | 0% | 2,056 | 5,563 | +171% | 0 | 0 | — |
case-13 | pass→pass | 16,344 | 7,103 | -57% | 1 | 1 | 0% | 2,370 | 5,431 | +129% | 0 | 0 | — |
case-14 | fail→pass | 11,747 | 6,189 | -47% | 1 | 1 | 0% | 2,088 | 5,371 | +157% | 0 | 0 | — |
case-15 | fail→pass | 8,662 | 3,967 | -54% | 1 | 1 | 0% | 1,201 | 4,941 | +311% | 0 | 0 | — |
case-16 | fail→pass | 12,077 | 8,170 | -32% | 1 | 1 | 0% | 1,946 | 5,731 | +195% | 0 | 0 | — |
case-17 | fail→pass | 7,567 | 4,678 | -38% | 1 | 1 | 0% | 1,233 | 5,067 | +311% | 0 | 0 | — |
case-18 | fail→pass | 15,134 | 6,939 | -54% | 1 | 1 | 0% | 2,335 | 5,541 | +137% | 0 | 0 | — |
case-19 | pass→pass | 8,494 | 5,560 | -35% | 1 | 1 | 0% | 1,288 | 5,225 | +306% | 0 | 0 | — |
case-20 | pass→pass | 10,283 | 8,184 | -20% | 1 | 1 | 0% | 1,740 | 5,743 | +230% | 0 | 0 | — |
case-21 | pass→pass | 12,845 | 10,768 | -16% | 1 | 1 | 0% | 2,308 | 6,206 | +169% | 0 | 0 | — |
case-22 | fail→pass | 8,539 | 9,420 | +10% | 1 | 1 | 0% | 1,124 | 5,731 | +410% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +77 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.