Repository navigation
v2.0.0 -- hook-free execution: delete the hook registrations, replace them with explicit commands and MCP tools - #255
Conversation
docs/ held 160 files in one flat directory, so nothing indicated what any given document was. Standards, pipeline policies, ADRs, runbooks, one-off audit reports, per-release design notes and the GitHub community files all sat side by side at docs/*.md. They are now grouped by kind: standards/ (52), policies/ (46), architecture/ (17), reports/ (20), guides/ (14), releases/ (6), contributing/ (5), alongside the pre-existing api/ and phase-1-architecture/. Classification used an objective signal wherever one existed rather than name-guessing -- a file whose name also appears in ~/.claude/rules/ is a standard, one that appears in ~/.claude/policies/ is a policy -- which covered 98 of the 160. Every relocation went through git mv so history follows the files. docs/README.md is a new index naming what each folder holds and why. Fixed along the way: - load_framework_standards() and load_language_standards() build their path as <repo>/docs/<filename> at runtime, so this change would have silently emptied the framework tier (flask, django, spring-boot) and the entire language tier. Runtime-assembled paths are invisible to a grep for docs/<file>.md, which is how the first reference sweep missed them. Both now read docs/standards/, verified by loading all nine bundled documents. - _bump_version_and_changelog() wrote to docs/CHANGELOG-SYSTEM.md, a file that has never existed here. It took the else branch on every run and logged "No CHANGELOG file found", so the version-release-policy enforcement it implements had never once fired. It now targets the root CHANGELOG.md, emits a proper Keep a Changelog section rather than a bare bullet, and stages the file it actually writes. - Four intra-docs cross-links were already broken before this change and are repaired now that link paths were being touched anyway. - Nine GitHub-relative links (../../issues, ../../discussions) broke further when their files descended a level; they now use absolute repository URLs, which no directory depth can invalidate. - CLAUDE.md documented a root rules/ directory holding 46 standard definitions. No such directory exists -- zero files tracked, nothing on disk. The Directory Layout section describes the real docs/ tree. README.md's 15 documentation links and setup_wizard.py's two references were repointed. A full audit of every relative link across all 165 tracked Markdown files reports zero unresolved targets, and the suite is green at 1238 passed / 47 skipped / 11 xfailed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rules/45-uml-diagram-lifecycle.md SS 2 requires that generated diagram
files are never committed and that both output directories appear in
.gitignore. Neither condition held: uml/ and drawio/ were absent from
.gitignore, and 13 diagram files were tracked in Git.
The 13 tracked files were also stale and non-conformant:
- dated April, predating the v1.15-v1.20 refactors
- hyphenated stems (class-diagram.drawio) against the underscore stems
rules/45 SS 5 mandates (class_diagram.drawio)
- missing call_graph_diagram entirely
- including system-architecture.drawio, which rules/45 does not define
Removed from the index and from disk; content remains recoverable from
Git history. Added /uml/ and /drawio/ to .gitignore, verified with
git check-ignore.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two source documents for the v2.0.0 plugin transformation effort.
v2.0.0-plugin-transformation-requirements.md is the original requirement
statement (FR-1..FR-18, NFR-1..NFR-5). It carries two supersession
annotations added during execution:
- FR-18 ("uninstall leaves no orphaned hooks, no stale MCP
registrations, no leftover settings.json entries") is marked NOT
ACHIEVABLE as written. Plugin uninstall behaviour is undocumented and
the engine cannot attribute pre-existing settings.json entries to
itself. Narrowed to plugin-attributable residue, with FR-24 added to
cover the remainder.
- The deliverable-5 gate row is corrected to the same narrower claim.
orchestration_prompt.md is the executable prompt driving the phased
run, including ADR-006 through ADR-010 as boundary conditions agreed
before Phase 1 began.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Chikofsky and Cross Level 1-3 reverse engineering of v1.21.4 as-built,
used as the factual basis for every later phase.
Principal findings, recorded in as_built_executive_summary.md:
- Impact analysis is blind to the code it protects. The call-graph
builder is capped at 300 files against 411 on disk. All 45 files of
langgraph_engine/sdlc_pipeline/ and 38 of 45 hook files are entirely
invisible; roughly 25 percent of the budget is spent on tests/.
- Three "always-on" maintenance policies are silent no-ops. Auto-commit,
session-save and session-pruning target scripts/architecture/ subtrees
that do not exist; their .exists() guards fail with no log trace on
every turn, independently of any hook change.
- "46/46 orphan policies" was a briefing artifact, not a finding. SRS.md
was never supplied to that knowledge-graph build. Redone against the
real 15 FR/NFR entries: 14 of 46 are genuine orphans.
- Deleting PreToolUse/PostToolUse falsifies SRS FR-9's own acceptance
criterion for 2 of its 4 hook events.
- Two measurement conflicts left explicitly unresolved rather than
silently picked: the Stop-hook per-turn spawn floor, and whether a
clean plugin extraction seam exists.
dead_code_report.json was repaired during ingestion: a "scope" key was
nested illegally inside the live_function_ids array, failing the parse at
line 158. The malformed original is retained as .malformed.bak.
Three artifacts are deliberately NOT committed and are gitignored here:
ast_call_graph.json (4102 KB), codebase_kg/edges.json (3619 KB) and
codebase_kg/nodes.json (1553 KB). Together ~9 MB, each over the 500 KB
check-added-large-files gate. They are regenerable machine output, not
authored content; docs/phase-5-uml/callgraph_coverage_probe.md documents
the read-only invocation that rebuilds them. The gate was respected
rather than bypassed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
prd-v2.md carries the approved requirement set: FR-1..FR-24 plus
FR-4a/8a/9a/14a (28 identifiers), and NFR-1..NFR-5, each with a
measurable acceptance criterion and an RTM row.
Requirements added because reverse engineering contradicted the original
brief rather than confirming it:
- FR-9a CallGraph discovery blindness. A prerequisite for FR-10, not a
parallel task: impact analysis cannot be trusted until the
builder can see the code it protects.
- FR-8a Repair the Stop hook rather than strip it. 7 of its 9 target
scripts do not exist, so three maintenance policies silently
no-op every turn.
- FR-10a Six different knowledge-graph schemas across the library
require an adapter; a single reader cannot consume them.
- FR-2a NFR-2 is already violated inside the engine. Six timeout
application sites across five files, including a 75-second
wall-clock timeout on the pipeline path. Deleting hooks does
not satisfy NFR-2.
- FR-24 Covers the residue FR-18 cannot, after FR-18 was narrowed to
plugin-attributable state only.
product-sequencing-v2.md holds the WSJF scoring, the MVP boundary and the
critical path. The arithmetic over the WSJF inputs is exact and verified;
the input integers themselves are single-pass estimates that were never
cross-checked, so the MVP line is softer than the numbers imply. That
limitation is recorded rather than smoothed over.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hld.md is the solution architecture: 17 ADRs, a C4 model, the six open
architectural questions and their verdicts, and the migration runbook.
The six open questions, resolved:
1 CheckpointManager already exists outside the deletion set, so
crash recovery was never at risk. Phase 0's "sole writer" claim
conflated two independent systems.
2 The 15 hook-coupled policies split 5 port / 5 demote / 5 delete,
with push_gate fixed as a mandatory port plus CI gate.
3 Manifest-anchored __file__ ascent finds the plugin root without
relying on any undocumented behaviour.
4 CallGraph truncation. CORRECTED after the Phase 5 probe: the cited
site (parsers/config.py:11) is dead code read by nothing. The
binding cap is call_graph_builder_legacy.py:64, enforced at :107
and :118. This HLD is the origin of that error and 19 artifacts were
written from it, so the correction is recorded here in full rather
than as a line-number edit. 17 truncation sites exist; only 2 bind.
5 No clean plugin extraction seam exists. 708 fragmented communities
is positive evidence against a cut, so the seam is drawn around the
monolith via a plugin_api Facade.
6 FR-15's home-directory split is left deliberately unsized pending AST
re-derivation.
hld-v1.20.0-superseded.md preserves the pre-existing git-tracked HLD that
this one replaced; the output path had been assigned without checking it
was occupied. The original remains recoverable at a955c43.
plugin_schema_spike.md records the FR-14a spike results, run against a
backed-up settings.json verified byte-identical afterwards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The BA and PM reviewed the HLD, the architect defended it, and the
quality gates ran over the whole exchange. Four consensus iterations,
three rejections, each on an independently verified defect.
Two new ADRs came out of it:
ADR-019 The plugin bundles ZERO MCP servers. Bundled .mcp.json stdio
servers are spawned processes, so bundling any would violate
NFR-1 through the very mechanism chosen to replace hooks.
Registration becomes an explicit opt-in register-mcp command.
This resolves Phase 1's PROVISIONAL ADR-018.
ADR-020 Three-layer control on the push gate: PREVENT (unregister-mcp
refuses to strip it), DETECT (doctor reports it missing),
PREVENT-THE-HARM (git pre-push as last line). Neither of the
first two is built yet, and the artifacts say so.
SS 10's migration runbook was rebuilt to 8 steps with register-mcp at
step 2; the prior version silently assumed servers arrived with the
plugin.
sa_defence.json carries a field-suffix convention -- _record/_as_filed
means frozen at filing, _now means live and must be current -- enforced
by a validator embedded in the file and covering all 14 top-level
sections. It exists because one defect class recurred four times in that
file while the architect was actively working on it. A date-stamped
manual sweep was offered as the alternative and rejected: a sweep is a
documented-only control, and this file is its own disproof.
Its stated limit is honest and worth keeping in view: the convention
classifies FIELDS, not content. It makes omitting the frozen/live
distinction impossible; it cannot verify that a correctly labelled _now
claim is true.
The convention's own validator went stale in the pass that extended it --
the embedded source still encoded the old three-array scope while the
file claimed 14 sections. It was caught only by executing the stored
string rather than trusting the authored form. That is now a standing
rule, alongside: any check needs a companion negative test proving it can
fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The probe answered one gating question -- can call-graph discovery be
widened for a read-only documentation pass without editing source? -- and
found four things while doing it. All figures MEASURED at runtime.
1 The cap every artifact cites is dead code. parsers/config.py:11 is
read by nothing; its only importer re-exports it, and its docstring's
claim to be "a single source of truth" is false. The binding cap is
call_graph_builder_legacy.py:64, enforced at :107 and :118.
Implementing FR-9a against the cited line would have changed the
constant, looked complete, and left discovery stopped at 300 files.
2 17 truncation sites exist, but only 2 bind: legacy.py:64 and
graph_model.py:43 (MAX_PATHS = 500). Four file-size caps have
measured-zero impact, one file-count cap is dead and one dormant.
graph_model.py:43 survives fixing the file cap, so every sequence and
interaction diagram is truncated at 500 paths regardless of files.
3 CLAUDE.md's "578 classes / 3,985 methods" is a genuine untruncated
measurement of a 226-file tree from 2026-03, traced via git log -S to
ab54428 and reproduced at 579/3,992 on that archived tree. Today's
complete figures are 480/3,506 -- lower despite nearly double the
files, because the v1.15-v1.20 refactors deleted class surface. What
the pipeline consumes today is 449/2,844.
4 "4 languages (Python/Java/TS/Kotlin)" is false and always was. Zero
Java, TypeScript or Kotlin files at either tree.
Truncation is an alphabetical accident, not a decision: the last admitted
file is index 299 and the first sdlc_pipeline file sits at index 304. The
budget dies five files short of the pipeline core.
Read-only override demonstrated: CallGraphBuilder.__init__.__defaults__
propagates through build_call_graph(). Rebinding module-level MAX_FILES
is a silent no-op because defaults bind at def-time -- that is the trap.
srs_update_report.md records the SRS append and the FR-9 acceptance
criterion revision, with its traceability table and coverage disclosures.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rion Appended per rules/44 SS 4, which makes SRS.md append-only. No existing content was removed and no pre-existing line was rewritten. 28 functional requirements carried from prd-v2.md as FR-10..FR-37, plus FR-38 for the resolver defect below. PRD FR-19 was deliberately not carried -- it defers to v2.1 pending sign-off -- and is recorded by name in a deferral subsection rather than silently dropped. FR-9's acceptance criterion is falsified by the hook deletion: it requires all four hook events to fire and a blocking policy to return exit code 2 from PreToolUse. The original line is retained verbatim; an appended AC-9 (Updated 2026-08-01) states that only Stop remains registered and names the three compensating controls, each labelled DESIGNED, NOT BUILT. Whether UserPromptSubmit's registration is deleted or repointed is specified by no artifact, so it is recorded as open rather than guessed. FR-38 covers the call-graph resolver defect. graph_model.py:265 returns candidates[0] for a bare method name matching multiple FQNs, so builtin calls bind to same-named project classes: list.append gives JsonlAppender.append an in-degree of 1592, str.format gives ErrorMessages.format 755-756. 55.5 percent of cross-file resolved edges are collision artifacts. This is not cosmetic: call_graph_analyzer.py builds danger_zones (:303) and hot_nodes (:1197) from a caller-count gate and injects them into the Step 0 planning prompt, so the top danger zone today is an artifact of every list.append in the repo. FR-9a alone is insufficient -- fixing discovery without fixing resolution yields a larger graph feeding the same broken resolver. The dependency is stated in the requirement. Four change-log rows, one of which is undated and marked PENDING: FR-22's criterion requires a row dated to the PR that deletes the hooks, and that PR does not exist yet. It was neither back-dated nor omitted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A single tiered entry point for reviewing 61 artifacts before
implementation begins, so the review is a reading order rather than a
directory listing.
SS 1 Must read -- items where the reader's judgement changes what
gets built
SS 2 Open items, carried forward deliberately rather than settled to
make the documents look finished
SS 3 Accepted risks, recorded so they are not silently revisited
SS 4 Phase 1's six architectural verdicts
SS 4a Phase 2's decisions (ADR-019, ADR-020, the field-suffix
convention and its stated limits)
SS 4b Phase 5's probe findings, including the dead-constant citation
and the 17-sites-but-2-bind distinction
SS 4c FR-9b / SRS FR-38, the resolver defect
SS 5-6 Skim tier and machine output
SS 7 What the quality gates do and do not guarantee, plus the full
correction record
SS 7 is the section to read before weighting any score. The gates verify
PROVENANCE -- that a claim traces to a cited source and the source says
what is claimed. They do not verify TRUTH. FR-15's "13 call sites" passed
both Phase 0 gates at 1.00/1.00 and remains unverified by any independent
method. That is the gates' scope, not a defect in them, and it is why
SS 2 exists.
The correction record holds 24 entries. Only one was authoring
fabrication; the rest were accurate citation of imperfect measurements,
or errors originating in the orchestrator's own briefs. That is the
dominant failure mode in this run -- not agents inventing facts, but
faithful propagation of unverified ones. Cited Phase 0 numbers should be
read as best available, not verified.
Entry 21 is this document's own defect: it stated two rules were "binding
on FR-25 and FR-26" with the (proposed) hedge dropped, in the section
that catalogues exactly that class.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Phase 5 probe established that langgraph_engine/parsers/config.py:11
is dead code -- read by nothing, its only importer merely re-exporting
it, and its docstring's claim to be "a single source of truth" false. The
binding cap is parsers/call_graph_builder_legacy.py:64, enforced at :107
and :118. A second cap, parsers/graph_model.py:43 (MAX_PATHS = 500),
survives fixing the first and truncates path traversal regardless of file
count. Of 17 truncation sites only these 2 bind.
19 artifacts cite the dead constant. They are NOT swept uniformly,
because they are not the same kind of document:
CORRECTED -- these give active instruction, and following them produces
a change that looks like the fix while leaving discovery capped:
prd-v2.md FR-9a the requirement row an implementer works from
builder_divergence.md its Recommendation named the wrong file
orchestration_prompt.md the executable prompt
as_built_executive_summary.md listed first in the review index
ANNOTATED, NOT REWRITTEN -- Phase 0 gate reports, scorecards and
knowledge-graph output faithfully record what was believed when they
were written. Rewriting them would falsify a record to make it agree
with a later finding, which is the opposite of what a record is for.
The corrected documents above carry pointers to the probe instead.
builder_divergence.md retains its original recommendation verbatim under
a superseding note rather than replacing it, so a reader arriving from
any of the 18 uncorrected citations finds the disposition rather than an
absence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dependency-ordered plan for v2.0.0. Nothing was created on GitHub; the
37 issues are drafted and await the owner's confirmation.
No velocity data exists for this project -- no sprint history, no team
size on record, no throughput measurement. The plan therefore contains no
sprints, no dates and no velocity. It is 8 dependency-closed batches
(A-H) with relative Fibonacci sizing anchored on 1 point = PRD FR-6 /
SRS FR-16. Point provenance is tracked and never merged:
74 points SOURCED from product-sequencing-v2.md SS 2/2a/2c
34 points ASSUMED 13 issues sized against the anchor, no source
1 item UNSIZED PRD FR-15, whose source forbids sizing it before
the AST classifier runs; that mandate is honoured
Batch counts, per-batch splits, key uniqueness, dependency closure and
acyclicity are machine-verified.
Two critical chains, converging only at release: the plugin/gate chain at
23 points and the selector chain at 25. Which one actually binds depends
on team size, which is unknown and labelled as an assumption rather than
resolved by guessing.
All four mandated sequencing constraints are honoured and none proved
unsatisfiable. FR-4 is blocked by register-mcp, FR-23 and the ADR-017 CI
assertion together, with batch F closing before G opens. FR-9a and FR-9b
are one deliverable in two issues. FR-9a is sized as a two-site job, not
a 17-site one; both binding sites were verified on disk.
Findings the plan carries:
- graph_model.py:43 appears in NEITHER the PRD nor the SRS acceptance
criterion for FR-9a. The written AC names four sites, one of them the
dead constant, and omits the second binding site entirely. An
implementer working strictly to that AC would see every assertion
pass and leave a binding truncation in production. The AC is carried
verbatim and flagged rather than rewritten -- changing an acceptance
criterion changes the definition of done, which is the owner's call.
- New constraint found: FR-7 must precede FR-5. Removing
UserPromptSubmit first leaves the pipeline unreachable.
- The WSJF rank-1 item cannot go first. SRS FR-34's criterion is half
satisfied by the Phase 5 append and half blocked by FR-4.
- Deliverable 1 is recorded DONE while its artifact is absent from
disk; 18 ASSUMED points depend on how the owner reads that.
- product-sequencing-v2.md's sequencing risk #3 is stale: it says three
times that the HLD runbook lacks a register-mcp step. It has one, at
step 2.
No batch boundary and no intra-batch ordering derives from WSJF -- every
one comes from a dependency, a fixed gate or a correctness constraint.
An ordering that could have rested on a 0.33 delta between integers
estimated once is called out as such, and the plan notes it deliberately
inverts that WSJF order on dependency grounds.
Also corrects SRS.md's numbering note, which said "FR-10 through FR-37
(28 entries)" -- written before FR-38 was appended and not updated when
it was. Verified by enumeration: 29 entries, FR-10..FR-38.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Owner ruling: FR-9a's acceptance criterion is amended, to the stronger of
the two options offered.
The written AC named FOUR truncation sites, one of them the dead
parsers/config.py:11, and omitted graph_model.py:43 entirely. An
implementer working strictly to it would see every assertion pass and
leave a binding truncation in production. The requirement's own
definition of done certified a half-fix.
The amended AC asserts on the output of an in-process build. No clause
can be satisfied by inspecting a constant, which is the failure mode that
produced this defect:
A Discovery is coverage-complete: the analysed set EQUALS the set of
eligible source files enumerated by the test's own oracle. Empty
symmetric difference, not a hardcoded number. The oracle must
replicate the builder's eligibility rules WITHOUT importing them, or
it proves nothing.
B Named canary whole: symmetric difference over sdlc_pipeline/ is
empty. Baseline 45 files, 0 of 45 analysed today; the budget is
exhausted five files before that tree begins, at discovery index 304.
C No traversal truncation emitted: zero log records matching
"hit max_paths=" over langgraph_engine.parsers.graph_model. LOAD
BEARING, and marked so in all three files -- it fired on both probe
runs including the cap-lifted one, so a fix passing A, B and D but
not C must fail the gate.
D Regression floor: at least 411 files, 480 classes, 3506 methods.
E The silent-no-op trap is closed: the test SHALL NOT prove the fix by
rebinding call_graph_builder_legacy.MAX_FILES, measured to be a
no-op because max_files=MAX_FILES binds at def-time.
F "Fixed the one site Phase 0 named" is not an acceptable end state.
Target corrected to the 2 binding sites. config.py:11 is dropped, with
the note that deleting it asserts nothing and must not be counted as
progress. Site accounting is stated so the count matches its
enumeration: 17 total, 2 binding, 15 non-binding as 1+1+1+4+1+2+5.
One deliberate deviation, flagged rather than hidden: clause D is a floor,
not a hardcoded equality. A legitimately added file would fail an
equality assertion, and the natural fix would be to bump the number --
reintroducing a maintained constant into the test that exists to prove
constants are gone. Clauses A and B carry the exactness instead.
Originals retained. SRS.md is strict append-only and its FR-21 row is
untouched. In prd-v2.md the acceptance-criterion cell is byte-identical;
only the row label gained a SUPERSEDED marker, so a reader cannot meet
the old criterion without finding its disposition. Stated precisely
because "verbatim" was the requirement: the AC text is verbatim, the
label is not.
github_issues.json: exactly one issue changed, V2-009. Verified after
write -- 37 issues, key order preserved, meta hash unchanged, sizes,
provenance, labels, dependencies and titles all untouched, zero dangling
blocked_by references, zero non-ASCII. No dependency array was read,
written or reordered.
SIZING: 5 points no longer holds. The estimate covered editing two
constants and retiring two dead sites. The AC now adds a runtime probe
harness, an independent enumeration oracle, module-scoped log capture,
and a negative test that must demonstrate a failure mode. Deliberately
NOT re-pointed; the flag is recorded in the PRD AC, the SRS AC and
V2-009's own array so it cannot be missed.
Two related findings, both verified on disk:
- test_discovery_covers_every_package does not exist, so the AC
describes a test to be written rather than extended. This pushes the
size estimate further still.
- CLAUDE.md's documented test command names
tests/test_call_graph_builder.py, which is absent. Only
test_call_graph_analyzer.py exists, so that command fails as written.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deliverable 1 was recorded DONE and its findings approved, but the report file was never written. 18 story points across sprint issues V2-004 to V2-008 rested on a recollection rather than a document. This is that document. Scoped as consolidation plus spot-verification, not re-analysis. The underlying work passed both Phase 0 gates; re-deriving it independently risked producing figures diverging from a deliverable already approved, which would re-litigate a closed decision. Status counts reconcile: 18 ENFORCED + 11 PARTIAL + 8 CONTRADICTED + 8 DOCUMENTED-ONLY + 1 STALE-TOPOLOGY + 0 DEAD = 46. Re-derived by grouping all 46 records by status and enumerating each group rather than trusting the summary table, because correction #9-13 in the review index records a tally that passed a total-check while being wrong. Two supporting reconciliations, both measured: docs/policies/ holds exactly 46 .md files, and the raw artifact holds 46 records with 46 unique policy_file values, 0 missing from disk. Spot-verification: 8 checks plus one corpus check, across 4 of the 6 status categories. 41 of 46 classifications were NOT verified, and the report says so -- PARTIAL, DOCUMENTED-ONLY and STALE-TOPOLOGY were not sampled at all. The two ENFORCED checks were deliberately chosen for different mechanisms, hook dispatch table versus LangGraph node edge, so a pass could not confirm one code path twice. - The 3 named no-op policies: CONFIRMED. scripts/architecture/ 01-sync-system/ and 09-git-commit/ do not exist. The guards have no else branch and no log on the negative path, so the silence is real. - 2 ENFORCED: both REACHABLE by registration, not mere presence. - 3 CONTRADICTED: all still hold against current code. A near miss worth recording: 03-execution-system/failure-prevention/ does exist and holds failure-kb.json, but the script spawned from it does not. The present directory makes the spawn look plausible to a casual check. Stop-hook dead scripts measured at 7 of 9, confirming the prior figure on the criterion "absent repo-wide". Two refinements the bare 7 conceals: - An 8th is unconditionally broken and not environment-dependent. post_impl.py:285 resolves sync-version.py as a sibling inside hooks/stop_notifier/. A copy exists at scripts/tools/sync-version.py, so a filename search finds it and it counts as present -- but the hook can never reach it. On a strict "does the spawn resolve" criterion the count is 8 of 9. Reported as 7 to stay consistent with the approved figure, with the discrepancy flagged rather than merged. - Row 9, voice-notifier.py, could not be determined: it resolves through CLAUDE_IDE_INSTALL_DIR, unset here but resolvable in IDE mode. Not counted as dead. The report records the retraction of the earlier "46/46 orphan policies" figure, which was false -- an orchestrator briefing error in which SRS.md was never supplied to that knowledge-graph build, leaving it no requirement corpus to match against. The real figure is 14 of 46, carried as CITED rather than re-derived, since re-deriving means re-running the SRS correlation this pass was scoped out of. The audit also reports a contradiction against its own source artifacts rather than smoothing it: policy_enforcement_raw.json still cites the dead parsers/config.py:11 as the enforcement point for the callgraph analysis policy. No status count changes; only the site attribution is wrong. Handled separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Deliverable 1 audit reported a contradiction against its own source artifacts rather than smoothing it: policy_enforcement_raw.json still named the dead parsers/config.py:11 as the enforcement point for the callgraph analysis policy. It enumerated four artifacts still carrying that attribution. Two of those four are corrected here, because an earlier classification of mine was wrong. I had filed both as records to be annotated rather than rewritten, on the principle that a record accurately captures what was believed when written. That holds for gate scorecards. It does not hold for these two: contradictions.md is listed as MUST-READ in docs/REVIEW-INDEX.md, and it asserted the dead site under the heading "Reality". A must-read document stating a dead constant as reality is active misdirection, not a historical record. policy_enforcement_raw.json is machine-readable and consumed downstream, so a tool acting on its enforcement_point field would be sent to a constant that changes nothing. Both corrections state that the CONTRADICTED status and every count are unaffected -- only the site attribution was wrong. The JSON keeps its original field alongside a dated correction field rather than silently overwriting, so a consumer that already read the old value can find its retraction. Validated after edit: 46 records, status tally 18 ENFORCED / 11 PARTIAL / 8 CONTRADICTED / 8 DOCUMENTED-ONLY / 1 STALE-TOPOLOGY, matching the approved figures exactly. Also repairs a structural error I introduced in contradictions.md, where the correction block had been spliced into the middle of a sentence, stranding the second half of the Reality paragraph inside a blockquote. The block now follows the complete section. Both files re-verified at zero non-ASCII characters. as-built-prd.md and ast_call_graph_summary.md still carry the old attribution and are left as records; neither directs implementation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ound All 37 sprint issues now exist on techdeveloper-org/claude-workflow-engine as #256-#293, created in batch order A-H so numbers ascend with dependency order. This file maps every local key to its issue number and URL. One duplicate exists and is recorded with its root cause: #257, a double-write of #256 from the non-idempotent POST retry fixed in mcp-github-api 33af037. #256 is canonical and open; #257 is closed with an explanatory comment. Zero duplicates occurred across the remaining 36 creates, each verified immediately after creation rather than in a batch at the end. Dependency cross-references are recorded as 26 issue comments, not in the issue bodies. The github-api MCP server exposes no issue-update tool -- no update_issue, no get_issue, no list_comments -- which was checked before choosing the mechanism rather than assumed. Bodies still name blockers by local key; the comment on each issue is the cross-reference of record and renders as a GitHub link. The map also records two defects so the next agent does not rediscover them: - github_list_issues capped silently at 25 rows. A listing of 38 issues returned 25 with no signal. Fixed in mcp-github-api bc79339. - No issue-update tool exists on that server, which is why the cross-references are comments. Both MCP fixes require a server restart to take effect, so every write in this run was made under the unfixed client. The clean result across 36 creates is evidence the duplicate-write race is timing-dependent, not that it was absent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds section 8b to the Phase 5 SRS report, covering the acceptance criterion work done under the owner's 2026-08-01 ruling. This file was updated alongside prd-v2.md, SRS.md and github_issues.json but was missed when those three were staged, so the report lagged the change it documents. Records what was wrong: FR-9a's criterion asserted on four truncation sites, one of them dead code, and omitted the one cap that survives fixing the file cap. It also asserted on end states of constants, and constant-inspection is precisely the failure mode that produced the defect. Reading config.py:11 and concluding the cap had been found is what 19 artifacts across every phase did. Records the verification performed BEFORE writing, as a table of claims against results rather than a summary. Each load-bearing input was re-checked at source: graph_model.py:43's exact declaration and its enforcement points, the truncation warning string verbatim, call_graph_builder_legacy.py:64's binding path from default to enforcement, config.py:11's dead status via its single re-exporting importer, and the 17-site enumeration reconciled to its component counts. Records the design choice and its reasoning: the measured figures were deliberately NOT turned into hardcoded equality assertions. A legitimately added source file would fail such a suite, and the natural fix would be to bump the number, reintroducing a maintained constant into the thing meant to prove constants are gone. Coverage is asserted as set equality against an independent oracle instead, with the measured values kept as a documented regression floor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
V2-009 (PRD FR-9a / SRS FR-21, live as issue #265) was sized 5 on the assumption that the work was changing two constants and retiring two dead sites. The owner then ruled its acceptance criterion be corrected and strengthened to require runtime proof, which changed the work. Two agents declined to re-point it, correctly: re-estimating after a scope change is the product manager's call, not the author's of the criterion nor the creator of the issue. Nothing was silently adjusted in the interim; the stale figure was flagged in three places instead. New size 8, reasoned against the plan's existing anchor of 1 point = PRD FR-6, and enumerated as 8 work items in plan section 2.4. The load-bearing comparison is V2-010 at 5: a one-line fix whose cost is six mechanical consumption-point edits plus a collision check and one unit test. V2-009 now carries eight items rather than eight edits, and one has no comparable anywhere in the plan -- the enumeration oracle must duplicate logic it may not import (5 extensions in glob order, a 12-entry excluded-dirs set, a 100 KB size cap, OSError handling). An oracle that imports proves nothing; one that hardcodes a stale copy diverges silently. Not sized 13: 13 appears in this plan only as a 4-issue set total, and V2-009's surface is bounded at two files plus one test module. A risk was named rather than absorbed into the number. Criterion (C) demands zero "hit max_paths=" records over a graph measured at 26,114 edges. Whether an unbounded DFS terminates acceptably has NOT been measured. If it does not, the fix becomes algorithmic and 8 will not hold. 8 does not cover a redesign of path enumeration. Arithmetic, each total matching an enumeration: Batch B 8 + 5 = 13, was 10 Selector chain 8 + 5 + 8 + 2 + 2 + 1 + 2 = 28 across 7 issues, was 25 Plugin chain unchanged at 23 The two chains are not additive: V2-036 is counted in both, and their union is 13 distinct issues. Provenance totals: SOURCED 69, ASSUMED 34, RE_ESTIMATED 8, total 111, up from 108. SOURCED drops by 5 because V2-009's original figure leaves that bucket. Reconciles two ways: 69+34+8 = 111, and the eight batch totals also sum to 111. RE_ESTIMATED is a new provenance bucket rather than a fold into an existing one. The figure is not SOURCED, because the source no longer describes the work, and not ASSUMED in the sense the other 34 are, because a prior figure existed and was displaced rather than absent. Assumption A5 records that this is A3's problem one degree worse: same anchor, third estimator, third pass. The critical path remains undetermined. The re-point did not change which chain is longer, only the margin, from 2 points to 5. Which chain binds still depends on team size, which is not on record, and no team size was assumed. Both chain sums rest on single-pass estimates never calibrated against a completed item; a 2-point gap was never a safe basis for declaring a binding chain and 5 is only better, not sound. Exactly one issue changed in github_issues.json, proven by round-tripping the serializer first so the diff is purely the edit. Issue order, all 37 keys, every blocked_by and all 36 other sizes are unchanged, as is V2-009's own body and its 14-item criterion. Acyclicity and dependency closure re-verified: acyclic, no dangling references, all 37 reachable. One disclosed deviation from the instruction to change only size and size_provenance: V2-009's size:5 LABEL was also changed to size:8. Leaving it would have produced a label contradicting its own size field, which is the count-versus-enumeration defect class this project has caught repeatedly. Accepted as correct judgement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Routes every v2.0.0 sprint issue to agents and skills from the global
library, selected via the master knowledge graph. 37 of 37 routed: 22
HIGH confidence, 11 MEDIUM, 4 LOW, 0 NO MATCH.
24 distinct agents are used and the work did not collapse onto a
generalist. AST work went to ast-graph-engineer and
static-analysis-engineer, CI gates to architecture-conformance-auditor,
resilience to harness-engineering-architect, checkpoint durability to
graph-orchestration-runtime-engineer, Gherkin to
acceptance-testing-bdd-engineer.
Name verification used two independent checks on every name, aborting on
any miss: exact membership in the master catalogues, and existence of
agents/<name>/agent.md or skills/<name>/SKILL.md on disk. The catalogues
were reconciled against the filesystem first, 505/505 agents and 992/992
skills matching in both directions with zero orphans either way. A
post-write validator then re-extracted all 26 agent paths and 54 skill
tokens from the finished documents and re-verified them.
This rigour is deliberate. The coordinator previously dispatched an agent
citing three skills that do not exist, so no name here is accepted from
inference.
SIX CAPABILITY GAPS, three of them structural. The library cannot cover
the two competencies this release is actually about:
GAP-1 Claude Code plugin packaging. No agent and no skill. The only
plugin agent in 505 is figma-plugin-engineer, which is Figma
manifest v2 and does not transfer. architecture-conformance-
auditor covers only the CI-gate half of one issue.
GAP-2 MCP server engineering. A content grep across all 505 agent.md
files for Model Context Protocol, MCP server, .mcp.json or
Claude Code plugin returns 2 files, neither an owner: one
consumes an MCP server, the other lists MCP as an attack
surface. This sits ON THE CRITICAL PATH via V2-016, the
register-mcp command.
GAP-3 Technical writing. Zero technical writers across 505 agents and
93 domains. The only runbook skill fires from incident
telemetry, which the migration-runbook issue does not have.
GAP-4 coverage-ledger cross-checking, GAP-5 decision-provenance
schema, GAP-6 distributed lease renewal, one of five ADR-016
mechanisms with no named owner; the other four map directly onto
harness-engineering-architect's mandatory skills.
The routing is executable rather than decorative. Library agents are not
registered subagent types and cannot be invoked by name, so every row
records the agent.md path its persona is lifted from. A live PreToolUse
hook blocks generic subagent spawns without an injected persona block,
so a row naming an agent without that path would not run.
V2-009 is routed with TWO supporting agents rather than one:
ast-graph-engineer for the binding sites, harness-evaluation-engineer for
the probe harness and log capture, unit-testing-specialist for the
independent oracle and the mandatory negative test. That matches the
re-point from 5 to 8 rather than contradicting it.
Routing is a capability match, not a competence guarantee. The four LOW
rows need their domain facts supplied as literal prompt input rather than
left to the agent's background knowledge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…heck hld_v2.md contradicted itself. OAQ 4 had been corrected to state that parsers/config.py:11 is dead code and that only two of seventeen truncation sites bind. ADR-013, in the same file, still gave that dead constant as its Context and asserted the defect existed at "FOUR independent sites" with it named first. ADR-013 is the section an implementer opens for the discovery fix. OAQ 4 is a question-and-answer block further down. The two are read by different people at different times, and the one carrying the wrong target was attached to the decision. Context now leads with call_graph_builder_legacy.py:64, bound as the default at :76 and enforced at :107 and :118, and names graph_model.py:43 as a second cap that binds independently and survives any fix to the first. config.py:11 is retained and relabelled dead rather than deleted, since other artifacts still point at it. The site count is corrected in substance, not just attribution: 17 exist, exactly 2 bind, with the other 15 enumerated as inert with reasons. The refinement the architect made earlier is applied at the point it matters: a work list padded with inert sites invites a fix aimed at the wrong target, which is precisely how the original citation error would have played out. A binding clause aligned to the strengthened FR-9a criterion was added: proof by runtime output, set equality against an independent oracle, zero max_paths truncation records, a negative test, and an explicit prohibition on rebinding MAX_FILES, since the probe measured that rebinding is silently ignored while discovery stays at exactly 300 and appears to have been raised. ALSO ADDS A MECHANICAL CHECK, in answer to a question I asked rather than a fix I requested: can anything catch two prose sections of one document disagreeing about a fact? General semantic contradiction between prose passages is NOT mechanically detectable, and that boundary is stated plainly rather than papered over with a control that sounds good and never fires. But a narrower thing is detectable and would have caught this one: a contested-fact registry. A list of identifier-to-required-qualifier pairs, checked by proximity grep. Register config.py:11 as requiring a co-occurring dead / read by nothing / asserts nothing qualifier, and ADR-013's Context fails immediately. It works for the same reason the _record/_now convention works: it does not verify truth, it verifies that the classification is present. The trigger point is what makes it practical -- a fact is registered as contested at the moment it is corrected, which is exactly when its contested status is known. That converts "after correcting a fact, grep every other mention" from a procedure into a check, and procedures are the documented-only failure this project keeps re-learning. Built, run, and stress-tested: passes at 0 across all 8 mentions and fires on an injected unlabelled one, per the standing rule that any check needs a companion negative test proving it can fail. The first probe's qualifier set was too narrow and produced 3 false positives, which is itself the finding -- a noisy check gets disabled. Limits stated: it covers only registered facts, false positives are real and qualifier patterns need care, and it raises the floor without closing the class. Both files remain byte-identical at 1,976 lines, ASCII-clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Handoff spec for authoring in claude-global-library. The owner will have these built separately and bring them back, so this states requirements rather than implementing them. Phase 7 established that the library cannot cover the two competencies this release is actually about. Both findings come from enumeration over the master knowledge graph, not impression: the catalogues were reconciled against the filesystem in both directions with zero orphans, and a content grep was run across all 505 agent.md files. PRIORITY 1, mcp-server-engineer, on the critical path. A grep for Model Context Protocol, MCP server, .mcp.json or Claude Code plugin across all 505 agent files returns 2, neither an owner: one consumes an MCP server, one lists MCP as an attack surface. Under ADR-019 the plugin bundles zero MCP servers, so register-mcp is the only path to any MCP-backed capability in v2.0.0, and the push gate has no preventive cover until it lands. PRIORITY 2, claude-code-plugin-engineer. The library's only plugin agent is figma-plugin-engineer, which is Figma manifest v2 in a browser sandbox. Nothing transfers. PRIORITY 3, technical-writer-agent. Across 505 agents and 93 domains there is no technical writer, no documentation engineer and no docs-as- code agent. The only runbook skill fires from incident telemetry after a production incident resolves, which a product uninstall procedure does not have. Right shape, wrong trigger. Four new skills are specified alongside them. The spec also says what NOT to author. GAP-4, GAP-5 and GAP-6 are narrow single techniques inside issues that already have a competent owner, and are better supplied as prompt input than as library entries. Authoring agents for them would add surface without adding capability. Source material is listed per fact so the author does not have to invent domain knowledge this run already measured, including the FR-14a spike results and the two real MCP defects found today, whose commits are the best available teaching material for the MCP skill. Acceptance is stated as a falsifiable test rather than a description: each agent must take its assigned issue WITHOUT the domain facts pasted in as prompt input. That is exactly what the current library fails, since four rows are routed LOW today with instructions to paste the manifest contract, the MCP registration format and the write-safety rule as literal source. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Surveyed the workspace as the owner asked. Result on registration: there is nothing to register. All 21 MCP servers are already in settings.json, mapped to their source repos and verified by path rather than by name. The only mcp-* directory not registered is mcp-base, correctly, because it is a shared package and not a server. Three further servers (dataforseo, google-trends, seo-mcp) are registered from outside the workspace, and video-learner comes from claude-visual-learner. An earlier pass of mine reported 20 unregistered servers. That was a false result from a shell-mangled regex, caught because 20 contradicted the fact that these servers are demonstrably in use. It is not a finding and is recorded here only so it is not resurrected. The survey did change the P1 agent spec substantively. The capability gap is real, since no agent owns MCP engineering, but the domain knowledge is not missing from the organisation -- only from the library. 21 working servers exist, all sharing a vendored copy of the mcp-base framework. The agent must be written against that corpus so it produces servers consistent with the existing 21 rather than a 22nd dialect. Two concrete consequences, both now in the spec: AtomicJsonStore (mcp_base/persistence.py:26) already implements exactly what ADV-008 demands of V2-016 -- thread-safe, atomic read-modify-write, write-to-temp-then-rename for crash safety. V2-016's hardest correctness constraint has a tested implementation in the owner's own library and should be reused rather than reimplemented. Whether that changes V2-016's size is the product manager's call and is deliberately not decided here. mcp-base is vendored by copy as base/ rather than depended on as a package, so a fix to one copy does not reach the others. Demonstrated today: the non-idempotent POST retry was first fixed in a copy while canonical and 18 other copies still carried it. An MCP-engineering agent that does not know this will keep fixing copies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sses
The owner's team authored the three agents and four skills specified in
library_gap_spec.md. Verified present on disk and in both master
catalogues: agents 505 to 508, skills 992 to 996, reconciled in both
directions.
ACCEPTANCE TEST PASSES. The test was stated as: each new agent must take
its assigned issue WITHOUT domain facts supplied as prompt input, lifting
the four LOW rows.
HIGH 22 -> 29
MEDIUM 11 -> 8
LOW 4 -> 0
NO MATCH 0 -> 0
11 rows revised, distinct agents 24 to 26. Rows were lifted only against
a cited passage of delivered content, never against the existence of a
plausibly-named file, since an unjustified lift hides a gap rather than
closing it.
V2-015 LOW to HIGH. The packaging skill supplies the manifest location,
the closed eight-field schema, and the nine fixed discovery names. It
also proves hook merging produces a flat unlabeled union with no
surviving provenance, so whole-plugin enable/disable is the only
control surface. That is ADR-010's forcing fact, and the agent
carries it as an operating rule rather than as background.
V2-021 LOW to HIGH. The cleanest lift. The row was LOW because the only
runbook skill fires from incident telemetry. The new agent rebuts
that objection in its own text, directing incident-corpus work
elsewhere and claiming known-process documentation for itself.
V2-024 LOW to HIGH. Callable-by-name is now defined by the tools/list
to tools/call triad and the descriptor shape.
V2-016 LOW to MEDIUM. Held below HIGH for two independent reasons
below.
V2-016 is the one row that did not reach HIGH, and the reasoning is worth
keeping. A grep for mcp-base, AtomicJsonStore, MCPResponse,
mcp_tool_handler, LazyClient or vendored across all seven delivered files
returns zero matches, so the amendment committed in 0974e4d did not reach
the author. The MCP domain facts themselves are fully present, including
merge-against-fresh-read ranked honestly below optimistic concurrency
with the warning that atomic rename does not solve lost updates. What is
missing is a workspace fact, not a domain fact, so the cost is reuse
rather than correctness: the agent will build a 22nd private write-safety
helper while AtomicJsonStore already exists and ADV-008 already flags
exactly that helper as an escalation candidate. Separately and
independently, ADR-020 Path C still has no owner in the library, new
agents included -- it is a one-shot measurement, not MCP engineering.
Three deliveries are thinner than specified, recorded so the team can
close them rather than discover them later:
1. The MCP skill was written from the public protocol specification
rather than against the 21 existing servers. This is what costs
V2-016 its HIGH.
2. The technical writer does not claim ADR bodies or audit-report
prose, both named in the spec. The string ADR appears nowhere in the
agent or its core skill. No row pays for this, since
solution-architect already owns those rows at HIGH, but the library
still has no owner for ADR prose.
3. The ADR-019 eager-spawn fact is split across two skills, with the
packaging skill stopping short of it. One cross-reference paragraph
closes this.
The packaging and procedural-documentation skills are at or above spec.
A side effect confirms GAP-3 was genuine rather than a matching
preference: postmortem-facilitator-agent now holds no position on any
row. It was the stand-in for the missing technical writer, and with a
real one present it has no remaining role.
Also recorded: the library VERSION file and the master graph's
library_version both still read 29.72.0 while agent_count moved to 508.
The graph was rebuilt without a version bump -- the same drift class
V2-002 exists to fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…batch A Adversarial readiness gate before any code is written. The brief was to try to start batch A and find what blocks it, not to produce a clean bill of health. It found enough to change what happens in week one. BATCH A: 3 of 8 issues can start today, not 8. Startable: V2-001, V2-002, V2-003 (build only, its run is gated by a closes_after constraint). The other four carry unmet INTRA-batch blockers. The Deliverable 1 set is a four-level serial chain, V2-004 then V2-005 then V2-006 and V2-007 then V2-008, holding 18 of batch A's 23 points. The sprint plan's claim that batch A can start today is scoped to other batches and is literally true, but its batch table has no blocked_by column, so anyone staffing eight issues in parallel finds four blocked. THE FALSE PREMISE, and it originated in this run. V2-004 asserts that docs/reports/policy-implementation-audit-v2.md is ABSENT. Four artifacts and the live issue repeat it. The file EXISTS: 507 lines, 28,780 bytes, committed bf92747. The sprint artifacts asserting absence were written before it existed and were true when written. Issue #260 was created twenty minutes AFTER the premise became false, from a plan nobody re-read. The file also satisfies none of V2-004's three acceptance criteria. AC1 wants a 46-row matrix; the file has four tables of 18, 11, 8 and 8. 27 of 46 rows carry no evidence cell. The header states the opposite of AC3. And the string post-plugin occurs zero times, so V2-005's seven-column requirement is already unmet before V2-005 starts. AC3 is separately UNSATISFIABLE as written: it demands a line-by-line read of a policy tree holding 32 documents, while AC1 requires 46 rows and 18 of the 46 are absent from that tree. MOST LIKELY FIRST-WEEK FAILURE, argued rather than listed: V2-004 gets closed against the file that already exists, taking V2-005 through V2-008 with it. Week one is batch A, V2-004 roots 78% of its points, the issue says absent while the file is present and authoritative-looking, AC1 is half-true, and the only criterion that would catch the substitution is the unsatisfiable one, so skipping it is the natural move. Sequencing risk R-6 predicted this exact mode and then resolved it by asserting absence, an assertion that was false fifteen minutes later. With no velocity data, week one is the calibration anchor: 23 points would be booked against roughly 5 points of real work. ROUTING IS CLEAN, checked exhaustively rather than sampled. All 37 primary and 36 supporting agent.md paths resolved on disk, 73 of 73. 26 agents and 50 skills present in both the catalogues and the filesystem. All 37 routing_table.md sections agree with routing_map.json on issue number, confidence and agent, with zero mismatches. The persona gate is live and blocked the auditor's own tool calls during the audit. FIFTEEN CROSS-ARTIFACT DISAGREEMENTS, the two dominant defect classes of this project still producing instances. Notable: issue_key_map.json holds V2-009 at size 5 and SOURCED, the only stale entry of 36, summing to 108 against the corrected 111. V2-009's own body and its AC item 10 assert size 5 against their own size field of 8. The sprint plan's "Still open" paragraph is false on all three of its targets and was added by the correction commit itself. Assumption A3 says 74 sourced points while A5 directly beneath it says 69. WSJF was never recomputed after the re-point, 4.60 to 2.88. Point totals themselves reconcile exactly at 69+34+8 = 111 across 37 issues, recomputed from enumeration. DEPENDENCY GAP: V2-017 is absent from both V2-027's and V2-026's blocked_by, although V2-026's body requires V2-017's layer-2 check. The push-gate chain is otherwise correctly encoded, FR-25 and FR-26 are confirmed undrafted, V2-016's one-shot Path C timing is encoded in its AC, and FR-7 before FR-5 is enforced by the dependency graph rather than by batch ordering alone. Recorded as not verified: live issue bodies, since the github MCP server exposes no get_issue; whether the 26 cross-reference comments duplicated; and batches B through H, which were not swept for the same premise-staleness class and are the largest unexamined area. One probe returning zero policy files via a non-recursive glob was discarded rather than reported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e-point Four mechanical corrections found by the Phase 8 alignment gate. All are verifiable against decisions already taken; none introduces new judgement. 1. issue_key_map.json held V2-009 at size 5 and SOURCED, the only stale entry of 37, summing to 108 against the corrected 111. Corrected to 8 / RE_ESTIMATED. The total is now computed from the map's own entries rather than copied from the plan: SOURCED 69 (45 pure plus 24 split inside a sourced set), ASSUMED 34, RE_ESTIMATED 8, total 111 over 36 int-sized entries plus one unsized. A crude sweep for "size 5 and SOURCED" also returns V2-016, V2-020 and V2-026. Those are legitimate -- register-mcp, FR-16 and FR-7 are genuinely 5 SOURCED -- and were correctly left alone rather than swept along with the target. 2. ASSUMPTION A3 said 74 SOURCED points while A5, directly beneath it, said 69. A3 predated the re-point. 69 is correct: V2-009's original 5 left the SOURCED bucket when it moved to RE_ESTIMATED, so SOURCED went 74 to 69 while the total went 108 to 111. The two now agree. 3. The "Still open" paragraph was re-verified against disk rather than accepted from the brief that flagged it, and the brief was wrong on two of three targets. It claimed the amended FR-9a criterion had not propagated to prd-v2.md section 5 -- FALSE, the superseding row is present with the four-site row retained verbatim and marked superseded, the RTM re-pointed and a change log row added. It claimed the same of SRS.md -- also FALSE, the revised criterion is appended per rules/44, and the old row necessarily survives because that file is append-only, which makes appending the only legal propagation mechanism there. The third claim, about the ADR-013 bodies, is PARTLY TRUE and is recorded rather than closed. See below. 4. WSJF for V2-009 was recomputed from the published inputs rather than adopted from a summary: UBV 6, TC 9, RROE 8, so CoD 23, and 23/8 = 2.875, rounding to 2.88 at the two decimals every other row uses. CoD is unchanged because the re-point moved size, not cost of delay. The recomputation surfaced a tie: NFR-2 also scores 2.875, from materially different inputs (8, 8, 7 against 6, 9, 8). Two units with different value, urgency and risk profiles now score identically -- a fair illustration of what compressing three single-pass integers into one ratio costs. V2-009 drops from 10th to joint 13th of 17 WSJF-bearing units, and NOTHING WAS REORDERED. The plan now states explicitly that nothing should be: no batch boundary and no intra-batch ordering derives from WSJF. V2-009 stays first in batch B because V2-010 hard-blocks on it. Carried forward and not laundered: recomputing makes the score arithmetically consistent with the new size. It does not make it calibrated. The cost-of-delay integers remain single-pass estimates never cross-checked against a completed item. A RESIDUAL DEFECT IS RECORDED, NOT FIXED. hld_v2.md line 1727 still reads "each of sites 1-4 must reach one of exactly two recorded end states", with no supersession marker, contradicting ADR-013's own corrected body 1,283 lines above it in the same file, which now reads "17 truncation sites exist; exactly TWO bind". An implementer reaching that closing clause rather than the ADR body still receives the four-site instruction including the dead config.py:11. Its cited location had drifted from 1696-1700 to 1727 because content was inserted above it, which is the likely reason it was missed: a correction keyed to line numbers stops finding its target as soon as the document grows. Left for solution-architect, since correcting a binding clause in an approved HLD is that owner's call. V2-009 keeps its needs-decision label; that hazard is not discharged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… premise
Owner ruling after the Phase 8 gate. V2-004 could not have been completed
as written, for two independent reasons.
THE UNSATISFIABILITY. AC3 demanded a line-by-line read of the home policy
tree, while AC1 required a 46-row matrix over docs/policies/. Those two
cannot both be met: 18 of the 46 are absent from the home tree, so no
reading of it produces evidence for 46 rows. This was a defect in the
original criterion, not a shortfall in anything produced.
Replaced with a policy-to-code mapping verification: six machine-checkable
assertions that test what AC3 was reaching for -- that a status
classification is grounded in code rather than asserted -- and that can
actually pass.
1 Row-set identity: the matrix's policy cells EQUAL the basenames in
docs/policies/, checked as an empty symmetric difference rather than
as a count of 46, so a wrong-but-equal count cannot pass.
2 Every row carries a Verification label.
3 MEASURED rows must RESOLVE: the evidence cell names a path and line,
and a script confirms the path exists and the file is that long. This
is the grounded-in-code test, and it is mechanical.
4 CITED rows must attribute a source artifact that exists on disk.
5 NONE is explicit and never blank, and such a row may not be MEASURED.
6 The header's coverage split must equal the split recomputed from the
rows.
There is deliberately NO minimum MEASURED count. The audit discloses that
41 of 46 classifications remain CITED, so a 46-MEASURED bar would be
unsatisfiable in a new way -- replacing one impossible criterion with
another. The assertion is on the presence and correctness of the label,
not on its value, which is the same principle the record/live field
convention and the contested-fact registry both rest on.
THE FALSE PREMISE. V2-004's body asserted that
docs/reports/policy-implementation-audit-v2.md is ABSENT. It exists: 507
lines, 28,780 bytes, commit bf92747, written roughly twenty minutes
before the issue was drafted from a plan nobody re-read. The body now
opens with that correction and instructs against closing the issue on the
artifact's current shape, and build_status reads BUILT, NOT YET
CONFORMANT, because the outstanding work is a reshape and not a rebuild.
TWO OF MY FIGURES WERE WRONG and are corrected here. The home policy tree
holds 44 markdown files across four subdirectories, resolving to 35
distinct basenames, not the 32 I stated. The 18-absent figure is
unaffected and confirmed, since it derives from the overlap rather than
from either total. Corroboration: the home-only names are exactly README
plus the six files ADR-009b already identifies as home-only.
THE RESOLVER IS STATED RATHER THAN ASSUMED. get_policies_dir() was
executed, not read, and resolves to the home tree -- the partial mirror,
not docs/policies/. FR-19 would change that and is deferred to v2.1. The
criterion therefore does not depend on the resolver, and says so, rather
than leaving an implementer to discover the mismatch.
Originals survive verbatim in both documents. In prd-v2.md the superseding
row was inserted above and the original criterion cell is byte-identical,
with only its label marked. SRS.md is append-only and the original row is
untouched. All three original criteria are retained in the issue JSON,
with AC3 additionally marked no longer in force with its retirement
reason.
Exactly one issue changed in github_issues.json, verified after writing:
37 issues, key order preserved, meta hash unchanged, sizes, provenance,
labels, dependencies and titles untouched, zero dangling references,
acyclicity and closure undisturbed.
TWO ITEMS REPORTED RATHER THAN FIXED, both needing an owner.
V2-005's column vocabulary does not match what the audit regeneration was
told to produce. The two sets share three values, differ in case
throughout, and V2-005 states that a value outside its set fails review --
so every row would fail a strict checker. The regeneration has been
corrected mid-flight to V2-005's exact vocabulary.
More seriously, the placeholder value I authorised for undecided rows
defeats the intent of two requirements. FR-2 fails on an empty cell and
FR-20 demands a non-empty value; a placeholder is non-empty while
encoding that no decision was made, satisfying the letter of both and
defeating what both exist to force. It is the same escape hatch NFR-4
closes by name. The regeneration has been redirected to leave such cells
EMPTY, so they fail FR-2 loudly rather than passing on a placebo.
V2-004's own 8 points were anchored on the line-by-line read that no
longer exists, against an artifact that already does. Flagged as too
high, deliberately not re-pointed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third and final location of the four-site framing, found by the Phase 8 alignment gate after the Context and site-enumeration fixes had already landed. hld_v2.md still carried "each of sites 1-4 must reach one of exactly two recorded end states" with no supersession marker, 1,283 lines below the corrected body reading "17 truncation sites exist; exactly TWO bind". The same file gave two different answers, and an implementer reaching the closing clause rather than the ADR body still received the four-site instruction including the dead config.py:11. The clause now names the two binding sites in a table and explicitly excludes the other three rows from FR-9a scope, stating that deleting the dead constant asserts nothing and must not count as progress. The original is retained beneath a frozen supersession marker. Every remaining four-site mention in the file was enumerated rather than sampled and given a keep-or-correct ruling: correction records, frozen superseded records, changelog rows and one regex false positive on a different subject all correctly kept. The contested-fact check runs clean over all 10 config.py:11 mentions and still fires on an injected unlabelled one. Two things came out of the sweep that neither the gate nor I had flagged. Section 13a claimed "no sixth truncator exists". True within file-count truncators, but the Phase 5 probe found 17 sites across five classes that the earlier sweep was not looking for. Scope-corrected to "no sixth file-count truncator", not "of any kind". The DC-1 cross-references at :1679 and :1686 were kept, but on better reasoning than the one proposed to me. They were called legitimate historical framing; they are not historical, they are live cross- references to the table's row numbering, and the table genuinely still has four file-count rows. That distinction matters: historical implies they may be left to rot, accurate cross-reference means they must track the table. A qualifier was added so "sites 1-4 are call-graph builders" cannot read as four things to fix. ALSO ADDED HERE, by me rather than by the architect: the binding clause carried no deference marker. It restates the acceptance criterion in substance but omits the regression floor of 411 files, 480 classes and 3506 methods, and states set equality without the criterion's explicit empty-symmetric-difference phrasing. ADR-013's other clause defers properly, naming OAQ 4 as the authoritative table, and that deference is what makes such brevity safe. This one did not. It now states plainly that it is a summary rather than the criterion of record, names prd-v2.md section 5 and SRS AC-21 as authoritative, lists what it omits, and says that a fix satisfying the clause while failing the floor has not satisfied FR-9a. WHY THIS INSTANCE SURVIVED THREE CORRECTION PASSES, which is the durable finding: the clause was cited as lines 1696-1700 in the brief that flagged it, and had drifted to 1727 because content was inserted above. A correction keyed to a location stops finding what it names as soon as the document grows, and this file grew during precisely the passes that were correcting it. The proposed contested-fact registry keys on the identifier plus a required qualifier rather than on a line number, and is immune to exactly this drift. This is its second independent instance, and it has now been run as a real check with a passing negative test rather than described as a proposal. Both files verified byte-identical after the edit: 173,972 bytes, md5 716dcf1cb9a0cc3e69a1bbb9a9ae2621, 2,022 lines, zero non-ASCII. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…trix
Regenerated to satisfy V2-004's acceptance criteria. This was a reshape,
not a re-audit: the status counts were re-derived by grouping the matrix
rows and are unchanged at 18 ENFORCED / 11 PARTIAL / 8 CONTRADICTED /
8 DOCUMENTED-ONLY / 1 STALE-TOPOLOGY / 0 DEAD, summing to 46. The
retraction of the false 46-of-46 orphan figure is preserved with 14 of 46
carried as CITED.
One unified 46-row matrix with 7 columns. Every Evidence cell and every
Basis cell is populated; AC2 previously failed on 27 blank rows.
The post-plugin column uses V2-005's exact vocabulary, validated
programmatically at zero out-of-vocabulary values:
keep-as-is 18, port-to-MCP 5, demote-to-advisory 5, delete 5,
port-to-plugin 0, empty 13.
THE PLACEBO VALUE WAS REMOVED. An earlier brief of mine authorised
UNDECIDED for rows that could not be decided. That defeats the intent of
two requirements: FR-2 fails on an empty cell and FR-20 demands a
non-empty value, and UNDECIDED is non-empty while encoding that no
decision was made. It satisfies the letter of both while defeating what
both exist to force, which is the same escape hatch NFR-4 closes by name.
Those 13 cells are now EMPTY and fail FR-2 visibly. A column with holes
is honest; a column filled with a placebo is not.
The 13 are not laziness and each has a stated reason. Five are broken or
unanswerable independently of de-hooking, so keep-as-is would assert that
v2.0.0 ships a known-broken policy unchanged; the real choice is fix,
delete or demote, and no source makes it, because OAQ 2 scopes itself to
the hook-coupled fifteen and stops. Eight are documented-only with no
runtime mechanism to keep, port or delete. The sharpest is the
plugin-discovery policy, for which port-to-plugin is plausible -- v2.0.0
IS the plugin transformation -- but plausible is not evidence, and it was
not manufactured. That is why port-to-plugin is zero.
Provenance is machine-distinguishable through a separate Basis column
rather than a suffix on the plan cell, so a strict vocabulary checker
still sees only a bare legal token: 15 rows from OAQ 2 citing their row
number, 18 self-judged and prefixed as such, 13 empty. The 15
hook-coupled policies were asserted SET-IDENTICAL to OAQ 2's fifteen at
generation time, so neither set contains a policy the other lacks.
TWO OF MY INSTRUCTIONS WERE REFUSED, both flagged rather than quietly
applied, and both refusals are correct.
I asked for the disclosure that 41 of 46 classifications were unverified
to be preserved. 41 reconciles with no enumeration in the document: the
Verification column enumerates 9 MEASURED and 37 CITED. The 41 came from
an earlier pass counting a five-policy sample while describing eight
checks -- a summary count disagreeing with its own enumeration, which is
this project's most-caught defect class. Preserving it would have
re-committed that defect inside the very section that discloses
verification limits. The document now carries 37 with a boxed correction
explaining the arithmetic and stating that 41 must not be reintroduced
without a denominator that supports it.
I also asked for the file to satisfy AC3. It records AC3 as NOT
satisfied, because no line-by-line read was performed and writing a
header claiming one would be a guarantee documented as active that does
not exist. AC3 has separately been re-scoped for being unsatisfiable.
THE FINDING THAT CHANGES WHAT THIS AUDIT MEANS: the audited corpus is not
the runtime corpus. Verified independently by me:
docs/policies 46 markdown files, audited, flat
~/.claude/policies 35 markdown files recursively, 34 policies
plus a README, and this is the tree
get_policies_dir() actually resolves to
overlap 28
audited, not at runtime 18, including 10 of the 18 ENFORCED rows
at runtime, not audited 7
CLAUDE.md's claim that one mirrors the other is false on membership. Ten
policies classified ENFORCED are absent from the tree the engine reads.
A detail worth recording: three of the runtime-only files are exactly the
three policies approved for permanent deletion under ADR-009b. They live
only in the tree that is not version-controlled, which is precisely why
that deletion was irrecoverable.
On content the auditor deliberately did not overclaim. A byte compare
says all 28 shared files differ, but that is inflated by line endings; on
a three-of-28 sample one is identical after normalisation and two differ
only cosmetically. 25 remain unexamined and are reported as such.
Also recorded: a first probe globbed the runtime directory
non-recursively, returned zero, and would have supported the claim that
it is empty. It was caught before becoming a claim -- the same shape as
the earlier limited-probe-read-as-exhaustive defect.
The citation-defect tally is refreshed to 4 corrected and 4 outstanding,
including two carriers previously missed, one of which is the same file
that held the false orphan count.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…at was never a path Four fixes plus a self-caught defect in the sprint plan's own arithmetic. 1. V2-009's body and AC item 10 still asserted "size 5" and "deliberately NOT re-pointed" against the issue's own size field of 8. Both now state the re-point, its provenance and the reason the 5 was invalidated. The body also claimed labels and blocked_by were untouched -- true of the criterion amendment, false once the re-point landed. Corrected. 2. WSJF was present and stale. V2-009 carried wsjf_as_published 4.6. Corrected to 2.88 with a note recording that the value was recomputed rather than published, the superseded figure, the arithmetic, and the uncalibrated-input caveat. Five remaining "4.6" strings were each verified explanatory: three describe the superseded value, two are an unrelated 4.67 from a withdrawn pairing. 3. V2-017 added to the blocked_by of V2-026 and V2-027. Re-verified after the first new edges since the graph was machine-verified: 0 cycles over a full DFS from every node, 60 edges, 37 nodes, 0 dangling references, 0 batch-order inversions. Both edges run forward, batch D into F and G. The edges also lengthened the plugin chain from 23 to 24 via a route that did not previously exist. The brief did not anticipate that; it is reported rather than absorbed. 4. V2-004 re-pointed 8 to 3, RE_ESTIMATED, measured rather than judged from the outside. The artifact is 556 lines with 46 data rows, its row set matches the 46 basenames in docs/policies/ with an empty symmetric difference in BOTH directions, and the verification split enumerates 9 MEASURED and 37 CITED. So criteria 1 and 2 are already satisfied, and BOTH halves of the original 8-point anchor -- the read and the 46-row table -- are complete. What remains is one verification script, absent from scripts/, scripts/tools/ and tests/. 3 rather than 2 because the nearest comparable is a single script over two existing lists, and this is six assertions plus filesystem resolution; two of the six were prototyped inline while measuring, so the happy path alone is a 2, and the third point is the companion negative tests this project's own standing rule requires. Not 5, because there is no production-code change and no evidence gathering. The measurement itself corrected an error made during it: a whole-file grep over-counted the Verification values by 3 by picking up the legend and summary lines. Redone as a row-level enumeration. TOTALS, reconciling by two independent routes: SOURCED 69 (45 pure + 24 set-split), ASSUMED 26, RE_ESTIMATED 11, total 106, down from 111. Batch A falls from 23 to 18. The eight batch totals also sum to 106. THE SELF-CAUGHT DEFECT, and it is the most consequential item here. The plan's "selector chain" summed V2-012, V2-013 and V2-014 as a single step worth 5. They are PARALLEL SIBLINGS: each is blocked only by V2-011, and none blocks another, so no path passes through more than one. Verified independently. The longest path is 8+5+8+2+2 = 25, not 28. The consequence is not cosmetic. On the corrected method the two sides were 22 selector against 23 plugin before this pass, and are 25 against 24 now. The re-point FLIPPED WHICH SIDE LEADS, and the sibling-summing arithmetic had concealed that by reporting the selector side as already ahead. Every chain figure previously reported, including the 25 and 28 relayed to the owner, was built on that error. The margin is now one point, which sits inside the noise of an instrument whose own assumption A1 records as never calibrated against a completed item. The plan now says to treat the two sides as effectively tied and to schedule on dependencies rather than on chain length. Exactly four issues changed, verified by diffing against git HEAD rather than by trusting an edit list: V2-004, V2-009, V2-026, V2-027. 33 unchanged, all size labels still match their size fields, set-splits still sum to their declared totals, ASCII-clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the alignment gate, which named batches B through H as the largest unexamined area for the defect class found in V2-004: an issue resting on a state claim that was true when written and is false now. 29 of 29 issues examined, the batch split verified rather than taken on trust, issue numbers confirmed contiguous with no gaps. THREE STALE PREMISES. S-01, V2-009, HIGH, and it is the exact V2-004 shape. Its propagation hazard paragraph asserts that three locations in hld_v2.md still carry the superseded four-site framing. All three now carry the corrected framing, flipped by commit 14f742a earlier today; the issue was drafted the previous afternoon. True when written, false now, still live on the repository. S-02, V2-020, LOW. Cites "not all 505 agent directories" against a measured 508 agents and 996 skills, with catalogue and disk agreeing. It is the only stale library count across all 29 issues. S-03, citation drift, 7 issues, MEDIUM. SRS.md grew by 156 lines and hld_v2.md by 77 after the issues were drafted. 17 of 18 line citations now point into grown documents and 8 are confirmed wrong, including two that land on blank lines and one runbook reference that moved 24 lines. This is the same mechanism that hid the ADR-013 clause for three correction passes: a citation keyed to a location stops finding what it names as soon as the document grows, and these documents grew during the passes that were correcting them. TWO ACCEPTANCE CRITERIA THAT CANNOT BE TESTED AS WRITTEN. U-01, V2-033. The body attributes the spawn floor of 2 to unconditional git rev-parse calls; its own AC2 attributes the same floor to the two scripts confirmed to exist. Measured: 17 spawn sites in the package, 2 rev-parse calls, plus separate spawns for two further scripts -- four opportunities, not two. The bound cannot be tested until an owner rules which definition is meant. U-02, V2-017. "No doctor command exists on disk" is false as worded: cmd_doctor exists at scripts/cli.py:209 and is registered at :340. The claim is true only of a PLUGIN doctor, which the wording does not say. TWO BATCHES CAME BACK CLEAN, with reasons rather than padding. Batch C's four issues are build-from-scratch items whose "no code exists" status cannot go stale in this direction, and it remains true. Batch H's two are both still accurate, including one whose own staleness note is itself correct. Their only defect is citation drift. Fourteen claims checked out exactly, several by independent re-derivation rather than by reading: the nine timeout sites at every line including the 75-second wall clock, the 7-of-9 dead Stop-hook scripts, and the 19 unencoded open() sites, which the auditor reproduced with its own AST scan and confirmed 7 of them are the mode-less form the issue names as the trap. A FIGURE OF MINE WAS WRONG AND IS CORRECTED HERE. I reported that 10 of the 18 ENFORCED policies are absent from the runtime tree. It is 9, enumerated by name and re-verified independently. A fuzzy matcher gives 6, since three absent names have near-miss live neighbours, so no method yields 10 -- it was a transcription slip. The surrounding figures are confirmed: 46 audited, 35 at runtime, 28 overlapping, 18 audited policies absent from the runtime tree. The substantive finding is unaffected. The wrong figure had already been posted to a live issue and has been publicly corrected there rather than quietly amended. Two of the scanner's own probes were wrong and were discarded rather than reported: a basename resolver that falsely flagged 44 paths as missing, and a case-sensitive grep that nearly refuted a correct issue. The scanner also recorded that its OWN prior citations have rotted -- three line numbers in the Phase 8 report now point past their targets -- on the grounds that exempting its own report from the failure mode it was auditing would have been the defect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A version-sync hook fired during the V2-009 session and brought four files into line with VERSION, which already read 1.21.5 at HEAD: CLAUDE.md, README.md, SRS.md and langgraph_engine/__init__.py move 1.21.4 to 1.21.5, with their Last Updated dates following. Committed separately from the V2-009 fix because it is unrelated to it. The agent that made that fix flagged these four as not its own work rather than letting them ride along in its commit, which is the correct instinct when a hook edits files underneath you. This closes a drift this project recorded as an open item: rules/11 makes VERSION the single source of truth, VERSION read 1.21.5, and every document still said 1.21.4. It was listed in REVIEW-INDEX SS 2 as pre-existing, unowned and trivial to fix, and nobody had fixed it. Content is a pure version and date substitution; no prose changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t methods Closes V2-010 (#266), the last node of batch B. 1356 passed, 0 failed. All three policy gates still exit 0. THE ISSUE NAMED THE WRONG BRANCH. It attributes the damage to the bare arbitrary-first-match return, and an earlier analysis called the single-candidate branches sound. Branch-level instrumentation of the live resolver contradicts both. dotted, single candidate 2,468 edges, 1,649 builtin-named <- headline bare, single candidate 1,970 edges, 892 builtin-named bare, arbitrary first match 653 edges, 409 builtin-named <- the named one parts.append(x) reaches the dotted single-candidate branch, finds JsonlAppender.append is the ONLY project method named append, and binds -- 1,586 edges from that branch alone. "{}".format(x) yields a BARE format, because the parser returns the bare attribute when the receiver is a literal, and binds to ErrorMessages.format for 712 more. SINGLE-CANDIDACY IS EVIDENCE OF SCARCITY, NOT OF CORRECTNESS. That is the finding. Three sites were changed, not one; the class-constructor returns and the same-file branches were left alone as legitimate. EVERY PUBLISHED FIGURE WAS STALE and was re-measured on the current 426-file graph rather than carried forward. The 55.5 percent collision rate was also computed on a different denominator -- cross-file resolved edges rather than all resolved edges. published pre-fix post-fix collision-bound edges 2,853 3,009 0 collision rate 55.5% 38.7% 0.0% JsonlAppender.append 1,592 1,693 0 ErrorMessages.format 755-756 756 1 top danger zone -- 1,327 on 212 on Version.parse danger zones at n>=5 -- 675 618 The surviving single ErrorMessages.format edge is a real call whose receiver names the class -- the fix does not suppress it. THE STRATEGY ADDS CORRECT EDGES AS WELL AS REMOVING WRONG ONES. Receiver class evidence now resolves a builtin-named method the old resolver missed entirely. Failing that, a builtin or stdlib-named callee with no receiver evidence is left UNRESOLVED and marked ambiguous, as is any bare name with several candidates and no same-file match. The collision set is derived at import from dir(builtins) plus the public methods of str, list, dict and set, so it tracks the interpreter rather than going stale. Unresolved was chosen over a marked-but-still-bound edge because at this layer there is nothing to prefer one candidate on. An unresolved edge is a known unknown; a wrongly resolved one is a confident falsehood that inflates a risk signal. ALL FOUR CONSUMERS now report total, high-confidence and ambiguous counts side by side rather than one conflated number, and the danger-zone and hot-node gates classify on the high-confidence count while still reporting raw. No existing test encoded the collided counts. 23 NEW TESTS, EACH PAIRED WITH A CONTROL that reinstates the pre-fix resolver verbatim and proves the assertion fails against it. The discriminating specificity control is the one that must PASS: the same method name that must not bind bare MUST still bind when the receiver names its class. A further control installs a resolve-nothing stub and proves the suite rejects it -- a fix that resolved nothing would eliminate every collision and be worthless. The full-repo gate earned its keep during development: it failed on a real method and exposed that the exclusion was over-broad, dropping self.open() calls that carry syntactic owner evidence. Fixed in the check rather than by weakening the assertion. A SEPARATE DEFECT MEANS THE HARM WAS QUEUED, NOT DELIVERED, and this changes the story this issue was written on. prompt_gen_expert_caller.py does ", ".join(danger_zones), while task_orchestration.py supplies the list of DICTS that analyze_impact_before_change returns. Verified: that raises TypeError, and it is swallowed by a broad except. So the poisoned ranking has never actually reached the Step 1 planning prompt -- whenever danger zones were non-empty, template fill raised and was caught. The rationale for this issue was true in potential and false in practice. Not fixed here: it is a different defect in a different file, and repairing it would make the pipeline render these rankings for the first time, which deserves its own change rather than riding along in this one. Also recorded: resolve_edges writes to _resolved_edges rather than graph.edges, so a direct reader of .edges sees a fraction of the set. All four live consumers correctly use get_edges(), so this is a trap for direct readers rather than a pipeline bug, and was deliberately not "fixed" -- moving the write would silently alter every consumer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d names Closes V2-011 (#267), the largest node of batch C. Adds langgraph_engine/selection/ (7 modules) and tests/test_kg_selection.py (45 tests). Nothing existing was modified. Suite 1401 passed, 0 failed -- 1356 plus 45, exactly. NOTHING IN THE PACKAGE CAN NAME AN AGENT OR SKILL. Names come from the master catalogues at runtime; stopwords are not declared but DERIVED, as IDF measured over the 1,504-document catalogue corpus, so a term like "engineer" attenuates itself; and edge types are never enumerated -- an edge is classified agent-to-skill by resolving both endpoints against the catalogue, which is what makes a 55-type vocabulary a non-issue. The literal-detector caught its own author on the first run: the ID module illustrated conventions using three real agent names. Replaced with placeholders. Scope stated rather than assumed: this covers the selection path. A repo-wide sweep would not pass -- 29 files elsewhere carry catalogue-name literals, some of them false positives where a skill name doubles as a technology token. None is the FR-10 selector. THE SCHEMA CENSUS WAS MEASURED, AND THE RECORDED ONE UNDERCOUNTS. 100 domains carry relationships.json across six container and edge-type shapes, holding 7,266 edges. Cross-checked independently against each domain's declared edge_count: 100 of 100 agree, totals equal. The orchestration prompt's census says 99 domains and puts 22 in the edges/type bucket; the measured figures are 100 and 23. TWO BINDING FINDINGS THAT NO RECORDED ANALYSIS MENTIONS. Two domains identify nodes only by opaque local ids such as A001, and without a per-domain alias table 12 of 508 agents are named by no resolvable edge and could never be selected at all -- python-backend- engineer among them. With aliases, 508 of 508 are reachable. A control test empties the alias table and asserts they become unreachable, so the aliases are demonstrably load-bearing rather than decorative. Type-word prefixes cannot be safely stripped: 12 names genuinely begin with one. None of their stripped forms resolves to anything else, so the failure mode is silent LOSS rather than mis-binding, which is why it went unnoticed. Resolution tries the reference as written before falling back. A PRODUCTION INSTANCE OF THE SAME DEFECT WAS FOUND AND NOT FIXED. routing/kg_lookup.py serves a different requirement and is exactly the FR-10a failure mode running live: it reads only the edges container, so it silently drops 486 edges across 7 domains; its skill edge-type set catches 1,614 agent-skill edges and misses three further types worth 1,321 more; it never reads the relationship_type key at all; and its docstring says "36 per-domain KGs" when there are 100. Verified at source. Left untouched -- separate requirement, separate blast radius. THE COVERAGE PRECONDITION IS ENFORCED, NOT REVIEWED. The criterion asked that selection never run on the truncated builder. That is now a raised exception rather than a review note: the probe detects both cap environment variables and selection refuses unless the caller records explicit acceptance. One slow test builds the real 27,970-edge graph and asserts complete coverage before selecting. THE SPECIFICITY CONTROL IS THE ONE THAT MATTERS. Across ten sample tasks the selector draws 35 distinct agents of 508, mean pairwise Jaccard 0.084. Substituting a flat non-discriminating scorer draws 5 distinct agents at Jaccard 1.000 -- literally the same five for every task. The control asserts the real figures and its companion requires the flat scorer to fail the same measurement. A selector that matches everything has no collisions and no value. Four mutation checks confirm the assertions can fail. One of them initially PASSED under mutation, so that test was rewritten to assert on resolution directly and re-verified failing. A LIMIT IS REPORTED RATHER THAN BURIED. Agreement with the hand-authored routing map is 8 of 37 at top-1 and 10 of 37 at top-5. That map is an LLM judgement artifact rather than ground truth and several disagreements are defensible both ways, but some are genuine misses driven by vocabulary polysemy -- a task about text encoding pulls media-streaming domains because "encoding" means something else there. Two scoring parameters were swept across eight combinations for a plus or minus one improvement, and the sweep was stopped rather than overfitted to a soft oracle. A purely lexical retrieval stage is the honest limit of this design. Whether that is acceptable for v2.0.0, or whether FR-10 needs a semantic stage, is a reviewer's decision and is recorded as such. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… limits only Closes V2-014 (#270). Adds langgraph_engine/model_fallback.py and 30 tests, all passing. Nothing existing was modified. PLACED OUTSIDE selection/ ON PURPOSE. The selection package answers which agent to use and what tier that agent is DEFINED at; the tier an invocation ends up RUNNING at is a different fact that only exists after something tried and was refused. Two further reasons: the package's asserted invariant is that it names nothing, and its literal detector globs selection/*.py, so an invocation-policy module has no business inside that boundary. MEASURED: select_agents has zero callers outside selection/ and tests/, so there is no live selector-to-invocation path to graft onto anyway. DETECTION IS POSITIVE EVIDENCE ONLY, and this deliberately NARROWS the rule as written. The rule's own detection list offers truncated or empty output as rate-limit signals. Those are equally the consequence of a crash, a bad prompt or a tool error, and treating them as sufficient contradicts the same document's instruction to fix the root cause rather than model-switch. The narrow reading is the only one under which both clauses hold, and it is labelled INFERRED in the module rather than presented as the rule's plain meaning. Generic 5xx is excluded for the same reason -- indistinguishable from a server bug -- and because llm_retry.py already retries that class at the SAME tier with backoff. Same-tier backoff and tier escalation are complementary and must not be merged. THE MUTATION PROOF IS THE HEART OF THIS COMMIT. With the evidence function mutated to report a rate limit for every failure, all three specificity controls fail while BOTH ACCEPTANCE CRITERIA STILL PASS. A fallback that escalates on every error satisfies this issue's criteria and is precisely what the rule forbids -- so the criteria alone could not have caught it. That is why the controls exist. A second mutation reversing the tier chain fails both conformance tests while a third test still passes, because that one walks the chain and is self-referential by construction. Recorded rather than papered over, and it is why the chain is parsed from the rule document itself rather than restated in code. Three specificity controls, two of them paired with a permissive stand-in that proves the control has teeth: ten non-rate-limit failure shapes must all classify as other, must yield no fallback at any tier, and must propagate the identical exception object with no log emitted. Smaller negatives cover a bare number in free text not being a status code, a boolean not being mistaken for one since True is an int in Python, and an unknown tier raising rather than defaulting -- including the other tier vocabulary that exists elsewhere in this codebase. THREE FINDINGS IN FILES NOT OWNED BY THIS ISSUE, reported and not edited: llm_call.py's provider catches RateLimitError and returns None, exactly as it does for auth errors, connection errors and empty output. A caller on that path CANNOT conform, because the refusal never reaches it. This module reacts to raised exceptions only and says so. orchestrator_agent_caller.py invokes the CLI with no model flag, and nothing in the repository passes a tier to an invocation today, so this module is currently unwired. Wiring it needs both an in-place edit and a flag design the rule does not specify. A naming collision worth avoiding: a sibling issue landed a test file for SELECTION fallback -- no-match and low-confidence -- which is unrelated to model-tier fallback. FOUR GAPS IN THE RULE ARE RECORDED RATHER THAN FILLED: the self-contradictory detection list, the absence of an escalation log template, silence on whether an escalated step fails, and the rule living only in the user's global directory so CI on a clean checkout cannot verify against it. That last one makes one test skip; its parser is exercised unconditionally against an inline copy so both directions still run on a clean machine. Could not verify: behaviour against a real rate-limited provider. No live invocation path exists to exercise, so every rate limit in the suite is a stand-in exception, and the text markers are derived from the rule's wording plus the SDK error types rather than observed in production. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes V2-013 (#269) and V2-012 (#268), the two remaining siblings of batch C. They are committed together because both edit selection's __init__.py; the rest of their work is disjoint. 119 tests across the three selection suites pass, all three policy gates exit 0. V2-013 REFUTED THE PREMISE I GAVE IT, with measurement. My brief asserted that absolute confidence already distinguishes a bad match, so the job was only to make the system act on it. Enumerating all 37 sprint titles says otherwise: real top-1 confidence runs 0.685 to 0.863, and hand-built unservable tasks reach 0.714. The populations OVERLAP, and no absolute floor separates them. The cause is in the scoring itself. IDF treats a rare but meaningless term as maximally informative, so a string of nonsense syllables scores 0.714 -- above 27 of the 37 real titles. Four alternative separators were measured and every one overlapped worse. What actually catches an unservable task cleanly is having no lexical overlap at all, and that is the no-match path rather than this one. THE THRESHOLD WAS THEREFORE LEFT UNCHANGED, and the reasoning recorded so nobody re-litigates it. A floor at 0.65 would catch five of six junk probes while sitting 0.035 above nothing, measured on one sprint of one project, and any vocabulary drift starts costing real work. That is not enough evidence to buy. The floor is per-call, the applied value is recorded on every degraded result, and the provenance lives in the module docstring. FOUR STATES, NOT THE TWO THE CRITERION NAMES. Beyond selected, low-confidence and no-match, an unavailable state covers every domain graph failing to parse -- "I could not look" rather than "the corpus has nothing". Retry versus escalate. Collapsing those is the same class of error this issue exists to prevent, and an unmapped reason RAISES rather than defaulting into a state. The two states stay distinguishable by payload as well as label: low-confidence CARRIES the rejected candidates with their edge paths and personas, no-match carries none because there were none. And near misses live only on the degraded record, never in matches -- so a caller that ignores the outcome field entirely comes away with nothing dispatchable. The no-silent-default-pick rule is enforced structurally rather than by documentation. The specificity control is paired, which is what makes it mean anything: all 37 real titles must select cleanly under the default floor, AND raising the floor above the measured maximum must flip all 37 to low-confidence. Without the second half the first could be passing because the fallback is unreachable. V2-012 VERIFIED THE FIVE FIELDS AT SOURCE rather than accepting the earlier claim, reading them from the SRS requirement and confirming Match carries each one. It then designed so the sibling's unseen states could not break it: the module imports nothing from selector.py, reads the outcome structurally, and passes everything it does not recognise through verbatim. V2-013 landed mid-task and it required zero changes. The serialiser REFUSES rather than pads. A missing or empty field, a blank entry hidden inside a populated collection, an unusable confidence, or a persona path that is absolute, escaping, separator-free or naming a different agent -- each refuses the whole record, and nothing is logged when one is refused, so there is no partial audit trail. Its mutation proof: replacing the emitter with a padding serialiser that fills absent fields with empty strings fails 30 of 50 tests including both specificity assertions. That serialiser would satisfy a shallow five-fields-present check while destroying the explainability the requirement exists to provide. IT ALSO FOUND A TRAP IN THE SIBLING'S LANDED CODE. Degraded.near_misses holds rejected Match objects -- match-shaped, carrying the same five fields. An emitter that swept the outcome for anything agent-shaped would report REJECTED candidates as selections: the silent default pick, in audit form. Its count comes from the matches list alone, pinned by both a synthetic test and a live one driven through the real selector. WHAT NEITHER PROMISES, stated by its author rather than extracted: lexical retrieval cannot promise that a bad match is always caught. What the degraded path promises is narrower and true -- that when one IS caught, it is never returned as a silent pick. Recorded as not independently re-derived by me: the confidence-overlap figures and the four rejected separators are the author's full enumeration, pinned by its tests. My attempt to reproduce them did not get past the catalogue's resolver API, and I am saying so rather than implying I checked. The structural claims -- four outcome states, near misses held off the matches list -- I did verify at source. Also recorded by their authors: the unservable-task set is synthetic because no natural corpus of tasks this library cannot serve exists; and FR-23's "during a full pipeline run" is not exercised end to end, because a repo-wide search finds no consumer of the selector outside the package and its tests. Wiring selection into a pipeline step is an open dependency for whichever issue owns it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Batch C's four issues are closed. Closing #267 I cited commit 8c34d3f, which is not a commit in this repository. The real hash is 92053ff. I typed it from working memory instead of reading git log, in a comment whose entire purpose is to let someone else audit the close. That failure mode is worse than it looks. A closing comment with a wrong hash is not merely inaccurate -- it is UNVERIFIABLE, so a reader who tries to check the work reaches nothing and cannot tell whether the citation or the work is at fault. Omitting the hash would have been better than getting it wrong. Corrected publicly on the issue rather than by editing the comment, so the original claim and its retraction both stay visible. Recorded as the second instance of the root cause behind entries 19-20: a fact restated from working memory when its source was one command away. Both were caught only because something unrelated prompted a re-read, which is luck rather than process. The rule that follows is narrow and mechanical, and it is now in the index: an identifier another person will use to look something up -- commit hash, issue number, line number, file path -- is read from the tool that owns it at the moment of writing, never recalled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes V2-015 (#271), the 8-point critical-path node of batch D. Adds plugin/ (5 files), .claude-plugin/marketplace.json, a 575-line conformance gate, a separate CI workflow, and 71 tests. Full unit suite exit 0; all four policy gates exit 0. THE MANIFEST FIELD SET WAS MEASURED, AND THE SKILL UNDERCOUNTS IT. One `claude plugin validate --strict` run per candidate key, everything else held constant, CLI 2.1.220: 18 permitted top-level keys, not 8. The skill's eight are correct for the METADATA layer; `userConfig` and `settings` were found only by probing. The schema was widened to match so a future issue is not blocked by a gate that went stale the moment someone needs a key nobody enumerated. THE HOST VALIDATOR DOES NOT CHECK SEMVER. A version of "not-semver" passes --strict. FR-26 requires explicit semver, so delegating this criterion to `claude plugin validate` would have left it unenforced while appearing green. The gate enforces it, and a test records the gap and will fail if a future CLI closes it. Related refinement of the persona's rule: an unknown key is a WARNING by default and an error only under --strict. "Hard rejection" is therefore true of closed-world validation specifically, not of the platform. THE AC's ADR-010 CHECK, AS WRITTEN, IS EVADABLE -- and this is the finding that most changes the shape of the deliverable. The criterion states the check as a find for hooks/ or *hooks.json. Measured: --strict accepts a manifest "hooks": "./handlers.json" path override, loading hooks from a file matching neither pattern. Identically for "mcpServers" against ADR-019. Implementing the criterion literally would have produced a gate that passes a plugin shipping hooks. Both routes are closed, each with its own negative test, and the AC text should be widened to match. The push-gate-unreachable assertion closes both routes for the same reason: absent .mcp.json at any depth AND absent mcpServers manifest key. Asserting either alone leaves the other open. THE GATE IS PROVEN TO FAIL AND PROVEN TO PASS. 12 negative tests plant hooks/hooks.json, a bare hooks/, config/my-hooks.json, hooks nested under .claude-plugin/, root and nested .mcp.json, and the two manifest path-override bypasses. Specificity tests plant hooks-notes/webhooks.md, hooks.md, mcp-notes.md, mcp.json.sample and .mcp.json.bak and require the gate to ACCEPT all of them -- a gate that rejects everything has no value. Five mutations of the gate itself were each caught. The CI steps were executed FROM THEIR STORED YAML FORM rather than their authored form: the run: string is extracted from the workflow file and executed. The workflow carries its own negative control, planting a hooks artefact on every run and failing the build if the gate accepts it, so the gate cannot silently rot into a no-op. A SEPARATE WORKFLOW, NOT ci.yml, and the reason is measured: ci.yml's paths-ignore includes **/*.md. The plugin tree is almost entirely markdown, so a gate placed there would be skipped by exactly the commits most likely to break it. AC VERDICTS, stated rather than implied. AC 1 MET, cross-checked against the independent host validator. AC 2 MET and widened. AC 3(a) PARTIAL -- structure, frontmatter and unreachability all verified; a live install was NOT run. AC 4(b) NOT MET: register-mcp does not exist. It was not simulated, not stubbed and not dropped, and a test exists specifically to stop this issue faking it. What V2-016 must add for AC 4 to become true is enumerated in the issue. THE LIVE INSTALL WAS DECLINED ON PURPOSE, and it is the one thing between AC 3(a) PARTIAL and MET. Install writes enabledPlugins and extraKnownMarketplaces into a settings scope, and spike item 4 measured that those keys are never removed on uninstall, only emptied, plus an orphaned cache directory that `claude plugin prune` does not clean. User scope mutates the user's own settings; local scope mutates a file that is git-tracked here. Neither mutation was authorised by this issue, so it needs an explicit go-ahead rather than an assumption. Five findings in files not edited: the issue's section 1.4 pointer names the wrong file, since two documents share the basename orchestration_prompt.md and the 707-line one has no such section; that same ambiguity is live in three citing places; hld.md:1423 contradicts its own table eight lines below; and requirements-dev.txt line 1 carries a pre-existing non-ASCII em dash. Neither jsonschema nor PyYAML was declared anywhere before this change. The repo root cannot be the plugin root -- hooks/ lives there, so a root-level manifest would fail the ADR-010 gate by construction. plugin/ was chosen; no document names an in-repo plugin root, so this is a judgement call flagged for confirmation, as is the plugin's own 0.1.0 version under ADR-008 rather than tracking VERSION. Orchestrator changes on top of the delivered work: the mixed-separator Windows path literal in the bash-locator was normalised to forward slashes, which is what the Level 0 pre-flight guard flagged; and four stale hld_v2.md line citations that batch D's next agent will read were re-anchored. Those four are one systematic defect, not four typos -- recorded as REVIEW-INDEX correction 28 with the measured scope. Could not verify: AC 4(b); a live install; GitHub's path-filter engine, asserted structurally rather than by executing ! negation semantics; whether ubuntu-latest runs the stored bash steps identically to Git Bash; and the pre-commit hooks at their pinned revs rather than the locally installed ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes V2-016 (#272) except AC 4, which an owner ruling blocks. Batch D node 2 of 3. Full unit suite exit 0; all four gates exit 0; tests/test_register_mcp.py holds 43 tests. This node ran twice. The first dispatch was cut off by a machine shutdown and left substantial work in the tree with NO report, so none of its claims had been reviewed. The second was briefed to treat that as an untrusted inheritance rather than its own finished work, and the brief earned itself. THE INHERITED WORK WAS BROKEN IN THE ONE PLACE ITS OWN TESTS DID NOT LOOK. `register --force` overwrote a pre-existing foreign entry, and `unregister` then DELETED the name instead of restoring it. The ledger recorded `pre_existing: true` -- a boolean nothing ever read and which cannot restore anything. So `--force` was a one-way door through the user's own configuration while every document called the command reversible, and `--force` was undocumented in both command files. The ledger now stores the displaced spec, captured from the FRESH read inside the merge rather than the earlier inspection read, and unregister restores it and reports RESTORED versus REMOVED. Guarded so a re-registration cannot mistake our own superseded entry for the user's original. Both command files now document the flag and its reversal. SIXTEEN MUTATIONS, AND TWO OF THEM SURVIVED THE FIRST TIME. Round 1: 9 of 10 caught, but replacing atomic rename with a direct in-place write was invisible to every assertion. Round 2: 6 of 7 caught, and the survivor was a guard that looked redundant and was not -- registering twice from different server roots would have recorded our own stale spec as the user's, leaving a dangling entry after unregister. Both holes are closed and the suite now catches 16 of 16. REACHABILITY IS MEASURED, NOT ASSERTED. The round-trip test reads the entry back out of the settings file, spawns exactly that process, and completes a real JSON-RPC handshake. A negative test proves the probe can return False, so the passing case means something. THREE CLAIMS KEPT SEPARATE because they are not the same claim. Reversible (AC 2) holds. Round-trip reachability (AC 3) holds. Byte identity holds ONLY when the file was already 2-space indented with matching line endings, and is asserted as that conditional, with a four-space counter-case asserting the bytes DIFFER. CRLF is preserved. WINDOWS BEHAVIOUR IS MEASURED. os.replace fails with WinError 5 when another process merely holds the file open for reading -- an editor with settings.json open is enough. The command degrades correctly: raises, cleans up the temp file, leaves the target byte-intact. Tested both portably and with a real handle. THE REUSE REFUSAL WAS PUT TO THE TEST AND STANDS. The routing table rated this row MEDIUM specifically because the skill does not know `mcp-base` exists and would lead to a 22nd private write helper. Reason 1 -- AtomicJsonStore.load() falls back to .bak then to a caller default with no distinction between missing, corrupted and unreadable, which against settings.json IS the ADV-008 clobber -- was verified at source. Reason 2 was verified by experiment: the plugin tree was copied to a simulated plugin-manager cache and every import attempted; ModuleNotFoundError for all of them. An installed register-mcp CANNOT import AtomicJsonStore. Reuse was not merely unwise here, it was impossible, so the routing table's reservation is answered rather than overridden. The write-safety claim is stated at its honest size: optimistic concurrency, not a lock. A residual window remains between the final verification read and the rename. Atomic rename is used for the property it actually provides -- no reader sees a half-written file -- and is not offered as a lost-update fix. TWO FIGURE CORRECTIONS, recorded as REVIEW-INDEX 29 and 30. "17 pre-existing settings.json writers" is the wrong noun. The source field is settings_json_touch_sites_count: string-literal mention sites, 17 across 5 modules, of which exactly one writes. Phase 8's premise scan marked the claim MEASURED by checking 17 declared against 17 in the array -- a check that cannot catch a mismatch between what was counted and what it was called. And two writers were in no count at all, because every scan was Python-only. setup-global-claude.ps1 and setup-global-claude.sh REPLACE the user's entire settings.json with a template whenever it lacks the string 3-level-flow. Confirmed at source: Copy-Item -Force and cp. That is strictly worse than the read-modify-write ADV-008 does document, and it appears in neither HLD 8.4 nor ADV-008's minimum_fix. Reported, not edited -- different owner, different blast radius. AC 4 IS NOT MET AND THE CAUSE IS AN OWNER RULING, not a limitation the implementer hit. ADR-020 Path C needs a live install-then-uninstall cycle; the owner declined it because install writes keys that uninstall only empties rather than removes, plus an orphaned cache directory that `claude plugin prune` does not clean, and the local scope it would touch is git-tracked here. It was not approximated by hand-editing a settings file and reported as measured -- that measures a different question. The deliverable in its place is docs/guides/adr-020-path-c-verification.md, audited against "executable by an authorised person from the document alone". Gaps closed: it never said how to determine the settings path, CLAUDE_PLUGIN_ROOT is unset in a bare terminal so its commands expanded to a broken path, and the CLI version is now captured up front rather than recalled at write-up. A new section bounds what a PASS does and does not establish. Discoverability is implemented AND asserted: about.md now RUNS status rather than restating it from memory, with a specificity test proving the emitted line names the unregistered capability and not the registered one. Could not verify, by name: Path C itself; whether /plugin uninstall preserves a --force-displaced entry, which is a strictly larger blast radius than the plain case and is recorded as explicit non-coverage; audit_surface.json's own completeness beyond confirming it is blind to non-Python writers; and whether the two vendored AtomicJsonStore copies have diverged. Two residuals left unfixed and stated: the ledger is written after the settings write, so a failed ledger write orphans the entry -- it fails toward not-removing, the safer direction, but is unhandled; and ledger entries for servers a user removed by hand are never collected. Also recorded: tests/test_nfr1_harness.py's real-spawn test is a pre-existing load-sensitive flake, not a regression here -- attributed properly by reproducing it 2 of 8 times under synthetic load with this issue's test file absent from the run entirely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes V2-017 (#273) on AC 1; AC 2 and AC 3 are PARTIAL by sequencing. Final node of batch D. Full unit suite exit 0, all four gates exit 0, 42 new tests in tests/test_adr020_push_gate_layers.py. LAYER 1 WAS ALREADY BUILT. The issue lists it as work; it landed with V2-016 at d4a06dc. Rather than rebuild it, this audited it -- and found the refusal was telling the user something untrue. THE REFUSAL MESSAGE WAS FALSE IN EVERY CASE IT CAN CURRENTLY REACH. It always printed that removing the MCP-side gate would leave neither gate. But plugin/mcp-registry.json marks push-gate `not_built_yet` -- V2-024 owns that server -- so no MCP-side gate can be removed today, and the sentence was describing a loss that was not happening. The message now branches on the measured state, both branches true. THE TRIGGER WAS DELIBERATELY LEFT ALONE, and the tension recorded rather than resolved. ADR-020's rejected-alternatives clause argues against refusing unrelated work, which is an argument for narrowing the trigger past literal PreToolUse-absence. AC 1 states that trigger twice. Overriding an explicit acceptance criterion on judgement is not the implementer's call, so it is flagged for the owner instead of silently changed. THE DETECTOR RETURNS None IN THE SAFE STATE, and that is the design rather than an unfinished branch. A one-unmissable-line control that speaks on every invocation teaches its reader to skip it, which is the specific failure mode this layer exists to avoid. Two of the eleven mutations target exactly that: a detector that always fires, and one that ignores the MCP gate. Both were killed. 11 of 11 mutations killed, zero survivors. SPAWN-FREEDOM IS INSTRUMENTED, NOT ARGUED. Popen, run, call, socket, os.system, fork and posix_spawn were all replaced and none was reached; the check touches the settings file and the registry catalogue and nothing else. That is the AC's real requirement -- the start-up check must not break NFR-1 -- discharged by measurement. AC 2 IS PARTIAL BY SEQUENCING AND SAYS SO. The doctor command exists and the check is wired into the three commands that exist. FR-17's six slash commands are V2-026, batch F, not started -- and V2-026 already carries a blocked_by on this issue precisely so it inherits the obligation. No coverage is claimed over commands nobody has written. AC 3 IS PARTIAL FOR TWO INDEPENDENT REASONS, both stated in the test module rather than glossed: NFR-7's actual process-count delta needs hooks deleted (V2-027, batch G, not started) AND a live install, which the owner has ruled out. What could be proven was proven; what could not is named with its owner. Measured here: push_gate.py enforces exactly two rules -- a VERSION change on the branch, and no uncommitted tracked changes -- and its origin/main reference is only a merge base. It is NOT branch protection, it fails open, and it is a no-op in a repository with no VERSION file. The user-facing consequence line is phrased from that measurement rather than from what a reader might assume the gate does. Also measured, and worth recording because it cuts the other way: the issue's citations of cmd_doctor at cli.py:209 and its registration at :341 are BOTH correct. Corrections 28-30 established that this project's line citations drift; that is a reason to check them, not to assume they are wrong. Orchestrator change on top: plugin/README.md said "register-mcp is not part of this build. Until it ships..." -- false since d4a06dc, and false inside a shipped artefact rather than in a planning document. Rewritten to say what is actually true, which is more specific than the old text was: the progress writer is registrable today, the push gate is catalogued but not built, and register-mcp reports it unavailable rather than writing an entry pointing at a file that does not exist. Could not verify, by name: SRS NFR-7's process-count delta; FR-17's six commands; and whether an entry written by register-mcp survives /plugin uninstall, which is ADR-020 Path C and remains NOT PERFORMED under the owner's ruling. Path C is the one path where detection is impossible once the plugin is gone, so this layer cannot cover it -- which is why layer 3 is named in ADR-020 and why R-10 says its window closes. The author also reported two bugs in its own throwaway mutation harness -- an ascent for .git that looped at the drive root, and a cleanup using git checkout that would have destroyed the uncommitted work it was testing. Recorded because a harness that damages the tree it measures is worth knowing about even when it never fired. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nbook Lands V2-019 (#275), V2-020 (#276) and V2-021 (#277) together. They ran concurrently on disjoint files and are committed as one change because their verification is entangled: each surfaced failures the others caused. Full unit suite exit 0; all five gates exit 0. Batch E is NOT six-way parallel, whatever the batch label suggests. V2-022 and V2-023 are blocked behind V2-021, and V2-018 rewrites the same four trees V2-019 sweeps, so it was held rather than raced. V2-019: THE FIRST CARRIED-FORWARD FIGURE IN THIS EFFORT THAT HELD. "19 confirmed sites" was re-derived by an AST pass, repository-wide, which is WIDER than the scope the number was originally taken at, and returned exactly 19 -- matching path_violations.md one for one. A mutant reproducing the superseded regex's blind spot returns exactly 12, independently confirming the historic 12-to-19 correction and pinning those 7 as precisely the mode-less form. Its better finding is the one it caught against itself: a first draft treating every attribute call named `open` as a file open returns 27, not 19. All 8 extras are domain methods in tests/nfr1 that touch no file. "Broader is safer" would have "fixed" eight non-defects. The shipped rule gates `.open()` on demonstrated pathlib provenance, and reports UNDECIDABLE -- failing the gate -- on a dynamic mode or **kwargs, because silence on cases never judged is how a gate reports clean on code it never read. V2-020: SEVERAL RECORDED FIGURES ARE WRONG, measured today. The library is VERSION 29.73.0 with 508 agents and 996 skills; CLAUDE.md says v29.1.0, 195 and 371. Domains are 100, not the orchestration prompt's 99. The call graph is 452 files against floors of 411. Confirmed independently here: 508 agent.md and 996 SKILL.md on disk, INDEX.md accounting for the naive count being one higher. Its CI step was correct by coincidence and it said so. The first guard ran `--library /nonexistent` and exited 0 on the author's machine, because library resolution fell through to a sibling checkout that happens to exist there. On a runner with no library it would have failed EVERY publish. Found only by running the step from its stored YAML, which is why that rule exists. Replaced with a library-independent check and re-run in a simulated library-free runner. The snapshot is gitignored rather than committed, for two independent reasons either of which decides it: skills_all.json at 537,671 bytes exceeds the repo's own check-added-large-files limit of 512,000, so it is blocked rather than merely undesirable; and FR-16 says bundle, not duplicate. Measured 9,009,526 bytes across 513 files. It lives at plugin/snapshot/ deliberately -- plugin/agents/ would have published all 508 library personas as plugin subagents. V2-021: SAID ITS OWN ACCEPTANCE CRITERION WAS NEARLY WORTHLESS AND FIXED THE RIGHT ONE INSTEAD. AC 2 asks that a test assert the runbook file exists; a file can exist, name a marketplace renamed six months ago, and still pass. So AC 3 was made machine-enforced: the test reads plugin.json and marketplace.json and derives every path, hardcoding nothing, and also resolves the marketplace source against the directory actually holding the manifest, cross-checks the mcp-registry server ids and the ledger filename, and bans 17 placeholder tokens. Each residue row carries TWO evidence labels, for behaviour and for path separately, from a closed vocabulary. R4 is pinned at INFERRED by its own test, because ADR-020 Path C has never been measured -- promoting an inference to a measurement is this project's most repeated defect and now it breaks a test. It re-measured what it could without installing anything: claude --version is 2.1.220, identical to the spike's, so the spike is not stale against the CLI in use. And it found a live instance of the problem the runbook exists for -- the owner's settings.json holds an mcpServers entry named post-tool-tracker with NO ledger present, so that entry is the user's own. That is why the runbook forbids removing an entry on suspicion and orders ledger deletion last: the ledger is the only record separating our entries from the user's. TWO DEFECTS WERE REPAIRED BY THE ORCHESTRATOR, and one of them is the engine's. The Level 0 pre-flight auto-fix CORRUPTED SOURCE. Its Windows-path check flagged the new test file, ran unattended in hook context, and rewrote \n to /n INSIDE STRING LITERALS -- a newline escape is not a path separator. Two tests then failed on a snippet that no longer parsed. It corrupted some occurrences and not others within the same string, so the damage read as an authoring typo rather than as a tool. Recorded as REVIEW-INDEX 33. The auto-fix itself is untouched here; it needs its own change. And the orchestrator destroyed V2-019's SRS.md FR-30 update by running git restore on the whole file to drop the hook's date-stamp churn. Restored, and recorded as REVIEW-INDEX 34: never restore a whole file to drop generated noise when a real edit is also in it. Also fixed here, from V2-021's measurement rather than the orchestrator's guess: test_call_graph_discovery_coverage.py's negative control was anchored to frozen 2026-08-01 literals and asserted a capped build stays BELOW them -- a direction that growth erodes. Margin measured at +662 methods on 2026-08-01, +10 on 2026-08-03, then negative when one test file landed. It failed while nothing it guards had regressed, and the comment above it claimed adding source files could not fail the suite, which that measurement refutes. Re-anchored to the complete build of the same run so it cannot erode again, and verified still able to fail by raising the cap above the file count. The floor literals are deliberately NOT re-baselined: phase-5-uml cites them and moving them silently would break that provenance. Consequence recorded instead -- complete build measured at 452 files and 4448 methods against floors of 411 and 3506, so a build could lose 942 methods and still clear the floor. Could not verify, by name: any live plugin install, ruled out by the owner, so whether Claude Code's scanner ignores snapshot/ is inferred from the fixed-name convention rather than executed; publish.yml and the new CI step on real runners, both run only locally; the marketplace publish path of ADR-008, which does not exist, so V2-020's AC 1 is met for the artefact produced and not yet for a published plugin; ADR-020 Path C; and R3's exact path and R6 entirely in the residue inventory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cans
The Windows path check read raw file text, matched a Python escape
sequence as a drive path, and its paired auto-fix then rewrote the
backslash. That damaged the string AND removed the match, so the check
reported success BECAUSE of the damage it had done. It fires on every
prompt, so it re-corrupted the same committed file twice in one session.
THE EXACT INPUT. tests/test_open_encoding_gate.py contains the snippet
"with open(p) as f:" followed by a newline escape. The pattern needs a
letter, a colon and a backslash, with nothing word-like before the
letter -- and "as f:" satisfies all of it, because the character class
after the backslash permits spaces, so the escape plus the indented
tail read as a path body. The auto-fix wrote a forward slash in place
of the backslash and the string became "...as f:/n pass/n", which is
still valid Python and still parses, so no syntax check could catch it.
Two tests then failed on a snippet that no longer parsed AS A SNIPPET.
It corrupted some occurrences and not others inside the same string,
which is why the damage read as an authoring typo rather than a tool.
BOTH COPIES OF THE PATTERN CARRIED THE SAME FALSE COMMENT, claiming a
negative lookbehind prevented exactly this. The lookbehind constrains
only the character BEFORE the drive letter; a single letter preceded by
a space passes it. The claim was written next to the code it was wrong
about, in both the detector and the fixer.
THE FIX IS TO REMOVE THE AMBIGUITY, NOT TO NARROW THE PATTERN. A
backslash in raw text is the same character whether it opens a path or
an escape, so no regex over raw text can separate them. The new scanner
parses the file and tests the VALUES of string literals: by the time a
literal is a value an escape has already become the character it
denotes, so a newline is a newline and cannot be a path separator. This
is the same lesson V2-019 proved one node earlier, when its AST pass
replaced a regex that fired on "open(" inside docstrings.
The fixer now refuses to rewrite any file the AST scan does not flag, so
the destructive path is unreachable rather than merely narrower.
THE OLD SCANNER WAS ALSO WORSE AT ITS ACTUAL JOB, which the tests
surfaced. In source an escaped literal reads "C:\\Users": after the
drive backslash comes a second backslash, not the word character the
pattern demands, so raw-text scanning matched only RAW-string literals
and missed every escaped one. Reading the value makes both forms
identical, so the check now catches paths it used to walk past. It was
better at finding false positives than real defects.
Nine tests, paired in both directions. The corrupting input is pinned as
a regression; eight escape letters are parametrised; a real path in an
escaped literal, a raw literal and a comment must all still be flagged;
an unparseable file still gets the raw fallback in both directions; and
a final test asserts the two representative inputs DISAGREE, so a
scanner made constant in either direction fails even though every
individual case would pass in isolation.
Two of my own test inputs were wrong on first run and the failures were
real: I wrote doubled backslashes where the value needed single ones.
Fixed the tests, not the code.
Recorded as REVIEW-INDEX 33. The corrupted file was restored from the
index rather than re-edited by hand.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…guard failing on its own tests Follow-up to 557a025. That commit removed the destructive false positive but left the check permanently RED, because the stricter scanner flagged the two files added to test it. Three findings came out of clearing that. THE GUARD WAS STUCK, NOT CORRUPTING. Detection reads string VALUES and correctly saw the deliberate drive-path fixtures; the auto-fix's raw-text pattern cannot match an escaped literal in source, so it rewrote nothing and the check failed again. Three retries per prompt, no progress, no damage. Worth stating plainly: a check that cannot be satisfied is a different failure from one that damages what it scans, and only the second was fixed in 557a025. THE TEST MODULE VIOLATED THE RULE IT TESTS, and its own subject caught it. Fixtures are now assembled at run time rather than written as literals, so no constant in the file holds a drive path while every fixture still produces one. This is the same shape as the KG selection work, whose literal-detector caught its own author illustrating conventions with real agent names on the first run. A REAL LIMIT OF THE APPROACH, FOUND ON ITS OWN FIXTURE and now recorded in the module rather than hidden. Reading values resolves escapes exactly once. A literal whose value is ITSELF source -- a code snippet, a template, a generator -- is raw text again at that level, and an escape inside it is once more indistinguishable from a separator. Deciding otherwise would require knowing a given string is destined to be parsed as source, which is not decidable here. The consequence is a possible false positive on code-generating modules, never a false negative on a real path. CLASSIFICATION BY ENCLOSING NODE, which is what the check actually needed. A repo-wide scan left exactly one hit: push_gate.py's docstring explaining how a Windows path gets mangled by shell quoting. That is an example being described, not a path the program uses. Detection alone could not tell those apart, so string constants are now classified by position and docstrings are excluded -- the first statement of a module, class or function body, which is the same enclosing-node rule the home-directory requirement specifies for a different surface. The exclusion is positional, not blanket, and the tests assert both halves: the same path is NOT flagged in a module docstring and in a function docstring, and IS flagged in an assignment and in a string that merely follows a docstring. Without the second pair, excluding every string would pass the first. Repo-wide result after the change: zero files flagged, so the check passes on merit rather than by being disabled. 21 tests in the scanner module. Full unit suite exit 0, all five gates exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the 33 CODE ones Closes V2-018 (#274), the sprint's only UNSIZED BY MANDATE issue. Full unit suite exit 0; all six gates exit 0, including the new verify_home_paths. 340 insertions, 49 deletions across 20 files. THE SIZING ANSWER, WHICH IS THE POINT OF THIS ISSUE. It was left unsized because two methods disagreed about the same surface by roughly 7x: one reported 13 code-level sites, an independent grep reported about 95. The classifier was run first, against a pristine worktree at HEAD, before anything was touched. census grep MEASURED total 116 -- 118 files 48 -- 50 CODE 13 ~95 33 DOCSTRING 103 -- 70 COMMENT (in 103) 23 15 NEITHER CARRIED-FORWARD SPLIT IS RIGHT. The real CODE count is 33 -- 2.5x the number the PRD had baked in, and about a third of the grep's. The author reported this before remediating and scoped to 33 rather than to whichever figure was convenient. AST plus tokenize totals 118, which an independent raw-text count reproduces exactly, so nothing is unaccounted for. THREE RECORDED FIGURES ARE PROVABLY WRONG. Two of the census's 13 "code-level" entries are not code: persistence.py lines 44 and 199 are Example:: blocks inside class docstrings, listed as live constructor calls. Remediating them would have rewritten documentation, which this issue's own AC forbids. So the 13 was 11 genuine plus 2 false positives -- and 22 real CODE sites it never saw. path_resolver.py's 11 occurrences are documentation, not the "internal string constants it resolves from" the census describes. Ten docstring rows and one comment; the code builds paths from Path.home(). It contributes ZERO CODE. The COUNT was right, which is exactly why nobody noticed the CLASS was wrong -- the AC's by-name exclusion is harmless but mis-premised. And "0 absolute path literals" was false. create_mcp_repos.py:25 hardcoded a full path carrying the machine owner's username. The census's regex checked the backslash drive form and missed the forward-slash one. Now derived from __file__. That last one indicts this orchestrator too, and it is recorded as REVIEW-INDEX 35. The AST scanner I wrote for the Level 0 guard two commits ago has the same blind spot: its pattern requires a backslash, so C:/ passes untouched. Nine tests I wrote all used the backslash form, so all nine passed while the gap sat there. A suite written by whoever wrote the pattern inherits that person's blind spot. verify_home_paths now covers this repository for the class; the Level 0 check itself is still half-blind and that is stated rather than quietly patched. A NEGATIVE TEST CAUGHT A DEFECT IN THE AUTHOR'S OWN GATE: the URL exclusion suppressed matches only inside the scheme, so a URL path containing a home directory was still flagged. Fixed to span-based. Specificity runs both directions -- a docstring is never CODE, including a Path() constructor inside a docstring example, which is the exact persistence.py failure; and CODE is never DOCSTRING. Mutation: an all-DOCSTRING classifier fails 12 tests, an all-CODE one fails 13. REUSED THE RULE, NOT THE IMPORT, and measured why. Importing path_scan through its package chain costs 1.13s and 1454 modules and emits DEBUG logging to stdout, which is unusable for a leaf gate when the other five are dependency-free. So the rule is restated and PINNED BY AN EQUIVALENCE TEST comparing this classifier's docstring node ids against path_scan's across all 351 in-scope files. Drift fails the suite, which is what makes a second implementation acceptable rather than a fork. HOW 33 REACHED ZERO WITHOUT EMBEDDING A HOME DIRECTORY ANYWHERE. Only 7 of the 33 were genuine path construction; 26 were human-readable text -- argparse help, printed diagnostics, generated public-repo READMEs, diagram captions. Interpolating an absolute path into those would have written the author's home directory into published READMEs. display_path returns the tilde form by default and an absolute path only under an explicit override, and generated README output is byte-identical to before. Findings reported and not remediated: hooks/ide_paths.py is a second parallel path resolver, so two single-sources-of-truth now exist; the sys.path bootstrap is duplicated 23 times across 21 modules, and only one seam was added rather than a speculative refactor; preference_tracker.py builds the path directly with no literal, so it is outside this task's scope but also outside path_resolver. Could not verify, by name: the %USERPROFILE%, $HOME and ${HOME} branches are unexercised by real data because zero instances exist; the one-level-of-quoting limit is inherited from path_scan and bit this author once, on a drive-path pattern matching inside a regex literal's value; line numbers inside multi-line literals are estimated from value offsets and clamped, which does not affect classification or counts; and create_mcp_repos.py's actual repo creation needs an authenticated gh, so only path resolution and README rendering were exercised. The author also reported a self-inflicted issue it caught: a bulk-edit script silently converted three files from LF to CRLF, inflating the diff to 1756/1465. Normalised back before reporting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s conflict Closes V2-022 (#278) on AC (b) and AC (c); AC (a) is BLOCKED by the owner's ruling against a live install cycle. Adds 35 tests and a blocked procedure document. Full unit suite exit 0, all six gates exit 0. THE FINDING THAT MATTERS IS NOT AN ERROR -- IT IS A LIVE REQUIREMENTS CONFLICT, and it needs a decision before anyone runs the cycle. ADR-020 Path C's PASS is that `claude plugin uninstall` does NOT remove the mcpServers entry register-mcp wrote. That is the whole point: the push gate outlives the plugin. Verified at adr-020-path-c-verification.md:246, which states a PASS is evidence that uninstall does not remove that entry. PRD FR-18 / SRS FR-31 (a)'s PASS is that no MCP tool the plugin registered remains callable. The entry is gone. ONE INSTALL-THEN-UNINSTALL CYCLE SETTLES BOTH AND THEY CANNOT BOTH PASS. Both procedures are now written, both are blocked on the same ruling, and neither document referenced the other until now. What is undecided is not a measurement but an intention: if the push gate is meant to survive uninstall then FR-31 (a) is mis-stated; if uninstall is meant to leave nothing then ADR-020's Path C control does not exist and ADV-012's pre-push hook moves from proposed to required. Recorded as REVIEW-INDEX 37. AC (a) DECOMPOSES INTO TWO CLAIMS AND ONLY ONE IS BLOCKED, which the issue's single sentence hides. That a tool stops being callable after `unregister-mcp` is MEASURED -- V2-016's round-trip test spawns the real process and completes a JSON-RPC handshake at each state. That it stops being callable after `claude plugin uninstall` ALONE is not measured and is identical to Path C. Since ADR-019 bundles zero servers, register-mcp is the only route to mcpServers, so the second claim is the entire remaining question. The blocked test is WRITTEN AND RUNNABLE rather than deferred: the operator performs only the authorised half and exports four variables, and the test computes the verdict. It skips loudly, naming the ruling and the criterion, instead of passing silently -- a test that quietly passes when it never ran is the failure mode this effort keeps finding. A rehearsal test drives the identical snapshot-reading path with synthetic files and requires both a clean and a dirty verdict, so the blocked body is proved capable rather than merely never executed. AC (b) IS DRIVEN BY THE PROVENANCE LEDGER, never by name and never by file comparison, and the live condition it protects was re-measured today: the owner's settings hold an mcpServers entry named post-tool-tracker with NO ledger, so that entry is the user's own. A whole-file assertion calls it a residue failure; a name assertion deletes it. The delta separates under-removal, over-removal including a displaced entry deleted rather than restored, and host changes that are reported but never asserted. Stated precisely, as V2-016 was forced to: the assertion claims REVERSIBLE only. Round-trip reachability is V2-016's and is not re-proved here. Byte-identical is explicitly not claimed, and is the lever the mutation test pulls. THE MUTATION IS THE ACCEPTANCE CRITERION'S OWN PROHIBITION. Substituting whole-file comparison for the ledger delta fails 8 tests, and it is wrong in BOTH directions: it flags the user's own entry and a formatting-only round trip, and it MISSES a genuine residue when the file is otherwise identical. That is exactly the mutant AC (b) forbids, and it is now demonstrably rejected rather than merely discouraged. AC (c) WAS VERIFIED CLAUSE BY CLAUSE rather than assumed because a sibling reported it. Section 6 of V2-021's runbook names the cache directory and the .orphaned_at marker as limitations of Claude Code itself and states no test should assert their absence. Four machine checks now pin it, each with an independent negative, plus a repo-wide fitness check that no test asserts the cache residue is absent. That last check is line-oriented text matching and cannot see an assertion split across lines or routed through a helper; its docstring says so. Two more findings, reported and not edited: test_register_mcp.py's settings guard misses ~/.claude/settings.local.json, which is a real file on this machine carrying a permissions key -- this issue's guard covers all three plus a CLAUDE_HOME override; and the owner's settings.json sha256 is byte-identical to the FR-14a spike's recorded final hash, so the emptied bookkeeping keys are exactly as measured then. plugins/cache/ is present but empty, and V2-021's unattributed divergence persists rather than having been explained. Could not verify, by name: whether uninstall removes a register-mcp entry, which is the blocked measurement; whether the orphaned cache directory appears at the derived path for THIS plugin, since that path is INFERRED and was only ever observed against the spike's throwaway plugin; push-gate end to end, because V2-024 has not built it; and why the cache directory is empty, which is observed and deliberately not attributed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…owner's ruling Closes V2-023 (#279), the final node of batch E. Adds tests/test_plugin_lifecycle.py (64 tests: 62 pass, 2 loud skips) and docs/guides/nfr11-lifecycle-verification.md. Full unit suite exit 0 -- 1897 passed, 43 skipped, 11 xfailed, 0 failed -- all six gates exit 0. Also records the owner's resolution of REVIEW-INDEX 37: ADR-020 Path C wins, so a register-mcp-written entry persists across plugin uninstall, and FR-31 (a) is scope-limited to plugin-specific OPERATIONAL tools rather than safety-enforcement gates. THE RULING DOES NOT UNBLOCK THE CYCLE YET, and this node measured that rather than assuming the decision settled it. `claude plugin uninstall` is capability-blind, and push-gate is marked not_built_yet in plugin/mcp-registry.json because V2-024 owns it. So the only entry register-mcp can write TODAY is the progress writer, which is operational and which the narrowing explicitly does not exempt. A cycle run before V2-024 lands still resolves against an operational entry and criterion (a) can still FAIL. The narrowing is requirement-side scoping; the host cannot distinguish a safety gate from an operational tool and never will. TWO MODULES NOW DISAGREE ON PURPOSE, AND THAT DISAGREEMENT IS THE RULING. On a surviving gate entry V2-022's test fails and this one passes. That is not a regression to reconcile -- it is the narrowing made executable, and it is proved by an explicit mutation rather than left as a comment. The encoding is pinned to the ruling clause by clause, so reverting the decision breaks a test instead of silently drifting. WHAT WAS REUSED RATHER THAN REBUILT, which is most of it: the plugin conformance gate's FF-3 is CALLED rather than duplicated for the no-bundled-MCP half; V2-022's audit_uninstall, attributable_residue and three-file settings guard; V2-016's reachability probe and CLI runner; and the shipped push_gate_server_id, so no server id is spelled anywhere and V2-024's gate is covered the moment it lands. Only the install delta was written new, because nothing existed, and the residue PARTITION, because V2-022's is the unnarrowed reading and must stay that way. THE INSTALL DELTA PERMITS RE-FILLING, not only addition -- because the machine's real state after the FR-14a spike is two keys already present and emptied to {}. An additions-only rule would have failed on the actual post-spike state, which is the condition it is most likely to meet. Hook-event deltas are kept separate so the retained Stop and Notification registrations fail distinguishably from a plugin-owned one. AC 3 CARRIES A MEASURED SHORTFALL RATHER THAN A CLAIM. FR-26 (b) asks for two capabilities to flip; only one can, because push-gate is not built. That is asserted in the test, not assumed away. Step-one unreachability is decided statically -- the plugin ships no route to an mcpServers entry, so install alone cannot register anything by construction -- and step two runs the real register-mcp with a real spawn and JSON-RPC handshake. AC 5 SEPARATES THE THREE CLAIMS the criterion's wording blurs. Reversible at object level plus ledger restoration is asserted. Byte-identical is asserted as the conditional it is, in BOTH branches: it holds at two-space indentation with matching line endings and provably fails at four. Round-trip reachability is credited to V2-016 and not re-proved, pinned by a drift guard on the test name. Eight mutants, all killed, with the file restored byte-identically after each run: an install-delta blind to changes, an uninstall verdict that exempts everything, a blind FF-3 gate, a missing safety-gate exemption, a blind hook-event delta, a blind server-id parser, a blind ruling pin, and a ruling record pointed at the wrong file. Seven findings reported and not edited, four of them documentation debts the ruling created: fr31-uninstall-residue-verification.md SS 6 still states FR-31 (a)'s PASS is the entry being ABSENT and calls the tension unresolved; adr-020-path-c-verification.md still reads "Path C remains INFERRED safe" with a FAIL branch promoting ADV-012 to required; SRS.md FR-31 and prd-v2.md FR-18 still carry unnarrowed wording. Three predate it: prd-v2.md:229 names a plugin repository path that appears nowhere else and contradicts all four guides; SRS.md:779 is WEAKER than its own PRD source, omitting the settings diff assertion NFR-5 requires, and the stronger PRD version was implemented; and test_plugin_conformance.py's fresh-install check re-implements FF-3 inline rather than calling it. Could not verify, by name: the live install delta and uninstall residue; FR-26 (b)'s push-gate reachability, unbuilt; the third unreachable state of the round trip, credited to V2-016 and not re-measured; --force displacement across a real uninstall; project and local scope; a partly-failed install or uninstall; and whether uninstall could ever distinguish a gate from an operational tool -- it cannot, which is why the narrowing had to be requirement-side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lands V2-024 (#280) and V2-026 (#282) together. They ran concurrently on disjoint files and are committed as one change because each invalidated an assertion the other had written. Full unit suite exit 0, all six gates exit 0. V2-025 (#281) is NOT here: it is blocked directly behind V2-024 and builds the CI assertion that mechanically enforces the ordering below. WHY THIS PAIR MATTERS MORE THAN ITS POINT COUNT. sequencing_risks.md R-1 calls the hook-deletion ordering "the single most dangerous ordering in the plan", and states that the push gate is protected TODAY only because FR-4 has not run and the live PreToolUse hook still fires. This commit removes the first of the two reasons that deletion is unsafe. The second is V2-025. V2-024: THE PORT KEPT THE SEMANTICS AND SAID SO. Measured independently: push_gate.py makes 9 git calls, all read-only, and origin/main appears at exactly one line as one of four merge-base candidate refs. Zero branch-protection logic, zero network. What it actually enforces is a VERSION change on the branch and no uncommitted tracked changes, and it FAILS OPEN. All of that is preserved. A determination field (ALLOWED/BLOCKED/UNDETERMINED) makes a fail-open legible instead of indistinguishable from a pass, and NO VERDICT CHANGED. Whether the gate should fail closed remains a separate, unmade decision. THE EQUIVALENCE CLAIM IS ENUMERATED, NOT ASSERTED. All 23 tests in test_push_gate.py are accounted for: 17 ported verbatim, 4 equivalent, 2 NOT CARRIED. The two are hook-wiring assertions on source strings -- one has an analogue only in spirit, the other is a historical guard with none. Nothing in the MCP suite replaces them, and that is stated rather than rounded away. The counts are measured by a test that fails if the map omits or invents a name, and a TestNoDrift class runs the whole corpus through BOTH implementations and requires agreement, so divergence during the port-and-delete overlap window fails here rather than being discovered after the hook is gone. A PORTING DEFECT THE BRIEF DID NOT ANTICIPATE. The hook resolved a bare `git push` against os.getcwd(), correct because it ran inside the caller. An MCP server is a separate long-lived process whose cwd is wherever Claude Code spawned it, so the same fallback would silently judge the WRONG REPOSITORY. The tool takes an explicit cwd and reports when it had to guess. Placed in src/mcp/push_gate/ rather than a new sibling repo, and the reasoning is measured: the constraint that forced register-mcp into the plugin does not apply, because an MCP server is spawned by absolute path from settings.json rather than loaded by the plugin manager. A sibling checkout is absent on a fresh clone, so AC 2's equivalence evidence would silently skip in CI. It is also deliberately not FastMCP: a safety gate whose start-up depends on a pip package disappears silently when that package is missing. V2-026: NEITHER SOURCE DOCUMENT NAMES A COMMAND. Both name the same five phases plus a full-pipeline command, and they do not disagree. The literal names are therefore the implementer's decision and are labelled as such rather than cited. ORDERING IS MEASURED FROM THE COMPILED GRAPH, not from CLAUDE.md. create_flow_graph is built and read, and ordering is asserted as reachability in both directions so it does not depend on topological tie-breaks. Result: exactly steps 0-8, correctly ordered, with sdlc_step5_retry -> sdlc_step4_implementation as the ONLY backward edge -- asserted, not assumed. THE START-UP CHECK IS THE SAME FUNCTION OBJECT the three earlier commands call, proved by identity assertion rather than by copying it. Eight spawn routes are replaced and call-counted while it runs: zero calls, with a companion negative proving that harness is not vacuous. NFR-1 is this project's single primary metric and a start-up check that spawned anything would break it through the exact mechanism hooks are being removed to avoid. M12 CAUGHT A REAL DEFECT IN ITS OWN FIRST DRAFT: the dispatcher passed cwd=engine_root to subprocess, which would have run the pipeline against the ENGINE repository instead of the user's project -- opening an issue and a branch on the wrong repo while every other assertion still passed. 12 mutants, 12 killed, on each node. WHAT hook_mode SHOULD MEAN, answered rather than deferred: a stop-point selector, not an execution context. It already selects nothing about hooks -- it selects whether the graph ends after step 3 or continues to step 8. The parameter was deliberately NOT renamed, because it is a public signature and renaming collides with V2-027, whose scope is hook removal. Recommended there as stop_after_step. TWO ACs ARE PARTIAL FOR REASONS THAT ARE NOT FIXABLE HERE. Invocability by name needs a live install, which the owner ruled out; the skip-plus-rehearsal pattern applies, with the rehearsal requiring a clean, a dirty and an invalid verdict. And a real steps 0-8 execution is not blocked by a ruling but by consequence -- it creates an issue, a branch, a PR and a merge on a live repository -- so a test asserts no module here invokes the compiled graph. THE #37 RULING'S STATED REASON FOR REMAINING INERT HAS NOW EXPIRED, and this is the most consequential finding in the pair. That ruling recorded that the blocked cycle still resolves against an operational entry because push-gate was not_built_yet. It is built. A safety-enforcement entry is now registrable, so the ruling finally has something in scope. prd-v2.md FR-18, SRS.md FR-31 and both verification guides are now MORE stale, not less. Findings reported and not fixed: the engine has no start-at-step control, so only a prefix phase can be exact -- plan and run-pipeline are, the other four are not, and the dispatcher prints which extra steps will run rather than hiding it. SRS FR-17's "release" phase has no release step; step 8 is telemetry, the VERSION bump is step 7 and publishing is outside the graph. step_decorator.py:313 carries a pre-rename dry-run threshold that would let steps 2-7 run under --dry-run, measured as DORMANT because create_step_node has zero call sites. 3-level-flow.py exits 0 without running anything when a message starts with / or !, which produces a successful-looking run reaching no step. And hld_v2.md's C4 Level 2 diagram is not buildable as drawn: it shows the commands reaching the engine by import through a plugin_api/ that does not exist, and V2-016 measured that an installed plugin cannot import from this repository at all. Both authors also reported errors of their own. V2-024's module docstring stated its mapping counts twice and was wrong both times; the numbers and the sentence are now pinned by a test. Both mutation harnesses silently converted LF to CRLF via write_text round-trips, one of them rewriting the whole shared registry file; caught by diff size and normalised. Orchestrator note: CHANGELOG.md was mutated by the engine during this session -- four duplicate [Unreleased] placeholder blocks appended and the whole file converted to CRLF, by _bump_version_and_changelog(), not by either author. Restored from the index rather than committed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tion Closes V2-025 (#281), completing batch F. Adds scripts/verify_push_gate_reachable.py, 54 tests, and its own workflow. Full unit suite exit 0 (2207 tests), all SEVEN gates exit 0. R-1 -- "the single most dangerous ordering in the plan" -- blocks the hook deletion on V2-016, V2-024 and V2-025, all three and not any one. This is the third. What that means and does not mean is stated below by its author rather than inferred: this gate attests that the third precondition now exists and is mechanically enforced. It does not attest that V2-027 is safe to schedule. THE BICONDITIONAL IS MEASURED IN ALL FOUR CELLS, which is the whole value of the mechanism: registration PRESENT + tool REACHABLE -> PASS registration PRESENT + tool UNREACHABLE -> PASS registration ABSENT + tool REACHABLE -> PASS <- the post-deletion end state registration ABSENT + tool UNREACHABLE -> FAIL The third row is the one that matters. An assertion failing whenever the registration is absent would satisfy a naive reading of the criterion and would block the very deletion this exists to make safe. AC 2 IS PROVEN BY EXPERIMENT, not by inspection: a scratch tree with settings and catalogue held constant, toggling only hooks/pre_tool_enforcer/policies/push_gate.py, produces an unmoved verdict in all three states -- absent, present, removed. A second test proves the other direction, that planting the hook tree does not rescue a failing verdict. An AST scan additionally asserts no non-docstring string constant in the gate names the hook package at all. MUTATION TESTING FOUND TWO REAL GAPS BEFORE IT FOUND NONE. First pass: 24 of 26 mutants killed. The two survivors were both genuine -- an equality loosened to a comparison on the evidence count, caught only because the suite tested a SHORTENED record and never a lengthened one; and dropping the second fitness function from the composed entry point, invisible because every test called that function directly and nothing proved run_all composed it. Both closed; second pass 26 of 26. IT ALSO DISCHARGED V2-024'S STANDING OBLIGATION. V2-024's TestNoDrift and TestAssertionMappingIsComplete self-skip once the hook policy file disappears, so after the deletion nothing would fail if those modules were deleted alongside it and the equivalence evidence would vanish silently. FF-2 now asserts the record still exists and still reads 23 = 17 verbatim / 4 equivalent / 2 not carried. It never asks whether the hook exists, so it constrains HOW the deletion is written without blocking it -- and it is flagged as beyond the criteria in the source, the tests, and every finding it emits. AC 3 IS MET IN SUBSTANCE WITH THE LITERAL DIVERGENCE NAMED rather than glossed: test_push_gate.py was not extended; a new module was created. That is the better choice, because it keeps the hook suite intact so the drift comparison has something to compare against -- but the criterion as written is not literally true and the author said so. WIRED INTO ITS OWN WORKFLOW WITH NO PATH FILTERS, and the reasoning is measured: ci.yml's paths-ignore covers **/*.md and docs/**, and a commit that violates an ORDERING constraint is not confined to a file type. Kept off the test matrix so a governance failure is its own red check. Both control steps run the same script against inputs differing in exactly one axis, re-proving both directions of the biconditional on every run. TWO ERRORS IN THE ORCHESTRATOR'S OWN BRIEF, both caught by the agent refusing to take a premise on trust, and both recorded as REVIEW-INDEX 38 and 39. I stated that all three settings files carry a live PreToolUse registration and built a design instruction on it. Only ~/.claude/settings.json does; the other two have an EMPTY hooks block. That measurement changed the design: defaulting to the user-scope file would have parked the gate in the passing half of its own truth table and flipped whenever the owner edited a file CI cannot see. The CLI now defaults to the tracked repository file. The consequence the author drew from that is worth stating: because the tracked file declares no PreToolUse, the gate runs in the registration-absent half FROM ITS FIRST RUN. It is live-fire now rather than dormant until V2-027, and in this repository it reduces to "the MCP push gate must stay reachable" -- the monotone end state ADR-017 wanted, reached earlier than the ADR assumed. And I landed scripts/verify_home_paths.py with V2-018 and never wired it. A repo-wide search found it in no workflow and no pre-commit hook. This is the same defect class already in this record, from when three policy gates existed and nothing executed any of them -- a finding I wrote myself and then reproduced. Now wired into ci.yml, which is the one change in this commit that is mine rather than the node's. verify_policy_capability_dispositions.py is also unexecuted, but that one is explained: the capability ledger records its first assertion as EXPECTED TO FAIL pending an owner decision, so it cannot be wired yet. Left alone. Also corrected: my brief cited HLD section 7.7's signature at line 1143. 1143 is the heading; the signature is at 1146. The grep I ran had printed 1146 and I typed the start of a sed range instead -- correction 27's rule, again. Could not verify, by name: the workflow has never run on GitHub Actions -- all three steps were executed locally from their stored YAML form, but ubuntu-latest behaviour, checkout layout and Python 3.11 there are untested; the sibling-checkout entry-point fallback is unexercised because the push-gate catalogue entry uses the in-repository branch; and FF-2's pinned composition is verified against the shipped record rather than an independent re-derivation of which hook assertions ought to carry over, which is V2-024's judgement and was not re-audited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hook census Lands V2-031 (#287) and V2-033 (#289). They ran concurrently on disjoint trees. Full unit suite exit 0, all seven gates exit 0. V2-031 unblocks V2-027, the hook deletion that sequencing_risks.md R-1 calls the single most dangerous ordering in the plan. Five of its six blockers are now done. THE DURABLE CHECKPOINTER DOES NOT EXIST AT RUNTIME. Reproduced independently: langgraph.checkpoint.sqlite and langgraph_checkpoint_sqlite both raise ModuleNotFoundError, _SQLITE_SAVER_AVAILABLE is False, and asking for the durable saver -- get_default_checkpointer(use_sqlite=True) -- returns langgraph.checkpoint.memory.InMemorySaver. requirements.txt:31 declares langgraph-checkpoint-sqlite as a dependency that is absent, and :7 pins langgraph below 1.0.0 while 1.1.6 is installed. The degradation is TRIPLE-SILENT: checkpointer.py:126 catches (ImportError, Exception) and falls back with no log, and orchestrator.py:786 and :850 catch Exception and compile the graph with NO CHECKPOINTER AT ALL. Nothing anywhere says durability was lost. AND THE ISSUE POINTED AT THAT WRITER. Its AC 1 named the saver a prior node had "measured", and that node was half right: the CONFIGURATION is real -- checkpointer.py:253 does set thread_id -- but the OBJECT is not. A configuration check certified a durability guarantee the runtime does not honour, which is the same shape as the "17 writers" correction where a count matched its source and the noun was wrong. Had this author followed the citation instead of resolving it, the crash-resume test would have exercised an in-memory saver and PASSED, certifying durability that does not exist. It tested CheckpointManager instead -- the file-backed writer OAQ 1 already named as owning crash recovery -- and built no replacement, as the criterion required. THE FLAG WAS CHOSEN OVER THE RAISE, and the reasoning is the issue's real content. Raising fails the pipeline at the point of durability loss, which discards maximal upstream work AND leaves already-committed external effects orphaned and unrecorded -- and those orphans are exactly what a later retry duplicates, so raising makes the double-effect risk WORSE. The flag keeps the run alive but withdraws the durability claim: a later resume refuses. A resume reading a chain with a hole cannot distinguish "step N never ran" from "step N ran, committed its side effect, and the record was lost", and a participant that cannot resolve its own uncertainty must not guess. The marker is durable on disk rather than in-process, because the process that LOSES the write need not survive to report it. THE DEFECT THE ISSUE NAMED WAS DEAD CODE, AND ITS LIVE TWIN WAS NOT NAMED. step_decorator.py:169's citation is accurate -- unusually, given this project's citation record -- but create_step_node has zero call sites outside its own module and a re-export. Fixing only it would have reported a live guarantee over a dormant path. The live twin is subgraph.py:382-391 and :474-484 with the identical defect; both are fixed and labelled. Deeper still: at both sites the except clause is near-unreachable, because save_checkpoint catches broad Exception internally and returns False. The actual swallow was the DISCARDED RETURN VALUE. Checking it is the load-bearing fix; the except alone would have caught almost nothing. AC 3 WAS FALSE BEFORE THIS COMMIT. subgraph.py:403-418 wrote workflow-memory.json as an independent second writer and pre_nodes.py reads it on resume, so the resume-facing surface could report a step the checkpoint never recorded. Replaced by a projection that derives every field from the checkpoint record read back, and writes NOTHING when no record exists rather than advancing past the durable state. The replay guard is keyed on session-id-plus-step-number, asserted equal to the checkpoint id so ledger and chain agree on step identity, and tested against a simulated GitHub returning 256 and 257 -- the real duplicate-issue incident this repository already shipped. A PENDING entry found on a later run means the outcome is UNKNOWN, so it refuses rather than guessing. V2-033: THE SPAWN FLOOR IS 7, NOT THE ENUMERATED 4. Identical across all 20 runs. The enumerated model assumed the two unconditional git rev-parse calls return early, which holds only on a default branch -- this one is 64 commits ahead of main (verified), so each continues into a rev-list and branch detection adds three more. AC 2 still holds as written, because every spawn is inside enumerated-plus-named-exceptions and both unconditionals fired 20 of 20. Two of the criterion's own citations are wrong: voice.py:144 is cited as the spawn launch and is a DOCSTRING line, the only Popen in that file being at 164; and the claim that both guarded targets resolve as siblings is false, since the voice notifier resolves elsewhere entirely. Both confirmed at source. IT OBTAINED ZERO REAL INVOCATIONS AND SAID SO. The Stop hook is live -- 1,643 genuine firings since March -- and fired zero times during the task: 23,815 log lines before and after, mtime frozen for 720 seconds, while the tool tracker updated from its own calls. Subagent turns do not emit Stop events; the event belongs to the parent session's completed turns. It did not simulate 20 and call them measured. Three tiers are kept separate, and the observer cost is measured at +19.4ms, 7.7%, with no effect on spawn count. A NUANCE THAT BINDS V2-034: 8 of the 9 guards were evaluated and returned False, but the ninth was NEVER EVALUATED, because speak() is only reached on a voice-flag turn. GUARD_NOT_REACHED is reported distinctly from GUARD_FALSE_SCRIPT_SKIPPED; conflating them would assert more than was measured. And a sober one: retiring references 1-7 will NOT reduce the spawn count. They are already inert and all seven spawns are git calls. A correctness win, not an NFR-1 win. THE MOST DANGEROUS FINDING IN THIS COMMIT WAS INCIDENTAL. The Stop hook tries to open a pull request on every response turn. Verified at source: core.py:319, :363 and :483 call run_pr_workflow(), post_impl.py:166 calls github_create_pr(). Both modules are absent -- 276 and 72 occurrences of the resulting failure in live logs -- and that absence is the only thing stopping it. Restoring either without revisiting the trigger conditions makes the hook open real PRs unprompted, once per turn, on any feature branch. The Stop hook is one of the two hooks RETAINED across the migration. A defect dormant only because a file is missing is not fixed; it is armed. Recorded as REVIEW-INDEX 40. Both authors mutation-tested their own work: 10 of 10 and 11 of 11 killed. V2-033's M4 initially survived and exposed a latent defect in its own tool -- a third rev-parse would have raised IndexError instead of reporting unclassified. Further findings reported and not edited: checkpointer.py:126 and orchestrator.py:786,850's silent degradations; two further save_workflow_memory implementations writing a differently-shaped workflow-memory.json to a different directory; a tenth dead Stop-hook reference, hooks/metrics_emitter.py, whose absence is why real per-invocation durations are unobtainable; and an unreachable PermissionError branch ordered after IOError, which IS OSError in Python 3 -- that one fixed in place. Recorded as REVIEW-INDEX 40 through 43. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…imeouts Lands V2-034 (#290) and V2-035 (#291), the largest issue in the sprint. Full unit suite exit 0, all EIGHT gates exit 0 including the new verify_no_fixed_timeouts. THE V1.20 RENUMBERING SILENTLY VOIDED THE PER-STEP TIMEOUT TABLE, and nothing noticed for the whole era. Verified at HEAD: timeout_wrapper.STEP_TIMEOUTS was keyed {0,8,9,10,11,12,13,14}, the PRE-v1.20 numbering. The live wrapped steps are {2,3,4,5,6,7,8}. Intersection: {8}. So six of seven wrapped steps ran with NO DEADLINE AT ALL, and the seventh -- now Final Telemetry and Summary -- inherited the 900-second budget written for GitHub Issue Creation. The table's own comment, "v1.15.2: removed dead entries for steps 1,2,3,4,5,6,7", was correct under the old scheme and became actively misleading under the new one. And TestStepTimeout was PINNING THE STALE TABLE AND PASSING. The code and its test agreed with each other and neither agreed with the pipeline, which is why the fix came from resolving the actual _run_step call sites from the AST rather than reading either. Now unbounded by default with per-step overrides, and the unbounded path runs on the calling thread rather than abandoning a daemon thread that may still be writing to GitHub. AC 1 IS FLAGGED BY ITS OWN AUTHOR AS POSSIBLY NOT MET, which is the right way to leave it. The criterion names a SCAN scope and a ZERO condition and they are different quantities. The gate scans all 261 files of langgraph_engine/ and plugin/, and enforces zero on a named path set. Under the wider reading, 49 further sites remain -- 42 fixed and 7 unresolved -- and the gate PRINTS THAT NUMBER ON EVERY RUN rather than hiding it. The reviewer decides which reading was meant. AC 2's wording would have been satisfied by the defect. "Configurable or user-overridable" describes STEP1_PROMPT_GEN_TIMEOUT exactly, and those env vars were the problem. The gate therefore classifies an env-with-fixed-default as FIXED_DEFAULT and fails it: overridability is not the property NFR-2 wants, an unbounded DEFAULT is. AC 3's count is enforced rather than claimed. The exception list is keyed file::symbol::resolved-value, so it cannot silently widen to a second call method, and the gate fails if the list grows. Exactly one survives on the enforced path -- the Anthropic HTTP socket timeout, configurable, disabled at 0, and wrapped in a breaker -- with four tests enforcing each clause of that justification, including an AST assertion that the breaker wrapper is still present. ADR-016 AND THE ACCEPTANCE CRITERION DISAGREE ABOUT WHAT THE FIVE MECHANISMS ARE. ADR-016 lists slow-call rate as the fifth and folds full jitter inside the breaker; the criterion promotes full jitter to the fifth and DROPS slow-call rate -- the very mechanism ADR-016 calls "precisely the property NFR-2 needs". All six capabilities were implemented so neither document has to be declared wrong, and the contradiction is reported rather than resolved unilaterally. ADR-016's own census also undercounts: it names 6 sites across 5 files; measured on the PRD's scope there are 9, and across langgraph_engine/ there are 51. V2-034: THE 7 AND THE 9 WERE BOTH CORRECT, at different scopes, and the difference is exactly core.py. The criterion says "removed from hooks/stop_notifier/core.py", and core.py carried exactly 7. The package-scoped 9 adds two references in other files. Neither figure was stale -- a first in this effort -- and a test asserts the scope was not silently widened. Option (a) was unreachable for all seven, measured by a repository-wide find on basename: none of the seven exists anywhere. All took option (b), each with a disposition naming its live replacement or recording an unreplaced loss. plan-session-archiver.py has NO successor and is recorded as a genuine loss rather than given an optimistic token, and common-failures-prevention.py is asymmetric -- the knowledge base is now read but never written. THE PR-CREATION MODULE IS NOT ABSENT, AND THAT ESCALATES A RECORDED FINDING RATHER THAN ADDING ONE. run_pr_workflow is at scripts/github_pr_workflow/versioning.py:264, verified, and its own docstring enumerates "3. Create PR ... 5. Merge PR ... 7. Version bump + CHANGELOG on main", with "Called from stop-notifier.py when .session-work-done flag exists." The import fails ONLY because core.py:14-15 puts hooks/stop_notifier/ on sys.path instead of scripts/. The barrier is a one-line search-path bug, and what it holds back is not merely opening a PR but opening it, merging it, and bumping the version on main, once per response turn. REVIEW-INDEX 40 is escalated accordingly. A second landmine retired for the right reason: git-auto-commit-policy.py ran --enforce every turn, so arming it would have auto-committed once per response. Retirement was the safe direction, not merely the available one. Both authors caught defects in their own work. V2-034's subprocess import lived inside the FIRST retired block while the surviving git calls -- all seven measured spawns -- depend on it; naive removal would have raised NameError into each except and silently killed branch detection. V2-035 found a falsy-zero bug in CircuitBreaker.allows() under an injected clock at 0.0, and a stream.read(1024) that blocked until the buffer filled, killing a healthy streaming child in its own test. Mutation: 9 of 9 and 3 of 3 killed. V2-034's M3 initially SURVIVED because an elif chain masked it and the assertion only counted reasons; strengthened to assert reason text, re-tested, killed. Composition with the machinery landed earlier this week is explicit rather than assumed: same-tier backoff stays inside CLOSED and the breaker is a separate trigger; the liveness path CONSUMES the existing retryable-error classifier instead of reimplementing it; a breaker trip classifies as FAILURE_OTHER so it is not mistaken for rate-limit evidence; and the breaker is consulted BEFORE the effect ledger, so a fail-fast never writes the PENDING entry the ledger refuses to guess about. Lease durability is measured, not assumed. Both sqlite checkpoint modules ImportError despite being a declared dependency, so the lease marks itself degraded and warns once. An in-memory lease bounds a hang inside a live process; it does not survive the crash it exists to bound, and it says so. Further findings reported and not fixed: faithfulness_gate.py carried the argument against itself in a comment -- started at 60s, a real measured run took 84.6s, the gate fail-opened on ordinary latency, and the fix was to raise the number; call_execution_script had an undocumented timeout=30 default capping callers with inner budgets of 60 and 90; and the ledger's "AC1 EXPECTED TO FAIL / 19 of 27 decided" prose is stale against a gate now reporting 27 decided, 0 empty, PASS. Recorded as REVIEW-INDEX 44 and 45, with 40 escalated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…vocation Lands V2-027 (#283) and V2-028 (#284). Full unit suite exit 0, all eight gates exit 0. THE HOOKS ARE GONE FROM THE LIVE MACHINE. This is the only change in the sprint that wrote the owner's running configuration, authorised explicitly and twice, and executed in a mandated order that existed because of a measured fact: the push gate was NOT registered here. 25 mcpServers entries, push-gate absent. The replacement existed in the repository and not on the machine, so deleting PreToolUse first would have left NO push gate at all -- the exact lapse R-1 exists to prevent. Executed in four phases, and phase 1 was a precondition rather than a step. Backup taken outside the repository first. Then push-gate registered through the shipped register-mcp, and reachability proven by SPAWNING THE PROCESS THE SETTINGS ENTRY NAMES -- argv read verbatim from mcpServers, nothing substituted from the catalogue -- completing initialize, tools/list and a real tools/call that returned determination ALLOWED. A settings entry is not evidence; a completed handshake is. Only then were the registrations removed. Verified end state: hooks are Stop and Notification ONLY, push-gate is registered, 26 entries where there were 25, and the backup is on disk. STOP AND NOTIFICATION SURVIVED BYTE-IDENTICAL, proved by comparing canonical-JSON digests of the entries rather than their presence. All 25 pre-existing mcpServers entries are byte-equal, including the owner's own un-ledgered post-tool-tracker. USERPROMPTSUBMIT WAS REMOVED BY OWNER RULING, NOT BY THE ISSUE TEXT, and that is recorded rather than absorbed. V2-027's criteria name only PreToolUse and PostToolUse; V2-028's criterion needs UserPromptSubmit gone but was forbidden from touching settings and defers here. Left to the written criteria alone, NOBODY would have removed it. Both authors found the gap independently. The criterion text still needs amending, or the removal remains uncovered and a future regression passes both issues. AC 2 WAS DEMONSTRATED BY LIVE MEASUREMENT FOR TWO HOOKS OF THREE. PreToolUse: a Grep call it had refused earlier in the same session was re-issued verbatim after deletion and executed normally -- same call, same session, opposite outcome. PostToolUse: its live write surface froze, its final entry being the very command that performed the removal, with line count, byte count and mtime unchanged across the following calls. UserPromptSubmit could NOT be demonstrated, because a subagent turn does not submit a user prompt, and only its absence from the file is established. V2-028 MADE THE AUTHORISATION A RULE RATHER THAN AN INFERENCE. The pipeline now refuses to start without an explicit --invoked-by naming one of the six FR-17 commands. Deliberately an argument and not an env var: the engine spawns CLI subprocesses that would inherit an env var, and an inherited authorisation is one nobody made. The gate sits before the config loader and the LangGraph import -- measured at 0.117s for a refused run against 2.031s for the import it avoids. Its strongest evidence was taken WHILE USERPROMPTSUBMIT WAS STILL REGISTERED: the exact live invocation was replayed with a realistic hook payload on stdin and refused. So the gate holds independently of the registration, which is the point, since the registration was the part it was not allowed to touch. TWO FINDINGS THAT MEAN THIS WORK IS NOT FINISHED, both verified. scripts/settings-config.json STILL REGISTERS ALL THREE DELETED HOOKS. It is the template ~/.claude/settings.json is bootstrapped from, and the removal script does not touch it. Anyone setting up a machine from it re-creates exactly what was just deleted, and no gate notices. A migration is durable only when the thing that recreates the old state is also changed. And verify_no_fixed_timeouts.py cannot see the engine's own entry point: its scan scope is langgraph_engine and plugin, so scripts/ is excluded, while scripts/3-level-flow.py:539 holds ORCHESTRATION_TIMEOUT_SEC = 300 used as a wall-clock abort on the pipeline path. A fixed literal on exactly the surface NFR-2 governs, invisible to the gate written to ban it. The hook SOURCE FILES were deliberately not deleted -- only registrations. ADR-017 forbids the CI assertion from asserting on hooks/pre_tool_enforcer/, and keeping the files present keeps the equivalence tests running instead of self-skipping. A correction to the blast-radius record for V2-029: the issue names the PostToolUse progress surface as lost, but the fallback constant session-progress.json had not been written since 2026-07-30, five days before deletion. The live surface is tool-tracker.jsonl. Attributing the loss to the stale fallback would misstate what actually stopped. No sys.path line in hooks/stop_notifier/ was altered -- git diff over that tree is empty -- so REVIEW-INDEX 40's per-turn PR hazard is undisturbed. V2-027's own mutation M4 initially SURVIVED: its unparseable-settings refusal was shielded by settings_store's second refusal, so no test pinned its layer, and a dry run -- which never reaches merge_write -- would have reported an empty hook set for a file it could not read, reading as "nothing to remove". Two tests added; 4 of 4 now caught. Recorded as REVIEW-INDEX 46 and 47. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… hooks Lands V2-029 (#285), V2-030 (#286) and V2-032 (#288), completing batch G. Full unit suite exit 0, all eight gates exit 0. Both suite and gates were run by the orchestrator rather than taken from any node's report. THE BLAST-RADIUS MEASUREMENT DESCRIBES A DELETION THAT DID NOT HAPPEN, and that is the most useful thing in this commit. The recorded figure -- 135 of 2,218 nodes going dark, 6.09 percent -- re-measures correctly at 6.087 percent, and is decomposed for the first time. But it measures the removal of hook SOURCE FILES, and 2e371f6 deleted zero files: 1,589 insertions, 5 deletions, no removals. All five paths are on disk today. NO NODE WENT DARK. 135 nodes became unreachable from any hook entry point while remaining present, importable and testable. A structural-breakage measurement was the wrong instrument for what shipped, because nothing structural moved. Two further recorded figures corrected with measurements. "26 candidate cross-boundary edges" DOES NOT REPRODUCE -- the figure is 12, against 417 crossing in total. And "all were cha_only/LOW" is false as written, since 12 of the 417 are MEDIUM, though harmlessly: every MEDIUM is outbound, all 12 inbound edges are LOW, and they are now fully enumerated rather than spot-checked, so the "4 spot-checked" caveat is retired. The progress-surface loss was recorded against the file that actually carried it. session-progress.json's last write is 2026-07-30 14:17, FIVE DAYS BEFORE the deletion -- citing it would credit the removal with a death that predates it. The live surface is tool-tracker.jsonl, and its freeze was independently re-confirmed: line count, byte count and mtime identical at the start and end of a two-hour session spanning several hundred tool calls. THE SRS ROW WAS DATED TO A COMMIT BECAUSE NO PULL REQUEST EXISTS, and the argument is recorded in the row rather than assumed. Refusing to write it would satisfy "dated to the PR" by writing nothing, while producing the exact defect PRD FR-22 exists to prevent: FR-9's criterion becomes permanently false the moment the hooks are deleted, with no compensating record. The deletion is already executed on the live machine. Withholding is the worse failure. No PR number was invented. This project has a recorded instance of that class -- a commit hash typed from memory that did not exist, making the citation unverifiable and worse than an omission. The row cites 2e371f6, states plainly that no PR exists, and instructs that a follow-up row be appended when one is opened rather than this row edited. The date survives every reading: the settings mutation is measured at mtime 10:05:53.76Z, the commit at 10:57:39Z, and a PR from this branch cannot predate the commit it contains. The PR's absence costs the identifier, not the date. "The two removed hook events" is THREE, and the row says so. PreToolUse and PostToolUse under FR-13, plus UserPromptSubmit under FR-15, which went by owner ruling because it fell between two issues' criteria. Recording two would make the SRS disagree with the machine. It also closes a question the revised AC-9 deliberately left open -- whether the registration would be deleted or repointed; it was deleted outright -- without falsifying AC-9, which is under-specified rather than wrong and was deliberately not re-superseded. BYTE-IDENTITY WAS DECOMPOSED INTO FOUR GRANULARITIES, TWO ASSERTED. The entry's canonical JSON is asserted unconditionally; the entry's raw source bytes only while the document's formatting fingerprint holds; the hooks block and the whole file are reported, not asserted. Each choice is measured rather than argued: whole-file identity cannot survive any install, because install writes enabledPlugins and extraKnownMarketplaces, so asserting it would report measured host behaviour as a hook defect. The source-byte rung fails under re-serialisation AND, separately, under a key re-order at unchanged formatting -- proved by its own test, so the conditional cannot be misread as "only reformatting breaks it". The honest reading is recorded: the criterion as written is literally checkable only at whole-file granularity, and there it is unachievable for reasons unrelated to hooks. When the conditional lapses the test records the byte claim as NOT CLAIMED, by name, rather than passing silently. AND THE CRITERION IS SATISFIABLE WHILE THE USER'S STOP BEHAVIOUR CHANGES. Hook contributions merge across settings scopes into one flat unlabelled list per event, so an install registering a Stop handler at PROJECT scope leaves the user-scope entry byte-identical -- passing FR-8 to the letter -- while changing what actually runs. Nothing measurable from a single user-scope snapshot can see it. For this plugin the gap is closed by a different, already-executed control; the criterion's own wording does not close it. V2-027's result was explicitly NOT presented as satisfying FR-8: hook deletion and install/uninstall are different operations against the same keys, and only the instrument was carried over, imported rather than respelled. The AC is BLOCKED, with the skip-plus-rehearsal pattern and 14 of 14 planted mutations caught, including five against the shipped document itself. Also in this commit, the orchestrator's own corrections. REVIEW-INDEX 40 overstated its severity: the PR package re-exports only main, so the call raises AttributeError regardless of sys.path, and TWO independent things must change rather than one. Its call-site citations were correct when written against 463451e, where core.py was 506 lines, and drifted when V2-034 retired seven references and the file SHRANK to 315 -- correction 28's defect class arriving from the opposite direction, within hours, in a record whose purpose is to stay checkable. And scripts/verify_plugin_conformance.py had been flipped entirely LF-to-CRLF by something in the session, showing as 575 insertions and 575 deletions with byte-identical content. Restored from the index. SRS.md was NOT restored wholesale. Its diff carries three hunks: two engine date stamps and the real row at line 1033. A whole-file restore has already destroyed a node's SRS edit once in this effort, so only the two stamps were reverted, line by line. Recorded findings not fixed: docs/README.md's guides index claims 14 against 19 on disk, with five guides unlisted across four nodes; and four SRS FR status blocks are stale against work that has landed, whose correction under rules/44 is an appended supersession rather than an edit and is a larger deliverable than any of these issues. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lands V2-036 (#292) and V2-037 (#293), the last two issues of the sprint. Full unit suite exit 0, all eight gates exit 0. V2-036'S AGENT STALLED AFTER DOING THE WORK, NOT BEFORE, and left it in the tree with no report. Rather than re-dispatch a stalled run, every acceptance criterion was verified independently -- which is a better basis than a report anyway. VERSION bytes b'2.0.0\n': one line, valid semver, no trailing whitespace, no extra lines CHANGELOG ## [2.0.0] - 2026-08-04, ISO 8601, one [Unreleased] migration guide 381 lines, Steps 1 through 8 present and in order the two clauses "Required before steps 3 and 5" at line 90, "Do not perform this step if step 3 did not pass" at line 145 THE CORRUPTION THE BRIEF WARNED ABOUT DID NOT HAPPEN. CHANGELOG.md measures 0 CRLF and 524 bare LF with a single [Unreleased] block. In this same session, _bump_version_and_changelog() had appended four duplicate placeholder blocks and converted the whole file to CRLF, and the orchestrator restored it before every commit. The node was told to generate the entry with plain structured writing and verify the bytes afterwards rather than trust what it wrote, and the file's byte state says it did. The guide also names scripts/settings-config.json twice -- the bootstrap template that still registers all three deleted hooks, so a machine set up from it re-creates exactly what 2.0.0 removes. A migration guide omitting that would be wrong whether or not fixing the file is in scope. V2-037 FOUND ITS DEFECT DID NOT EXIST, AND PROVED IT RATHER THAN FIXING IT. REVIEW-INDEX section 2 claimed VERSION read 1.21.5 while CLAUDE.md read 1.21.4. That has been false since 1919bdb, traced with git log -L 4,4:CLAUDE.md, which moved CLAUDE.md, README.md, SRS.md and langgraph_engine/__init__.py to match. The row itself then went stale for two days and #293 was dispatched against a defect already closed. AND THE PREMISE SCAN CONTRACTED THE DEFECT CLASS IT WAS BUILT TO HUNT. premise_scan_bh verified that contradiction as "STILL TRUE. MEASURED" and was committed 2026-08-02 at 10:35 and 10:56. The fix landed at 15:30 THE SAME DAY. Its verdict was correct when written and false four hours later. A scan whose entire purpose was finding stale premises became one inside a working day. Recorded as REVIEW-INDEX 48. THE MOST OPERATIONALLY DANGEROUS FINDING IN THE WHOLE SPRINT arrived last. RUNNING THIS REPOSITORY'S UNIT SUITE MUTATES TRACKED FILES: tests/test_sync_version.py shells out to scripts/tools/sync-version.py, which rewrites CLAUDE.md, README.md, SRS.md and langgraph_engine/__init__.py to match VERSION. So the 2.0.0 propagation in this commit CANNOT BE ATTRIBUTED -- it may have come from V2-036 or from any full-suite run, including the orchestrator's own. Every "run the suite, then commit" cycle in this sprint was also a version-propagation step, and nobody knew. It also makes the restore-then-commit habit that protected against the engine's date stamps ACTIVELY WRONG from here: blanket-restoring those files would have silently reverted the release. This commit therefore inspects them hunk by hunk instead. The diff is exactly two kinds -- the version bump, which is the release, and date stamps that are now factually correct because the files genuinely changed today. Restoring either would be the error. Recorded as REVIEW-INDEX 49, with langgraph_engine/__init__.py named as a fifth file that was never on the restore list at all. Two drifters remain outside sync-version.py's targets and are recorded rather than fixed: Dockerfile lines 3 and 42 and k8s/deployment.yaml lines 13 and 28 still read 1.6.1, roughly fifteen minors behind. Their absence from the target list is the mechanism of the drift. V2-037's own contribution is a byte-level format check. The existing consistency test strips before matching, so it accepts a trailing space, a leading space, a blank second line and a CRLF terminator -- proven by measurement. 13 malformed shapes are now each rejected for the named reason and 6 well-formed ones accepted, including 2.0.0 so it cannot obstruct this release. It also caught its own process error: its mutation harness restored via write_text and flipped the file LF to CRLF -- the exact trap that file's docstring warns about -- hidden by read_text round-tripping and exposed only by the diffstat. Fixed with write_bytes. Recorded as REVIEW-INDEX 48 and 49, with the section 2 contradiction row struck through and dated to the commit that closed it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…deleted The hook deletion was not durable. scripts/settings-config.json is the template ~/.claude/settings.json is created from, and it still registered UserPromptSubmit, PreToolUse and PostToolUse -- the three registrations this release removes. Both setup scripts copy that template over an existing settings file WHOLESALE rather than merging, so a machine bootstrapped from it got exactly what it declared: the migration, undone, with nothing detecting it. Nothing could have detected it. The hook-deletion issue's criterion asserts against the LIVE settings file, not the template. The sibling issue that needed UserPromptSubmit gone was forbidden from touching settings and deferred. And the push-gate CI assertion reasons only about PreToolUse and the MCP gate. The most likely way this migration gets undone was also the way least likely to be noticed. THE FIX ARMED THE SCRIPT THAT READS THE FILE, AND THIS COMMIT NEARLY SHIPPED THAT. Removing the three hooks also removed the template's only occurrence of "3-level-flow" -- which is the SENTINEL both setup scripts grep the user's settings file for to decide whether hooks are already installed. Guard fails, else branch runs, and the else branch is Copy-Item -Force / cp: a wholesale replacement. So a machine set up from the corrected template would have been overwritten on EVERY subsequent run, the sentinel never matching what the template now produces. And on this machine, where the hooks are already deleted so "3-level-flow" appears nowhere, a setup run would have destroyed 26 mcpServers entries INCLUDING THE PUSH GATE REGISTERED HOURS EARLIER. The sentinel is now "stop-notifier" -- the retained hook -- verified present in the template, in the live settings file, and in both guards. That restores idempotence; it does not fix the wholesale replacement itself, which remains recorded as a separate finding. Recorded as REVIEW-INDEX 50. The class is not stale citation but IMPLICIT COUPLING THROUGH A LITERAL: nothing connected a file's contents to the string another file greps it for, and no gate here detects that. A paired test, not a one-directional one. Seven assertions: the three deleted events must be absent, and Stop must REMAIN -- because a blanket "no hooks" purge would satisfy the first half while being a regression in the opposite direction. The can-fail half plants each of the three and requires each to be named; the specificity half requires an empty hooks block to fail the retention direction. Also recorded as REVIEW-INDEX 51: writing the template flipped it from CRLF to LF, turning a 20-line edit into an 85-line churn, caught by reading git diff --stat rather than trusting the write. That is the FOURTH instance of this in the record -- two subagent mutation harnesses, one whole-file flip of verify_plugin_conformance.py by something unattributed, and now this. Path.write_text and io.open(..., "w") both normalise silently on Windows. Restored byte-for-byte; the final diff is 1 insertion and 46 deletions. Full unit suite exit 0. Gates unaffected and re-run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This PR's title is now wrong, and that matters more than usualIt reads "docs: segregate docs/ into nine categorised folders" — the first commit on this branch. It now carries 70 commits and 431 files, and its headline change is deleting hook registrations from a user's live configuration. A reviewer scanning titles would approve a docs reshuffle and merge a breaking release. Suggested title: What changes for a userThe hooks are gone. Every existing v1.x installation is affected and none upgrades silently, which is what makes this MAJOR. The replacement is a plugin ( Ordering
The deletion itself ran in four phases against the live machine: backup, register the replacement, prove it reachable by completing a JSON-RPC handshake against the process the settings entry names, and only then remove. A settings entry is not evidence. VerificationEight CI gates, all exit 0 — policy audit matrix, orphan dispositions, capability dispositions, plugin conformance, text-mode Two of those gates existed before this branch and were invoked by nothing; a third was landed here and left unwired until an audit caught it. All are wired now. What this PR does not fix, recorded rather than swept
A note on the evidenceAlmost every node in this sprint disproved a recorded figure: 13 code sites measured 33, a documented 8-key manifest schema measured 18, a 4-spawn model measured 7, 26 cross-boundary edges measured 12, and a per-step timeout table was keyed to a numbering scheme two versions stale while its own test pinned the same stale scheme and passed. The dominant defect class here was never wrong code. It was a measurement that was correct when taken, and a world that moved while the number stayed put. 🤖 Generated with Claude Code |
…missed docs/README.md is the entry point rules/11 sends a reader to, and it had rotted in two directions at once. Six of its nine declared folder counts disagreed with the tree, and EIGHT FOLDERS HOLDING 63 FILES WERE ABSENT FROM IT ENTIRELY -- every one created during this sprint by a node that added documents without touching the index. The scale is the interesting part. This was reported four separate times during the sprint as "the guides index says 14", and it measured 21 -- but guides was the least of it. phase-0-reverse-engineering alone holds 26 files and appeared nowhere. api/ was listed with a count that had never matched, because it holds a YAML spec rather than Markdown. NOTHING DETECTED ANY OF IT, BECAUSE THE INDEX IS PROSE. And the way it compounded is worth recording: five nodes each noticed their own new guide was unlisted, each reported it as a finding, and each FOLLOWED THE PRECEDENT of not listing it -- so a defect that was visible five times grew five times rather than being fixed once. The fix is therefore the test, not the edit. It asserts two directions, because either alone is satisfiable by a degenerate index: every folder under docs/ holding files must appear in the summary table, so a new folder cannot be silently omitted; and every declared count must equal the files actually present, so a listed folder cannot be left to drift. A third pair guards against the regex silently matching nothing, which would make both assertions vacuously true. Verified able to fail before it was made to pass: against the stale index it failed three assertions, and each names the figures rather than asserting a bare boolean. Counts are of ALL files rather than Markdown only, because api/ holds a single YAML spec and the index has always counted it that way. The index itself was regenerated programmatically rather than by hand -- existing descriptions preserved verbatim, counts and file lists rebuilt from the tree, eight sections appended with descriptions written for them. Line endings measured before and after: 0 CRLF, LF throughout, zero non-ASCII. That check is now reflex here; this is the fifth line-ending incident in the record and the second in two commits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ore merging The last thing this branch learned about itself, recorded so the merge does not bury it. CI IS RUNNING AND HAS BEEN ALL ALONG. `github_get_pr_status` reports `checks: []` for a pull request with 183 workflow runs against it. I read that as Actions most likely being disabled, and said in the same breath that I could not distinguish it from the tool simply not reporting the field. The second reading was the correct one. A push was made specifically to test the first, and had the hedge not been stated the next step would have been changing repository settings to fix a problem that did not exist. The workflow status badge endpoint renders its conclusion as text and settled the question in one call. When a tool reports absence, the question to ask is whether it looked. Recorded as REVIEW-INDEX 53. WHAT THE BADGES SAY: Push-gate reachability passing Plugin conformance passing CI failing -- Test (3.10) and Test (3.11), exit 1 BOTH WORKFLOWS THIS SPRINT ADDED PASS ON UBUNTU, including the one that verifies the release's actual contract: that the MCP push gate is reachable on a machine where the PreToolUse hook no longer exists. That is the assertion the hook deletion depends on, and it is green on Linux, not merely on the development machine. THE PRE-EXISTING TEST JOB IS RED, AND THE CAUSE IS UNKNOWN. The same suite runs to completion with exit 0 on this Windows machine, repeatedly, including immediately before this commit. The failure logs require authentication this session did not have. The plausible classes are the ones this repository has already been bitten by more than once: path separators, subprocess spawn mechanics, and default text encoding. It is untested territory rather than a regression this sprint introduced: CI's test job has no earlier green run on this branch to compare against. MERGED AS AN INFORMED, ACCEPTED RISK RATHER THAN A FIXED DEFECT. The owner was shown the red CI, the Windows-versus-Linux split, and the fact that nothing had diagnosed it, and chose to proceed on the strength of the two green workflows. Recorded as REVIEW-INDEX 52 so that nobody later reads "the suite passes" out of this repository's history without also reading why that sentence needs a platform attached to it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What
docs/held 160 files in one flat directory, so nothing indicated what any given document was. Standards, pipeline policies, ADRs, runbooks, one-off audit reports, per-release design notes and the GitHub community files all sat side by side atdocs/*.md.They are now grouped by kind:
standards/policies/reports/architecture/guides/releases/contributing/api/,phase-1-architecture/Classification used an objective signal wherever one existed rather than name-guessing: a file whose name also appears in
~/.claude/rules/is a standard, one that appears in~/.claude/policies/is a policy. That covered 98 of the 160; only the remainder fell through to pattern rules.Every relocation went through
git mv— all 160 renames register at 100% similarity, so history follows the files.docs/README.mdis a new index naming what each folder holds and why.Fixed along the way
load_framework_standards()/load_language_standards()would have stopped resolving anything. Both assemble their path as<repo>/docs/<filename>at runtime, so this change would have silently emptied the framework tier (flask, django, spring-boot) and the entire language tier — all six languagesdetect_project_type()recognises. Runtime-assembled paths are invisible to a grep fordocs/<file>.md, which is exactly how the first reference sweep missed them. Both now readdocs/standards/, verified by loading all nine bundled documents._bump_version_and_changelog()wrote todocs/CHANGELOG-SYSTEM.md, a file that has never existed in this repo. The function exists to enforce the version-release-policy rule that every code push updates VERSION and CHANGELOG; instead it took theelsebranch on every single run and logged"No CHANGELOG file found - skipping changelog update", so the enforcement had never once fired. It now targets the rootCHANGELOG.mdthat rules/11 designates as canonical, emits a proper## [X.Y.Z] - DATE/### Changedsection instead of a bare bullet, and stages the file it actually writes.Four intra-
docs/cross-links were already broken before this change and are repaired now that link paths were being touched anyway — two pointed at aCODE_QUALITY_REPORT.mdthat does not exist, one resolved todocs/docs/, one to a rootCONTRIBUTING.mdthat lives underdocs/.Nine GitHub-relative links (
../../issues,../../discussions,../../issues/212-217) broke further when their files descended a level. They now use absolute repository URLs, which no directory depth can invalidate.CLAUDE.mddocumented a rootrules/directory holding "46 coding standard definitions". No such directory exists here — zero files tracked, nothing on disk. The Directory Layout section now describes the realdocs/tree.README.md's 15 documentation links andscripts/setup/setup_wizard.py's two references were repointed.Verification
fastapistill correctly falls through to the library-skill tier.Deliberately not in scope
The global
~/.claude/policies/vs repodocs/policies/content drift (11 of 28 shared filenames differ;proactive-consultation-policy.mdis 26 lines here vs 331 globally) is untouched — this PR only changes where files sit, never what they say. That reconciliation is a separate decision.🤖 Generated with Claude Code