Night Market Failure Archaeology
This is a reference document, not a runbook. It records every major
investigation, dead end, rejected fix, and revert in this repo as
symptom, root cause, evidence, status, and lesson, so nobody re-fights
a settled battle. For triage of a failure happening right now, use
night-market-debugging-playbook. Come here when a proposed change
smells like something the repo already tried and rejected.
Jargon used below:
- "py39 hooks": hook scripts run under the host system Python (3.9
floor), while the rest of the repo requires Python 3.12. Enforced by
.github/workflows/python39-compat.yml.
- "unbloat": the conserve plugin's dead-code deletion campaign.
- "Stop hook": a Claude Code hook that runs when a session turn ends.
- "settled": the fight is over. Re-opening it requires new evidence
and a note in the collective memory, not a hunch.
How to use this chronicle
- Before implementing, scan the battle titles and the recurring-class
table for anything resembling your plan.
- If a battle matches, read its lesson. Follow it, or write down why
this time is different before overriding it.
- Verify any hash you rely on:
git log --format='%h %s' -1 <hash>.
- When your own battle settles, add an entry (format at the bottom).
Settled battles
SB1: tasks_manager cross-plugin DRY consolidation (reverted)
- Symptom: three near-identical
tasks_manager.py copies in attune,
sanctum, and spec-kit looked like textbook duplication.
- Root cause of the failure: consolidating them into a root
scripts/tasks_manager.py (1,169 lines deleted across 6 files)
broke plugin self-containment. Plugins are independent deployables
that run from a Claude Code cache directory, so they cannot import
from the repo root.
- Evidence:
054e2679 (consolidate, 2026-01-23), 29961cd2 (revert,
same day), d89a55c7 (differentiated per-plugin copies). All three
copies exist today under plugins/<p>/scripts/tasks_manager.py.
docs/dependency-audit.md tracks cross-plugin deps and fallbacks.
- Status: settled. Per-plugin copies are intentional.
- Lesson: DRY across plugin boundaries is an anti-pattern here. Do not
"helpfully" re-consolidate duplicated plugin scripts.
SB2: LSP proxy (speculative infrastructure, killed)
- Symptom:
scripts/lsp-proxy.py added for "graceful server fallback"
with no consumer demanding it.
- Root cause: speculative abstraction built ahead of need.
- Evidence:
2fcb256d (add), killed in PR #193 review, bc318947
(revert, 2026-02-11, restores .cclsp.json).
- Status: settled. Do not re-add without a concrete consumer.
- Lesson: infrastructure without a present consumer dies in review.
See also
.claude/rules/shared-utility-consumer-rule.md.
SB3: Tier-3 unbloat over-deletion (2026-03-28, the big one)
- Symptom: unbloat deleted 182 files (66K lines) using only the Python
import graph to decide what was dead.
- Root cause: skills and commands reference scripts from markdown, not
Python imports. 28 actively referenced scripts plus 18 companion
test files were classified dead and deleted.
- Evidence:
a3f11323 (the deletion), 3f280334 (restore the 28
scripts and 18 tests), b5f08bf7 (CI repair: dead docs workflow,
missing capabilities entries), d6c128f5 and 01a13e70 (further
repair and governance follow-ups). Recovery point was branch
backup/unbloat-20260328.
- Status: settled, with the highest single blast radius on record.
- Lesson: any deletion campaign must scan markdown references
(skills, commands, agents), never imports alone. Always take a
backup/unbloat-<date> branch first.
SB4: bulk lint ignores (reverted)
- Symptom: global ruff ignores made parseltongue lint pass while
hiding 73 real violations.
- Root cause: silencing the gate instead of fixing the findings.
- Evidence:
06b9b1db (2026-03-10) reverts the bulk ignores and fixes
all 73 at source. The commit body cites issue #296 as the
anti-bulk-ignore policy anchor.
- Status: settled. Constitution rule 6 also bans bare suppressions.
- Lesson: a suppressed violation is a hidden defect. Fix at source or
suppress per line with a stated reason.
SB5: datetime.UTC breaking py39 hooks (fixed 3+ times)
- Symptom: hook import chains crash under host Python 3.9 because
datetime.UTC is a 3.11+ alias. On PR #511 this was the sole
failing check, with three cascade failures rooted in one
ImportError.
- Root cause: ruff's pyupgrade rule UP017 kept auto-rewriting
timezone.utc back to datetime.UTC, silently re-breaking the fix.
The linter was fighting the fix.
- Evidence:
18c9340d (leyline quota_tracker, 2026-05-07),
b0049fde (abstract), 709dafc9 (attune).
- Status: settled by a durable guard, not by the fixes themselves:
an AST-scanning test (
plugins/leyline/tests/test_python39_compat.py)
fails CI on any reintroduction, root ruff config extend-ignores
UP017, and the surviving per-line pyupgrade suppression markers each
carry a stated reason.
- Lesson: when the linter fights the fix, only an AST invariant test
holds the line. A fix commit alone will be reverted by tooling.
SB6: unguarded yaml/anthropic imports in hook chains
- Symptom: every
git commit emitted a PreToolUse hook
ModuleNotFoundError.
- Root cause: gauntlet's
__init__ eagerly imported dependencies
(yaml, anthropic) that the bare host interpreter running hooks does
not have.
- Evidence:
45dd77ef (guard yaml import, #518, 2026-05-08),
9bfc0a7a (defer anthropic import to function scope). Regression
tests block re-imports with a sys.meta_path blocker
(plugins/gauntlet/tests/unit/test_challenges.py).
- Status: settled.
- Lesson: hook entrypoints and everything they transitively import
must be import-safe under a bare interpreter with no third-party
packages.
SB7: herald LLM timeout exceeding the hook budget
- Symptom: the herald double-shot-latte Stop-hook judge produced no
verdict at all in the failure case.
- Root cause:
LLM_TIMEOUT_SECONDS = 30 inside a hook registered with
a 10-second budget. The harness killed the whole hook before the LLM
subprocess returned, so even the deterministic path's verdict was
lost.
- Evidence:
3d22f02a (the judge), 268cff89 (2026-05-30: cap LLM
timeout to 8s, gate the LLM second shot to the single ambiguous
outcome). A guard test asserts LLM_TIMEOUT_SECONDS is below the
registered hook timeout
(plugins/herald/tests/unit/test_double_shot_latte.py).
- Status: settled.
- Lesson: assert every subprocess timeout against the registered hook
budget in a test. Deterministic tests do not exercise optional
branches, so the optional branch needs its own guard.
SB8: silent scanner failures B1-B4
- Symptom: quality scanners silently dropped files on malformed input,
so broken inputs produced clean-looking reports. One metric reported
Elite on malformed tags.
- Root cause: except-and-continue blocks in scanner loops swallowed
parse errors.
- Evidence:
666171c3 (2026-06-18, fixes #575, findings B1-B4 from
PR #521), b6de71cf (plugin-check: bound hangs, unmask failures).
- Status: settled. Scanners now emit ADVISORY findings instead of
skipping silently.
- Lesson: except-continue in a scanner is a landmine. A scanner that
cannot parse an input must say so in its output.
SB9: hooks reading env vars Claude Code never sets
- Symptom: the
[Learning] discussion digest went quiet after
2026-04-25. That starvation was the only visible symptom.
- Root cause: PreToolUse/PostToolUse hooks (
skill_execution_logger,
pre_skill_execution, homeostatic_monitor in abstract, leyline
noqa_guard, sanctum deferred_item_watcher) read CLAUDE_TOOL_*
environment variables that Claude Code never populates. The payload
arrives as JSON on stdin. The hooks exited 0 without doing anything,
for months.
- Evidence: CHANGELOG 1.9.14 (2026-06-27). Fix: canonical stdin-first
reader
read_hook_payload in
plugins/abstract/hooks/shared/hook_io.py, with env-var fallback
for the test harness. Consumers: the five hooks above.
- Status: settled.
- Lesson: a hook that exits 0 is not a hook that worked. Verify hooks
actually fire and consume the real payload contract (stdin JSON).
SB10: neutered mypy gate
- Symptom: typecheck was green while real type errors accumulated.
- Root cause: the global mirrors-mypy pre-commit hook silently
disabled 13 error codes, and the typecheck hook ran
--changed
instead of --all.
- Evidence: CHANGELOG 1.9.12 (2026-06-18): neutered hook removed,
per-plugin strict mypy with
--all, new typecheck.yml workflow
gating every PR.
- Status: settled.
- Lesson: a green gate that disables checks is worse than no gate.
Audit what a gate actually runs, not whether it passes.
SB11: CI pin breakage (recurring)
- Symptom: CI failures from tool pins that were never valid or aged
out.
- Root cause:
setup-uv@v8 bare tag does not exist upstream, and
bandit newer than 1.8.6 dropped Python 3.9 support needed by hooks.
- Evidence:
f81d89a5 (pin setup-uv to v8.2.0), 25bf5a9d (pin
bandit 1.8.6).
- Status: settled by standing guards
scripts/check_pinned_versions.py
and scripts/check_ruff_version.py (pre-commit, run only when the
relevant files are staged).
- Lesson: pin exact tags and let the guard scripts flag drift. Never
trust a major-version alias tag to exist.
SB12: gh discussion CLI does not exist
- Symptom: playbooks called
gh discussion create/comment/list and
failed. There is no such gh subcommand.
- Root cause: hallucinated CLI surface. GitHub Discussions are
reachable only through the GraphQL API.
- Evidence: CHANGELOG (minister playbook repair replaced the calls
with GraphQL mutations). Working templates live in
plugins/leyline/skills/git-platform/modules/command-mapping.md
using gh api graphql.
- Status: settled.
- Lesson: Discussions equal GraphQL only. Any script touching
Discussions goes through
gh api graphql, never a gh discussion
subcommand.
SB13: pensive break halted multi-hit overflow detection
- Symptom: pensive's integer-overflow scan reported at most one hit
per file (PEN-013).
- Root cause: a loop
break after the first finding.
- Evidence:
4993273b (2026-06-22).
- Status: settled.
- Lesson: in detection loops, collect all findings. An early exit in a
scanner is a correctness bug, not an optimization.
SB14: broken relative paths from the plugin cache directory
- Symptom: conserve's session-start hook failed when the plugin ran
from the Claude Code cache directory instead of the repo checkout.
- Root cause: a relative path assumed the repo layout. Installed
plugins execute from a cache dir where sibling repo paths do not
exist.
- Evidence: CHANGELOG ("Inlined JSON utilities in conserve
session-start hook"), later generalized in
a8c9e4e9 (vendor
json_utils.sh per plugin).
- Status: settled.
- Lesson: plugins run from a cache dir. No relative paths out of the
plugin root, ever. Vendor small utilities per plugin (see SB1: this
duplication is intentional).
SB15: imbue body-text fallback over-classification
- Symptom: imbue classified unrelated skills as review-workflow
because a fallback matched loose body text when frontmatter did not
answer.
- Root cause: a defensive fallback standing in for data that should be
authoritative. The fallback masked the real signal instead of
failing loudly.
- Evidence:
69fbd32c (2026-06-24, remove the fallback). Pattern
codified in .claude/rules/prefer-invariants-over-fallbacks.md
(9f771794) and the imbue verifier-integrity module (29081fda).
- Status: settled.
- Lesson: a fallback that guesses is worse than a loud miss. Prefer
invariants over fallbacks.
Recurring failure classes (ranked)
Ranked by observed cost across the repo's history. Hashes are sample
evidence, not complete lists.
| Rank |
Class |
Shape |
Sample evidence |
Standing guard |
| RC1 |
Silent failure / swallowed exceptions |
except-continue, exit 0 on failure, dropped files |
e0d1c464, 10b4b790, 52ae1459, fedb1c5b, 0d28a260, bfebb1ad, 7987649c, 89faaaf7 |
Constitution rule 10 and the narrow-bare-except campaign |
| RC2 |
py39 / host-Python import fragility |
hooks crash under system Python |
SB5, SB6 hashes |
python39-compat.yml, AST test, meta_path tests |
| RC3 |
PR-review rework churn |
long fix-chains after review |
tautological tests a94240e2, f1cbbcf1, 30e58586, 42f7ce84; AI-slop prose 2a039760, 62f598e9; stale wiring 204b927c, 332bcb75 |
shift-left pre-commit guards, slop-check CI |
| RC4 |
Unbloat over-deletion |
import-graph-only deletion |
SB3 hashes |
markdown-reference scan, backup branches |
| RC5 |
CI pin breakage |
invalid or stale tool pins |
SB11 hashes |
check_pinned_versions.py, check_ruff_version.py |
| RC6 |
Version drift |
manifests and SKILL.md frontmatter out of lockstep |
fan-out via plugins/sanctum/scripts/update_versions.py |
sanctum:version-updates flow |
| RC7 |
Fallback over-classification |
guessing fallback masks bad data |
SB15 hashes |
prefer-invariants-over-fallbacks rule |
Dead ends: fenced off
These branches are superseded. Do not resume work on them. They stay
for archaeology only.
| Branch |
Why it is dead |
attune-tasks-poc |
Superseded. Its lineage spawned the reverted SB1 consolidation. |
agent-coordination |
2026-03-17 experiment, superseded by conjure and egregore. |
feature/knowledge-brain |
Pre-1.0 prototype, superseded by memory-palace. |
feat/slop-clean-before-post |
Superseded by scribe Tier-5 patterns on master (509bffaf). |
The backup/unbloat-* branches (a dozen as of 2026-07-02) are
recovery snapshots and the institutional memory of RC4.
backup/unbloat-20260328 was the recovery point for SB3. Do not
delete them casually.
The costliest three (judgment call)
- The PR-review rework loop (RC3). Roughly 140 fix-chain commits
spent repairing review findings after the fact (estimate from the
2026-07 repo audit, exact count unverified). Response: shift
quality left into pre-commit guards so review stops being the
first gate.
- The 2026-03-28 unbloat cascade (SB3). Highest single blast radius:
one commit, 182 files, multi-commit recovery, CI repair.
- py39 hook fragility (SB5, SB6). Each occurrence was a total hook
outage, and the linter actively fought the fix until an AST
invariant test ended it.
Adding an entry when a new battle settles
What counts as a settled battle:
- a revert of a merged change (with the reason understood)
- a bug fixed more than once before a durable guard landed
- a feature or abstraction killed in review or post-merge
- an incident with a multi-commit repair chain
Format: copy the SB block shape. Five fields, all required: Symptom,
Root cause, Evidence (commit hashes, each verified with
git log --format='%h %s' -1 <hash> before you write it down),
Status, Lesson. Assign the next SB number. Add the class to the RC
table if it recurs.
Where the lesson must also land (this file is not the system of
record for policy):
- GitHub Discussions
[Learning] or [PR Finding] post, and the
decision journal (docs/tradeoffs.md, docs/lessons-learned.md,
append-only). See night-market-collective-memory.
- If the lesson is a durable behavioral policy, graduate it to
.claude/rules/ through the pipeline in
night-market-research-methodology, subject to
night-market-change-control.
When NOT to use
- A failure is happening right now and you need triage steps: use
night-market-debugging-playbook.
- You need the release/test/lint command anatomy behind a fix: use
night-market-operations.
- You are deciding how to classify or gate a change: use
night-market-change-control.
- You want the design invariants these battles produced (plugin
self-containment, hook import-safety): use
night-market-architecture-contract.
- You are recording a decision or lesson into Discussions or the
decision journal: use
night-market-collective-memory.
Exit Criteria
Provenance and maintenance
Compiled 2026-07-02 against repo v1.9.15, branch master history
(1,247 commits). Volatile facts: branch list, RC ranking, and the
costliest-three judgment reflect the repo as of that date. The ~140
fix-chain commit count for RC3 is an unverified estimate.
Re-verification commands:
# All cited hashes still resolve
for h in 054e2679 29961cd2 d89a55c7 2fcb256d bc318947 a3f11323 \
3f280334 b5f08bf7 d6c128f5 01a13e70 06b9b1db 18c9340d b0049fde \
709dafc9 45dd77ef 9bfc0a7a 3d22f02a 268cff89 666171c3 b6de71cf \
f81d89a5 25bf5a9d 4993273b a8c9e4e9 69fbd32c 9f771794 29081fda \
509bffaf e0d1c464 10b4b790 52ae1459 fedb1c5b 0d28a260 bfebb1ad \
7987649c 89faaaf7 a94240e2 f1cbbcf1 30e58586 42f7ce84 2a039760 \
62f598e9 204b927c 332bcb75; do
git log --format=%h -1 "$h" >/dev/null 2>&1 || echo "MISSING $h"
done
# Dead-end and backup branches still present
git branch --list 'backup/unbloat-*' attune-tasks-poc \
agent-coordination 'feature/knowledge-brain' \
'feat/slop-clean-before-post'
# Standing guards still in place
ls plugins/leyline/tests/test_python39_compat.py \
plugins/abstract/hooks/shared/hook_io.py \
scripts/check_pinned_versions.py scripts/check_ruff_version.py
rg -q "meta_path" plugins/gauntlet/tests/unit/test_challenges.py
rg -q "LLM_TIMEOUT_SECONDS < hook_timeout" \
plugins/herald/tests/unit/test_double_shot_latte.py
Source: athola/claude-night-market → .claude/skills/night-market-failure-archaeology/SKILL.md
1---2name: night-market-failure-archaeology3description: Chronicles settled battles, reverts, and dead ends. Use when a fix echoes a past failure. Do not use for live triage; use night-market-debugging-playbook.4---5
6
7# Night Market Failure Archaeology
8
9This is a reference document, not a runbook. It records every major
10investigation, dead end, rejected fix, and revert in this repo as
11symptom, root cause, evidence, status, and lesson, so nobody re-fights
12a settled battle. For triage of a failure happening right now, use
13`night-market-debugging-playbook`. Come here when a proposed change
14smells like something the repo already tried and rejected.
15
16Jargon used below:
17
18- "py39 hooks": hook scripts run under the host system Python (3.9
19 floor), while the rest of the repo requires Python 3.12. Enforced by
20 `.github/workflows/python39-compat.yml`.
21- "unbloat": the conserve plugin's dead-code deletion campaign.
22- "Stop hook": a Claude Code hook that runs when a session turn ends.
23- "settled": the fight is over. Re-opening it requires new evidence
24 and a note in the collective memory, not a hunch.
25
26## How to use this chronicle
27
281. Before implementing, scan the battle titles and the recurring-class
29 table for anything resembling your plan.
302. If a battle matches, read its lesson. Follow it, or write down why
31 this time is different before overriding it.
323. Verify any hash you rely on: `git log --format='%h %s' -1 <hash>`.
334. When your own battle settles, add an entry (format at the bottom).
34
35## Settled battles
36
37### SB1: tasks_manager cross-plugin DRY consolidation (reverted)
38
39- Symptom: three near-identical `tasks_manager.py` copies in attune,
40 sanctum, and spec-kit looked like textbook duplication.
41- Root cause of the failure: consolidating them into a root
42 `scripts/tasks_manager.py` (1,169 lines deleted across 6 files)
43 broke plugin self-containment. Plugins are independent deployables
44 that run from a Claude Code cache directory, so they cannot import
45 from the repo root.
46- Evidence: `054e2679` (consolidate, 2026-01-23), `29961cd2` (revert,
47 same day), `d89a55c7` (differentiated per-plugin copies). All three
48 copies exist today under `plugins/<p>/scripts/tasks_manager.py`.
49 `docs/dependency-audit.md` tracks cross-plugin deps and fallbacks.
50- Status: settled. Per-plugin copies are intentional.
51- Lesson: DRY across plugin boundaries is an anti-pattern here. Do not
52 "helpfully" re-consolidate duplicated plugin scripts.
53
54### SB2: LSP proxy (speculative infrastructure, killed)
55
56- Symptom: `scripts/lsp-proxy.py` added for "graceful server fallback"
57 with no consumer demanding it.
58- Root cause: speculative abstraction built ahead of need.
59- Evidence: `2fcb256d` (add), killed in PR #193 review, `bc318947`
60 (revert, 2026-02-11, restores `.cclsp.json`).
61- Status: settled. Do not re-add without a concrete consumer.
62- Lesson: infrastructure without a present consumer dies in review.
63 See also `.claude/rules/shared-utility-consumer-rule.md`.
64
65### SB3: Tier-3 unbloat over-deletion (2026-03-28, the big one)
66
67- Symptom: unbloat deleted 182 files (66K lines) using only the Python
68 import graph to decide what was dead.
69- Root cause: skills and commands reference scripts from markdown, not
70 Python imports. 28 actively referenced scripts plus 18 companion
71 test files were classified dead and deleted.
72- Evidence: `a3f11323` (the deletion), `3f280334` (restore the 28
73 scripts and 18 tests), `b5f08bf7` (CI repair: dead docs workflow,
74 missing capabilities entries), `d6c128f5` and `01a13e70` (further
75 repair and governance follow-ups). Recovery point was branch
76 `backup/unbloat-20260328`.
77- Status: settled, with the highest single blast radius on record.
78- Lesson: any deletion campaign must scan markdown references
79 (skills, commands, agents), never imports alone. Always take a
80 `backup/unbloat-<date>` branch first.
81
82### SB4: bulk lint ignores (reverted)
83
84- Symptom: global ruff ignores made parseltongue lint pass while
85 hiding 73 real violations.
86- Root cause: silencing the gate instead of fixing the findings.
87- Evidence: `06b9b1db` (2026-03-10) reverts the bulk ignores and fixes
88 all 73 at source. The commit body cites issue #296 as the
89 anti-bulk-ignore policy anchor.
90- Status: settled. Constitution rule 6 also bans bare suppressions.
91- Lesson: a suppressed violation is a hidden defect. Fix at source or
92 suppress per line with a stated reason.
93
94### SB5: datetime.UTC breaking py39 hooks (fixed 3+ times)
95
96- Symptom: hook import chains crash under host Python 3.9 because
97 `datetime.UTC` is a 3.11+ alias. On PR #511 this was the sole
98 failing check, with three cascade failures rooted in one
99 ImportError.
100- Root cause: ruff's pyupgrade rule UP017 kept auto-rewriting
101 `timezone.utc` back to `datetime.UTC`, silently re-breaking the fix.
102 The linter was fighting the fix.
103- Evidence: `18c9340d` (leyline quota_tracker, 2026-05-07),
104 `b0049fde` (abstract), `709dafc9` (attune).
105- Status: settled by a durable guard, not by the fixes themselves:
106 an AST-scanning test (`plugins/leyline/tests/test_python39_compat.py`)
107 fails CI on any reintroduction, root ruff config extend-ignores
108 UP017, and the surviving per-line pyupgrade suppression markers each
109 carry a stated reason.
110- Lesson: when the linter fights the fix, only an AST invariant test
111 holds the line. A fix commit alone will be reverted by tooling.
112
113### SB6: unguarded yaml/anthropic imports in hook chains
114
115- Symptom: every `git commit` emitted a PreToolUse hook
116 ModuleNotFoundError.
117- Root cause: gauntlet's `__init__` eagerly imported dependencies
118 (yaml, anthropic) that the bare host interpreter running hooks does
119 not have.
120- Evidence: `45dd77ef` (guard yaml import, #518, 2026-05-08),
121 `9bfc0a7a` (defer anthropic import to function scope). Regression
122 tests block re-imports with a `sys.meta_path` blocker
123 (`plugins/gauntlet/tests/unit/test_challenges.py`).
124- Status: settled.
125- Lesson: hook entrypoints and everything they transitively import
126 must be import-safe under a bare interpreter with no third-party
127 packages.
128
129### SB7: herald LLM timeout exceeding the hook budget
130
131- Symptom: the herald double-shot-latte Stop-hook judge produced no
132 verdict at all in the failure case.
133- Root cause: `LLM_TIMEOUT_SECONDS = 30` inside a hook registered with
134 a 10-second budget. The harness killed the whole hook before the LLM
135 subprocess returned, so even the deterministic path's verdict was
136 lost.
137- Evidence: `3d22f02a` (the judge), `268cff89` (2026-05-30: cap LLM
138 timeout to 8s, gate the LLM second shot to the single ambiguous
139 outcome). A guard test asserts `LLM_TIMEOUT_SECONDS` is below the
140 registered hook timeout
141 (`plugins/herald/tests/unit/test_double_shot_latte.py`).
142- Status: settled.
143- Lesson: assert every subprocess timeout against the registered hook
144 budget in a test. Deterministic tests do not exercise optional
145 branches, so the optional branch needs its own guard.
146
147### SB8: silent scanner failures B1-B4
148
149- Symptom: quality scanners silently dropped files on malformed input,
150 so broken inputs produced clean-looking reports. One metric reported
151 Elite on malformed tags.
152- Root cause: except-and-continue blocks in scanner loops swallowed
153 parse errors.
154- Evidence: `666171c3` (2026-06-18, fixes #575, findings B1-B4 from
155 PR #521), `b6de71cf` (plugin-check: bound hangs, unmask failures).
156- Status: settled. Scanners now emit ADVISORY findings instead of
157 skipping silently.
158- Lesson: except-continue in a scanner is a landmine. A scanner that
159 cannot parse an input must say so in its output.
160
161### SB9: hooks reading env vars Claude Code never sets
162
163- Symptom: the `[Learning]` discussion digest went quiet after
164 2026-04-25. That starvation was the only visible symptom.
165- Root cause: PreToolUse/PostToolUse hooks (`skill_execution_logger`,
166 `pre_skill_execution`, `homeostatic_monitor` in abstract, leyline
167 `noqa_guard`, sanctum `deferred_item_watcher`) read `CLAUDE_TOOL_*`
168 environment variables that Claude Code never populates. The payload
169 arrives as JSON on stdin. The hooks exited 0 without doing anything,
170 for months.
171- Evidence: CHANGELOG 1.9.14 (2026-06-27). Fix: canonical stdin-first
172 reader `read_hook_payload` in
173 `plugins/abstract/hooks/shared/hook_io.py`, with env-var fallback
174 for the test harness. Consumers: the five hooks above.
175- Status: settled.
176- Lesson: a hook that exits 0 is not a hook that worked. Verify hooks
177 actually fire and consume the real payload contract (stdin JSON).
178
179### SB10: neutered mypy gate
180
181- Symptom: typecheck was green while real type errors accumulated.
182- Root cause: the global mirrors-mypy pre-commit hook silently
183 disabled 13 error codes, and the typecheck hook ran `--changed`
184 instead of `--all`.
185- Evidence: CHANGELOG 1.9.12 (2026-06-18): neutered hook removed,
186 per-plugin strict mypy with `--all`, new `typecheck.yml` workflow
187 gating every PR.
188- Status: settled.
189- Lesson: a green gate that disables checks is worse than no gate.
190 Audit what a gate actually runs, not whether it passes.
191
192### SB11: CI pin breakage (recurring)
193
194- Symptom: CI failures from tool pins that were never valid or aged
195 out.
196- Root cause: `setup-uv@v8` bare tag does not exist upstream, and
197 bandit newer than 1.8.6 dropped Python 3.9 support needed by hooks.
198- Evidence: `f81d89a5` (pin setup-uv to v8.2.0), `25bf5a9d` (pin
199 bandit 1.8.6).
200- Status: settled by standing guards `scripts/check_pinned_versions.py`
201 and `scripts/check_ruff_version.py` (pre-commit, run only when the
202 relevant files are staged).
203- Lesson: pin exact tags and let the guard scripts flag drift. Never
204 trust a major-version alias tag to exist.
205
206### SB12: gh discussion CLI does not exist
207
208- Symptom: playbooks called `gh discussion create/comment/list` and
209 failed. There is no such gh subcommand.
210- Root cause: hallucinated CLI surface. GitHub Discussions are
211 reachable only through the GraphQL API.
212- Evidence: CHANGELOG (minister playbook repair replaced the calls
213 with GraphQL mutations). Working templates live in
214 `plugins/leyline/skills/git-platform/modules/command-mapping.md`
215 using `gh api graphql`.
216- Status: settled.
217- Lesson: Discussions equal GraphQL only. Any script touching
218 Discussions goes through `gh api graphql`, never a `gh discussion`
219 subcommand.
220
221### SB13: pensive break halted multi-hit overflow detection
222
223- Symptom: pensive's integer-overflow scan reported at most one hit
224 per file (PEN-013).
225- Root cause: a loop `break` after the first finding.
226- Evidence: `4993273b` (2026-06-22).
227- Status: settled.
228- Lesson: in detection loops, collect all findings. An early exit in a
229 scanner is a correctness bug, not an optimization.
230
231### SB14: broken relative paths from the plugin cache directory
232
233- Symptom: conserve's session-start hook failed when the plugin ran
234 from the Claude Code cache directory instead of the repo checkout.
235- Root cause: a relative path assumed the repo layout. Installed
236 plugins execute from a cache dir where sibling repo paths do not
237 exist.
238- Evidence: CHANGELOG ("Inlined JSON utilities in conserve
239 session-start hook"), later generalized in `a8c9e4e9` (vendor
240 `json_utils.sh` per plugin).
241- Status: settled.
242- Lesson: plugins run from a cache dir. No relative paths out of the
243 plugin root, ever. Vendor small utilities per plugin (see SB1: this
244 duplication is intentional).
245
246### SB15: imbue body-text fallback over-classification
247
248- Symptom: imbue classified unrelated skills as review-workflow
249 because a fallback matched loose body text when frontmatter did not
250 answer.
251- Root cause: a defensive fallback standing in for data that should be
252 authoritative. The fallback masked the real signal instead of
253 failing loudly.
254- Evidence: `69fbd32c` (2026-06-24, remove the fallback). Pattern
255 codified in `.claude/rules/prefer-invariants-over-fallbacks.md`
256 (`9f771794`) and the imbue verifier-integrity module (`29081fda`).
257- Status: settled.
258- Lesson: a fallback that guesses is worse than a loud miss. Prefer
259 invariants over fallbacks.
260
261## Recurring failure classes (ranked)
262
263Ranked by observed cost across the repo's history. Hashes are sample
264evidence, not complete lists.
265
266| Rank | Class | Shape | Sample evidence | Standing guard |
267|------|-------|-------|-----------------|----------------|
268| RC1 | Silent failure / swallowed exceptions | except-continue, exit 0 on failure, dropped files | `e0d1c464`, `10b4b790`, `52ae1459`, `fedb1c5b`, `0d28a260`, `bfebb1ad`, `7987649c`, `89faaaf7` | Constitution rule 10 and the narrow-bare-except campaign |
269| RC2 | py39 / host-Python import fragility | hooks crash under system Python | SB5, SB6 hashes | `python39-compat.yml`, AST test, meta_path tests |
270| RC3 | PR-review rework churn | long fix-chains after review | tautological tests `a94240e2`, `f1cbbcf1`, `30e58586`, `42f7ce84`; AI-slop prose `2a039760`, `62f598e9`; stale wiring `204b927c`, `332bcb75` | shift-left pre-commit guards, slop-check CI |
271| RC4 | Unbloat over-deletion | import-graph-only deletion | SB3 hashes | markdown-reference scan, backup branches |
272| RC5 | CI pin breakage | invalid or stale tool pins | SB11 hashes | `check_pinned_versions.py`, `check_ruff_version.py` |
273| RC6 | Version drift | manifests and SKILL.md frontmatter out of lockstep | fan-out via `plugins/sanctum/scripts/update_versions.py` | sanctum:version-updates flow |
274| RC7 | Fallback over-classification | guessing fallback masks bad data | SB15 hashes | prefer-invariants-over-fallbacks rule |
275
276## Dead ends: fenced off
277
278These branches are superseded. Do not resume work on them. They stay
279for archaeology only.
280
281| Branch | Why it is dead |
282|--------|----------------|
283| `attune-tasks-poc` | Superseded. Its lineage spawned the reverted SB1 consolidation. |
284| `agent-coordination` | 2026-03-17 experiment, superseded by conjure and egregore. |
285| `feature/knowledge-brain` | Pre-1.0 prototype, superseded by memory-palace. |
286| `feat/slop-clean-before-post` | Superseded by scribe Tier-5 patterns on master (`509bffaf`). |
287
288The `backup/unbloat-*` branches (a dozen as of 2026-07-02) are
289recovery snapshots and the institutional memory of RC4.
290`backup/unbloat-20260328` was the recovery point for SB3. Do not
291delete them casually.
292
293## The costliest three (judgment call)
294
2951. The PR-review rework loop (RC3). Roughly 140 fix-chain commits
296 spent repairing review findings after the fact (estimate from the
297 2026-07 repo audit, exact count unverified). Response: shift
298 quality left into pre-commit guards so review stops being the
299 first gate.
3002. The 2026-03-28 unbloat cascade (SB3). Highest single blast radius:
301 one commit, 182 files, multi-commit recovery, CI repair.
3023. py39 hook fragility (SB5, SB6). Each occurrence was a total hook
303 outage, and the linter actively fought the fix until an AST
304 invariant test ended it.
305
306## Adding an entry when a new battle settles
307
308What counts as a settled battle:
309
310- a revert of a merged change (with the reason understood)
311- a bug fixed more than once before a durable guard landed
312- a feature or abstraction killed in review or post-merge
313- an incident with a multi-commit repair chain
314
315Format: copy the SB block shape. Five fields, all required: Symptom,
316Root cause, Evidence (commit hashes, each verified with
317`git log --format='%h %s' -1 <hash>` before you write it down),
318Status, Lesson. Assign the next SB number. Add the class to the RC
319table if it recurs.
320
321Where the lesson must also land (this file is not the system of
322record for policy):
323
324- GitHub Discussions `[Learning]` or `[PR Finding]` post, and the
325 decision journal (`docs/tradeoffs.md`, `docs/lessons-learned.md`,
326 append-only). See `night-market-collective-memory`.
327- If the lesson is a durable behavioral policy, graduate it to
328 `.claude/rules/` through the pipeline in
329 `night-market-research-methodology`, subject to
330 `night-market-change-control`.
331
332## When NOT to use
333
334- A failure is happening right now and you need triage steps: use
335 `night-market-debugging-playbook`.
336- You need the release/test/lint command anatomy behind a fix: use
337 `night-market-operations`.
338- You are deciding how to classify or gate a change: use
339 `night-market-change-control`.
340- You want the design invariants these battles produced (plugin
341 self-containment, hook import-safety): use
342 `night-market-architecture-contract`.
343- You are recording a decision or lesson into Discussions or the
344 decision journal: use `night-market-collective-memory`.
345
346## Exit Criteria
347
348- [ ] Every commit hash cited in this file resolves: the loop in
349 Provenance below prints no MISSING lines.
350- [ ] For the change at hand, the matching SB/RC entries were read,
351 and any override of a lesson is written down with a rationale.
352- [ ] Any newly added entry has all five fields and at least one
353 verified commit hash.
354- [ ] The new entry's lesson is cross-posted per the collective-memory
355 skill (Discussions post or decision-journal line exists).
356
357## Provenance and maintenance
358
359Compiled 2026-07-02 against repo v1.9.15, branch master history
360(1,247 commits). Volatile facts: branch list, RC ranking, and the
361costliest-three judgment reflect the repo as of that date. The ~140
362fix-chain commit count for RC3 is an unverified estimate.
363
364Re-verification commands:
365
366```bash
367# All cited hashes still resolve
368for h in 054e2679 29961cd2 d89a55c7 2fcb256d bc318947 a3f11323 \
369 3f280334 b5f08bf7 d6c128f5 01a13e70 06b9b1db 18c9340d b0049fde \
370 709dafc9 45dd77ef 9bfc0a7a 3d22f02a 268cff89 666171c3 b6de71cf \
371 f81d89a5 25bf5a9d 4993273b a8c9e4e9 69fbd32c 9f771794 29081fda \
372 509bffaf e0d1c464 10b4b790 52ae1459 fedb1c5b 0d28a260 bfebb1ad \
373 7987649c 89faaaf7 a94240e2 f1cbbcf1 30e58586 42f7ce84 2a039760 \
374 62f598e9 204b927c 332bcb75; do
375 git log --format=%h -1 "$h" >/dev/null 2>&1 || echo "MISSING $h"
376done
377```
378
379```bash
380# Dead-end and backup branches still present
381git branch --list 'backup/unbloat-*' attune-tasks-poc \
382 agent-coordination 'feature/knowledge-brain' \
383 'feat/slop-clean-before-post'
384```
385
386```bash
387# Standing guards still in place
388ls plugins/leyline/tests/test_python39_compat.py \
389 plugins/abstract/hooks/shared/hook_io.py \
390 scripts/check_pinned_versions.py scripts/check_ruff_version.py
391rg -q "meta_path" plugins/gauntlet/tests/unit/test_challenges.py
392rg -q "LLM_TIMEOUT_SECONDS < hook_timeout" \
393 plugins/herald/tests/unit/test_double_shot_latte.py
394```
395
396---
397
398**Source:** [`athola/claude-night-market`](https://github.com/athola/claude-night-market) → `.claude/skills/night-market-failure-archaeology/SKILL.md`