Skip to content

v2.0.0 -- hook-free execution: delete the hook registrations, replace them with explicit commands and MCP tools - #255

Merged
piyushmakhija28 merged 72 commits into
mainfrom
docs/segregate-docs-tree
Aug 5, 2026
Merged

piyushmakhija28 merged 72 commits into
mainfrom
docs/segregate-docs-tree

Conversation

@piyushmakhija28

Copy link
Copy Markdown
Collaborator

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 at docs/*.md.

They are now grouped by kind:

Folder Contains Files
standards/ numbered rules 01-46 + per-language standards 52
policies/ one file per pipeline policy 46
reports/ point-in-time investigations, audits, migration notes 20
architecture/ ADRs, pipeline/level design, orchestration prompt 17
guides/ getting started, deployment, testing, runbooks 14
releases/ per-version design briefs and review notes 6
contributing/ issue/PR templates, CONTRIBUTING, CODE_OF_CONDUCT 5
api/, phase-1-architecture/ pre-existing 3

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.md is 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 languages detect_project_type() recognises. Runtime-assembled paths are invisible to a grep for docs/<file>.md, which is exactly 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 in this repo. The function exists to enforce the version-release-policy rule that every code push updates VERSION and CHANGELOG; instead it took the else branch on every single run and logged "No CHANGELOG file found - skipping changelog update", so the enforcement had never once fired. It now targets the root CHANGELOG.md that rules/11 designates as canonical, emits a proper ## [X.Y.Z] - DATE / ### Changed section 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 a CODE_QUALITY_REPORT.md that does not exist, one resolved to docs/docs/, one to a root CONTRIBUTING.md that lives under docs/.

  • 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.md documented a root rules/ directory holding "46 coding standard definitions". No such directory exists here — zero files tracked, nothing on disk. The Directory Layout section now describes the real docs/ tree.

  • README.md's 15 documentation links and scripts/setup/setup_wizard.py's two references were repointed.

Verification

  • A full audit of every relative link across all 165 tracked Markdown files reports zero unresolved targets.
  • Standards loader exercised end to end: all 3 framework docs and all 6 language docs resolve; fastapi still correctly falls through to the library-skill tier.
  • Suite green: 1238 passed, 47 skipped, 11 xfailed.

Deliberately not in scope

The global ~/.claude/policies/ vs repo docs/policies/ content drift (11 of 28 shared filenames differ; proactive-consultation-policy.md is 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

piyushmakhija28 and others added 30 commits July 31, 2026 21:31
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>
piyushmakhija28 and others added 23 commits August 2, 2026 15:30
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>
@piyushmakhija28

Copy link
Copy Markdown
Collaborator Author

This PR's title is now wrong, and that matters more than usual

It 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: v2.0.0 -- hook-free execution: delete the hook registrations, replace them with explicit commands and MCP tools


What changes for a user

The hooks are gone. PreToolUse, PostToolUse and UserPromptSubmit are no longer registered. Stop and Notification are retained byte-identical — they were never the plugin's. Pipeline execution now requires an explicit --invoked-by naming one of six slash commands, and a session in which no command is invoked runs no engine code and enforces nothing.

Every existing v1.x installation is affected and none upgrades silently, which is what makes this MAJOR. docs/guides/migration-v1.21.5-to-v2.0.0.md carries the eight-step runbook.

The replacement is a plugin (plugin/, zero hooks and zero bundled MCP servers by ADR-010 and ADR-019), the six commands, and user-scope MCP registrations installed by register-mcp — including the version push gate, ported from hooks/pre_tool_enforcer/policies/push_gate.py to src/mcp/push_gate/.

Ordering

sequencing_risks.md R-1 calls the hook deletion "the single most dangerous ordering in the plan" and blocks it on three things. All three landed first, and the order is visible in the history: the push gate as an MCP tool (0900fff), the CI assertion that fails the build when neither the hook nor the tool is reachable (f893fd2), then the deletion (2e371f6).

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.

Verification

Eight CI gates, all exit 0 — policy audit matrix, orphan dispositions, capability dispositions, plugin conformance, text-mode open() encoding, home-directory literals, push-gate reachability, fixed pipeline timeouts. Full unit suite exit 0.

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

docs/REVIEW-INDEX.md holds 51 corrections, each with its measurement. Four are open hazards a reviewer should know about:

  • The retained Stop hook attempts to open a pull request every response turn. run_pr_workflow exists at scripts/github_pr_workflow/versioning.py:264; two independent things — a sys.path line and a missing re-export — stand between it and running. A routine cleanup could arm it.
  • The durable checkpointer does not exist at runtime. use_sqlite=True returns an InMemorySaver, and the degradation is silent in three places. The contract now withdraws its durability claim rather than pretending; the backend is still absent.
  • Dockerfile and k8s/deployment.yaml read 1.6.1, outside sync-version.py's targets — which is the mechanism of the drift.
  • PRD NFR-1 / SRS NFR-7: build the per-component process-count measurement harness #259 (the NFR-1 process-count harness) is deliberately open. The harness is built; its measurement has never produced a pass, and the issue's own body forbids closing on that. Its blockers have now cleared, so the run is possible for the first time — it needs a live plugin install, ruled out during this sprint. Five executable procedures are written for whoever has that authorisation.

A note on the evidence

Almost 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

@piyushmakhija28 piyushmakhija28 changed the title docs: segregate docs/ into nine categorised folders v2.0.0 -- hook-free execution: delete the hook registrations, replace them with explicit commands and MCP tools Aug 5, 2026
piyushmakhija28 and others added 2 commits August 5, 2026 10:56
…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>
@piyushmakhija28
piyushmakhija28 merged commit 2f9276b into main Aug 5, 2026
4 of 6 checks passed
@piyushmakhija28
piyushmakhija28 deleted the docs/segregate-docs-tree branch August 5, 2026 08:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant