refactor: repository structure overhaul (65 tasks, DR-1..DR-12) - #1817
refactor: repository structure overhaul (65 tasks, DR-1..DR-12)#1817rsalus wants to merge 91 commits into
Conversation
Sequences comment-hygiene-enforcement against exarchos-repo-structure-cleanup instead of running them concurrently. They collide on every output path: scripts/, eslint-rules/, skills-src/ and every remediation target under servers/exarchos-mcp/src/ are moved or deleted by the structure refactor. Split line is logic before, registries after. The classifier, policy datum, extractor and fixtures are path-independent and land first; every baseline, budget, manifest entry and CI step is keyed to paths the refactor destroys and lands after Phase 1. Corrections, each verified against the tree: - The extractor comment-hygiene planned to relocate does not exist. No comment-prose path is tracked and HEAD contains neither extractCommentProse nor collectComments; the module lives only in stale agent worktrees. governing-catalog.test.ts is real but imports none of it. Task 003 authors the extractor rather than moving it. - Worktree debt is 66 worktrees (64 agent-*) and 408 local branches, not the two named. The two named do not exist, and git worktree prune reports nothing because every registered directory is still on disk. One of those worktrees holds the only copy of the unlanded extractor, so removal is gated on an unmerged-work check. - azd-templates/ and event-taxonomy-and-dkg/ are untracked top-level directories in neither the allow-list nor the deletion list, so the DR-1 test would fail on a working machine while passing on a pristine clone. Two folds remove duplicated work: the decomposition wave (048-051) rewrites the same six hotspot files comment-hygiene names for remediation, so it takes the comment criterion directly; and task 042 absorbs the ESLint scope widening, since retargeting guards onto the new tree is already its job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… code
The census behind the comment-hygiene spec found 7,456 comments citing a
planning artifact by identifier, with spec-citation lines growing 29-fold
between February and May 2026. The upstream source is the harness itself:
task-template.md and implementer-prompt.md hand every dispatched agent an
`Implements: DR-N` stamp and a task ordinal, and nothing tells the agent those
belong to the artifact rather than to the source.
State the rule where the code is authored. A comment states its constraint in
words and names no planning ordinal — DR-N, task N, T-N, wave N, slice N,
epic #N, INV-N, or a docs/{specs,designs,plans}/ path. Durable external
references (URL, owner/repo#123, CVE, RFC) are unaffected, since they name
something outside this repository's planning cycle.
The provenance chain is unchanged and still reads `implements: ["DR-N"]` off
the task.completed event, which is the point: the ordinal already has a home
that a reader can resolve and that gets updated when the plan is revised. A
comment repeating it adds nothing and rots on its own.
Ordering matters here. The repo-structure-cleanup refactor dispatches roughly
65 tasks through these exact templates; landing this first keeps that refactor
from manufacturing thousands of fresh citations across the whole moved tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…classifier
Lands the path-independent half of the comment-hygiene design: the parts that
survive the pending structural refactor untouched. Ships NO enforcement — no CI
step, no manifest entry, no baseline file, no enabled ESLint rule — so the
refactor's guard-liveness baseline stays clean and none of this lands inside
its rename-detected phase.
Extractor (scripts/lib/comment-prose.mjs). Authored, not relocated: the module
the design planned to move does not exist in tracked HEAD. Adjudicated the
unlanded worktree copy and kept its parse-based core — the literal-span walk
and the recovered-parse refusal — then added the per-comment positions the gate
and the rule both need. It parses rather than scans because a scanner cannot
resume a template literal after a substitution and re-reads the tail as a
comment, inventing prose the file does not contain.
Policy datum (.exarchos/comment-policy.json) and loader. One authority; every
consumer derives from it and neither carries a pattern of its own. The loader
fails closed on a missing file, malformed JSON, an expired waiver, or a
structural exemption that carries an expiry — a guard running with no rules
reports a clean tree forever, which is worse than an error. Structural
exemptions and dated waivers are separate types with different validation.
Classifier, with allowed references taking precedence by span rather than by
comment, so a permalink carrying an ordinal-shaped fragment is permitted while
an unrelated ordinal beside it is still caught.
Measured before enabling anything, over 1,885 files with zero indeterminate:
- 8,700-odd ordinal matches, corroborating the design's 7,456 regex estimate
with a parse-based count.
- passive-change-verb ships DISABLED at 42/50 (84%). Its false positives are
one coherent class — the verb describing a condition rather than this code's
history ("if `@proof` were renamed", "a reducer that was deleted still
boots").
- `no longer` ships DISABLED at roughly 50% over 22 adjudicated. Found 316
matches against the design's stated 321, so its volume was never the doubt.
- Added task-shorthand-padded after finding the hyphenated pattern missed the
tree's dominant form: citations are written T034, not T-34. Requiring three
digits keeps it clear of T0 timing and T1/T2 type parameters, both present
here. 40/40 across an evenly-strided sample.
Kill fixtures carry verbatim text from the tree. The atomic-write comment is
the argument for judged rewriting: it states its constraint fully and is an
offender only because it opens with an ordinal, so stripping mechanically would
take the reasoning with it. Its rewrite is the permitted counter-case.
The ESLint rule holds no pattern and is deliberately unregistered; wiring it is
Stage 2, after the tree settles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-dependent Task 001 demands a green clean clone and forbids an accepted-red escape, but green is not a single fact here. Measured at 2306dd2 with both dependency trees installed: the root suite is fully green, 167 of 167 files and 1,690 tests, and both typechecks pass, while the nested MCP suite reports 26 failures across 9 files, all in merge-orchestrate and store.race. CI is green on the same tree — the CI Gate run for 355ffd6 succeeded, as did the two before it. The task cannot be executed as written, so the amendment names the two honest resolutions: pin the oracle to CI and record the local delta by name, or fix the local-only failures so the environments agree. Capturing locally without deciding would bake 26 failures into the baseline every later reconciliation compares against, where they are indistinguishable from damage the refactor caused — the exact corruption this task exists to prevent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Green was not a single fact here: the root suite is green locally and on CI, while the nested MCP suite fails 26 tests locally that CI passes. The oracle is pinned to CI against a named run, and the local delta is enumerated rather than carried as an unrecorded difference — 26 unexplained failures in a baseline are indistinguishable from 26 the refactor caused, and every oracle built on this one would inherit that ambiguity. The exclusion states what voids it. A listed test failing on CI makes the failure real; an unexplained failure outside the list blocks. The accompanying test holds the enumeration honest: headline count equals the list, every excluded file still exists, every excluded test name is still present in its file, and no exclusion sits outside the merge-orchestrate cluster the justification covers. An exclusion that outlives its subject is permanent cover for whatever moves in next. The Windows leg is recorded as outstanding rather than dropped. This capture is Linux plus CI, and an oracle silently covering one platform would let a Windows-only breakage read as clean. Also fixes a collection hole this task walked into: tests/architecture matched no vitest project include, so this oracle and the three that follow would have passed by never running — the vacuous-guard class the design calls its highest risk, reached through the oracles themselves. Adding the include is a Phase 0 config change, so Phase 1's zero-semantic-edit property is untouched, and it moves the runner toward the target tree. Verified by the collected count rising from 1,645 to 1,654, not by reading the config. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records what all 27 guard and governance surfaces currently match, across 3,311 tracked files: both ends of the live error-severity dependency-cruiser rule, all five CODEOWNERS patterns, every package.json files[] entry, the 51 protected test suites, the invariants catalog references, the three lint scopes, and both knip workspaces. The refactor rewrites nearly all of these, and the failure they share is silence — a glob that resolves to nothing passes forever and reads as success. One genuine dead surface on main: package.json files[] declares CLAUDE.md.template, which does not exist. It is listed as a known defect with the task that removes it, and the assertion is an equality rather than a subset so fixing it forces the list to shrink instead of leaving standing cover. The instrument lied three times before it was right, each time claiming an evaporation that was not real, and each time caught by the accompanying test rather than by reading the output: - protected-suites entries are already repository-relative, so joining them with generatedFrom double-prefixed all 51 and reported a confident zero. - scraping every markdown list item ending in a code extension also collects applies-to entries, where `format.ts` names a concern rather than a file. - catalog references carry anchors, so `runtime.md#§4` resolved as a literal path and reported all eight deep links broken. That the errors all pointed the same way is the useful part: a liveness instrument fails toward false alarm, which is survivable, but the same carelessness pointed the other way produces a guard that reports health it never measured. Hence the partial-resolution check — a surface declaring N and resolving fewer has half-evaporated, which a bare non-zero count would hide — and the seeded-empty-surface test that proves the assertion has teeth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Counts live referrers into every subtree the design plans to delete or re-home, across 3,196 of 3,314 tracked files. Deletion is the one step with no cheap undo, so it is gated on measurement rather than on the plan's intent. The finding is that the exodus cannot proceed as a bulk move. Of the 16 prose subtrees, 12 carry live references and only 4 are clear — docs/audits, bugs, refactors and market, 51 files of 466. The blocked remainder is not marginal: docs/designs alone has 115 live referrers including 76 from source, and there are 173 code referrers across the delete candidates. Tasks 037 and 037a become per-subtree and reference-triaged rather than one move. A referrer counts as live when a reader or a tool would follow it — source, config, snapshots, and instruction markdown outside docs/. A dated record under docs/ that mentions a path is history, and rewriting it to survive a refactor would falsify the record it exists to keep. That distinction is what lets docs/audits clear despite carrying ten archival mentions, and the test pins the case so the split cannot quietly collapse either way. The scan surface is wider than source on purpose, covering the three classes that produce a confident false zero: markdown wherever it currently lives rather than under a destination that does not exist yet, .snap snapshots that embed paths as plain text, and extensionless governance files enumerated by name because no extension filter can see CODEOWNERS. Each is asserted to have been reached, so a glob that stops matching shows up as a failure rather than as a cleaner-looking census. The cleared list is a ratchet: as references are retired the set grows and the list must grow with it, so nothing is deleted against a stale measurement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
package.json declared `workspaces: ["packages/*"]` against a directory that does not exist, and shipped `CLAUDE.md.template` in files[] against a file that does not exist. Both are deleted rather than repointed: the nested server package carries its own lockfile and is deliberately not an npm workspace, and nothing generates the template. A glob matching nothing is not inert. npm resolves an empty workspace set, so the declaration silently describes a layout the repository does not have, and a reader trusts it precisely because it is committed. The existing assertion was itself the stale artifact. WorkspaceConfig_RootPackageJson_HasWorkspacesField REQUIRED the dead glob — it was written when packages/create-exarchos existed and outlived its subject, so it had been pinning the defect in place. It is replaced by a check that every declared workspace glob resolves to a real directory, which holds for an empty list and fails for a dead one. This empties the guard-liveness known-dead list. The equality assertion there is what forced the list to shrink rather than keep carrying a defect that no longer exists. Root suite green at 1,714 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…line without the pin Task 008 no longer removes anything. Of 67 registered worktrees, 66 carry commits absent from origin/main — several 60 to 74 deep — and 387 of 408 local branches are unmerged. One held the only copy of an unlanded comment-prose implementation, found while looking for something else, and there is no cheap way to know which others are similar without reading them. An inventory is reversible; a prune is not. The ahead-count is recorded with the caveat that makes it honest: this repository squash-merges, so a fully-shipped branch still reports commits ahead of the base. The number overstates unique work. That bias is safe for an inventory and disqualifying for a deletion warrant, and separating shipped from unshipped needs a patch-level comparison this deliberately does not attempt. The test also pins that this session's own worktree is among the records, which is the concrete reason a prune here is self-destructive. Task 001a captures nine benchmarks with environment metadata. The noise band is derived per benchmark from its own measured relative margin of error rather than from one global percentage: the spread across these nine is roughly thirtyfold, so Append_100Events_Sequential at ±14.89% cannot resolve a 10% change while Materialize_1000MixedEvents_PipelineView at ±0.52% can. A single threshold would wave through real regressions in the stable benchmarks and cry wolf on the volatile ones. The file states its own limits — one run, one workstation, within-run rather than run-to-run variance. A prior baseline did exist, contrary to the design's claim that none was ever recorded: benchmarks/baselines.json from 2026-02-16, two benchmarks, no environment or noise definition. It is acknowledged rather than quietly duplicated. The binary pin is deferred and says so. It needs a released build — the installed one predates the WFQ-006 gate fix, and a local build is not reproducible by anyone else. Phase 0 is safe unpinned because nothing here moves product source; Phase 1 is where it becomes load-bearing, and the gate is recorded and asserted so the gap cannot read as done. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t ids Records 12,564 test cases across 1,138 files, 0 unparseable. Identity is (suite path within the file, test name, runner); the file path is metadata and never identity. Keying on path would invalidate the whole oracle on the first move — strict comparison would report every test missing and loose comparison would conflate the many identically-named tests in different files. The relocation map starts empty and every move task appends to it; reconciliation is oracle minus relocations, and an unexplained delta names the missing source. Discovery is by extension over tracked files rather than by a runner glob, because a glob is precisely what goes stale — four oracles earlier in this workflow were collected by no vitest project at all. Parsing rather than running also lets the oracle be captured while part of the suite is red, and picks up the 45 shell suites vitest cannot see. Counting semantics are stated rather than left to be discovered: cases are call sites, not expanded executions, so a table-driven it.each is one entry here and N tests at runtime. That is why the total sits below the runners' combined count, and unexplained the gap would read as roughly 800 missing tests. The call site is the right unit anyway — it survives a move, whereas an expansion index churns on any edit to the table data. Modifier chains are unwrapped (it.each, describe.skipIf, it.concurrent.only) or every modified test would drop out silently. Computed titles are marked as dynamic rather than guessed at, since an invented title produces an id that reconciles against nothing. Root suite green at 1,735 tests; both typechecks clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d value
`docs/specs/` and `docs/designs/` were two module-private literals inside
rehydrate.ts, so the artifact location was a property of the classifier rather
than of the project. They now have one owner, `config/artifacts.ts`, and reach
`classifyArtifactLayout` by injection — the classifier stays pure, and omitting
the argument is byte-identical to what shipped.
The spec assumed `planArtifactExists` held a directory literal to replace. It
does not: the guard is `typeof value === 'string' && value.trim().length > 0`
and has never touched a path, so it is directory-agnostic already and needed no
change. The planned `PlanArtifactExistsGuard_ConfiguredPrefix_ResolvesAgainstIt`
test would have asserted behaviour that does not exist; it is dropped rather
than faked.
A blank configured prefix falls back to the default instead of being honoured —
an empty prefix `.includes('')`-matches every path, which would classify every
workflow as unified and strand in-flight two-artifact work on the wrong path.
The scan test carries two tiers because the failure modes differ: functional
uses (path construction, prefix comparison, directory constants) are down to a
closed allowlist of two files, while the ~10 agent-facing prose mentions are
pinned as a budget — they shape agent behaviour without gating it, and the docs
move retires them later.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s not The docs exodus mounts the artifact directory as a symlink out of the tree, so a consumer that must actually read the directory now has `resolveArtifactDirPath` to follow the link. It is total on a missing or dangling target — it returns the unresolved path rather than throwing, precisely so nothing here can be mistaken for an existence check. That is the property the load-bearing test defends. Existence is the event projection's answer, never a filesystem stat, and the test pins the process cwd at an artifact-less repo while rehydrating. Without that pin the handler never sees the directory at all and the test would pass for the wrong reason; with it, a mutant that resolves `workflowExists` from `existsSync` fails three assertions, including both named cases. The mirror case is checked too: a directory full of specs must not conjure a workflow that was never started. Classification is deliberately untouched by any of it. The recorded artifact path stays repo-relative wherever the directory physically lives, so the prefix match never learns the link exists. Authored beside the code rather than at the plan's `tests/integration/` path: no vitest project collects `tests/integration/**`, so a test there would pass by never executing — and this one needs the MCP workspace's `bun:sqlite` alias to construct an EventStore. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…iling The knip gate was RED: two comment-hygiene kill fixtures landed on this branch without exemptions. They are read with readFileSync and never imported, which is exactly the class the ledger exists for, so they get rows following the swallows.fixture.ts precedent — not a knip `entry` or `ignore`, since both would change what the graph treats as reachable and the fixtures' whole value is being data the detector reads. Gate now passes: 105 findings, all allowlisted, none expired, none stale. The sweep found less rot than the plan assumed and a different kind. All 103 pre-existing entries already carried an owner, a deadline and a rationale, and knip still flags every one — nothing stale to delete. What the schema cannot see is whether a rationale is a reason, and 59 of them state no condition under which the entry could ever be removed; 45 say "forward-compat surface" and stop. Those are pinned, not fixed. Every one is an exported TYPE and the plan gates deletions on the task 004 census, so retiring them is a census-gated sweep. Padding the prose would move the measurement without moving the claim. The two tests the plan names were already covered in substance by knip-diff.test.ts, so these are additive: a rationale floor set AT the measured minimum (49 chars) so it binds, a stub-rationale reject, a dangling-file check, a duplicate check, permanence that has to argue for itself, and budgets on both ledger size and the un-retirable residue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ping tests The DR-30 suite invariant computes scope from ASSERTION SHAPE, not from the annotation, so both new tests were in scope the moment they swept a real corpus — and neither declared its authorities. That was the whole delta between the task 001 baseline's 26 known-local failures and the 28 this branch produced. Both now name two genuinely independent authorities. artifacts.test.ts pairs the shipped resolver against the hand-transcribed pre-extraction expectation table, which reads nothing and so cannot agree with it by construction. The symlink test pairs the resolver against the operating system itself — the reason it builds a real link instead of mocking realpathSync. `../workflow/rehydrate.ts` is deliberately not declared anywhere: it imports config/artifacts.js, so naming both would be one authority wearing two names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adding `artifacts` to the resolved config without adding it to buildConfigDescription meant `exarchos_workflow describe config:true` reported every section except the new one. DescribeConfig_AllSectionsPresent exists to catch exactly that drift — it compares the description's keys against DEFAULTS — and it did. Nested suite is back to the task 001 baseline: 26 local failures, all merge-orchestrate/store.race, none from this branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…remises
The checklist still read "Ready for plan-review" while the gate had been waived,
which is exactly what makes a resuming agent re-derive the question. It now says
so, and points at the decision on the workflow stream.
Tasks 005, 006 and 009 each carried a premise the tree contradicted, so each
gets its correction beside the task rather than only in a commit message:
005 — planArtifactExists holds no directory literal; it is a non-empty-string
check and needed no change. The functional coupling was 5 sites, and a
scan test cannot be absolute while ~10 agent-facing prose mentions exist.
006 — the stated tests/integration/ path is collected by no vitest project, so
a test there passes by never executing. Any later task naming that path
inherits the bug.
009 — the ledger was already justified and the gate was already red, which is
the reverse of the assumption. 59 entries remain un-retirable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every later Phase 1 move task reads tools/audit/layer-map.json to decide where a directory goes, so this is what stands between "the map is the plan of record" and "each move invents a placement." 55 directories: 44 mapped onto one of the 11 targets, 11 carrying a stated exception with a reason. The plan said ~60 directories; the tree has 55. The 11 targets -> 9 published layers relation holds exactly as the plan described: L5 is served by contract/ AND dispatch/ (the contract is asserted independently of the dispatcher that honours it), and install/ is a declared non-layer peer rather than a tenth layer. Task 044 asserts that relation, so the test asserts it as a relation and not as set equality. Both open questions resolve here. `evals/` is NOT a layer — it measures the engine from outside, and code that measures the engine must not sit in the engine's call graph. `install/` is the peer described above. `utils/` is recorded UNRESOLVED rather than forced: atomic-write, paths, process and task-id have consumers in five layers, and deciding who owns each helper belongs to task 020 where the moves happen. A bucket chosen now would only have to be undone. Scope is read from disk, never from the map — a map that simply omitted a directory would otherwise be self-consistent and wrong. Two name traps are called out in the doc because they read as product source and are not: commands/ is three prose tests, runtimes/ is four adapter tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing it The plan's end state is "one product package plus explicitly-declared tool packages" — NOT one lockfile, and NOT one test tier. The second reading is the dangerous one: the core suite runs ~900 files under a 60s per-test budget chosen for the Windows runner (#1620), and folding it into the root unit tier's 5s budget would fail healthy tests by lottery there. So the unification is declarative. The four manifest/lockfile pairs stay four, each now classified with a role and a reason, and the policy that a later collapse could silently drop is written down and checked. Two of my own assumptions were wrong and the config was right. The bun:sqlite alias is declared TWICE — the core tier for unit tests, the root outcome tier because it drives real MCP handlers — so "resolves once" is about the SHIM, not the declaration count. The test now asserts what actually matters: every declaration resolves to the SAME shim, because two shims would mean tests running against different SQLite bindings depending on which tier collected them, surfacing as a storage bug rather than a config one. The coverage baseline moves to tools/audit/ with the other audit oracles. It sat inside the workspace it measures, and Phase 1 relocates that workspace — a ratchet that has to move whenever the code moves is one more thing that can quietly stop governing. Both consumers repointed (check-coverage-ratchet.mjs and check-base-substrate.ts); the ratchet's own 24-case self-test still passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… not orphaned Both third and fourth manifests are retained, each under a stated reason rather than by default. evals-pkg stays. It exists so the heavy eval-only promptfoo dependency stays OUT of the product install closure (DR-3), the graders resolve it from that package at runtime, and ci.yml names it in the prompts: paths-filter so a change there still fires RUN_EVALS. Retiring it would delete a live capability and orphan the filter. documentation/ stays too, scoped to task 039, which owns what that tree becomes. The test closes the loop the plan asked for: every tracked package.json is the product, a declared tool, or explicitly retired — there is no fourth state, so a fifth manifest cannot appear unnoticed. A retired package must also leave no CI paths-filter behind, because a filter naming a package that no longer exists is a gate that can never fire, and a gate that never fires reads as green. One assertion earns its place beyond the plan's wording: promptfoo must not appear in either product manifest. That is the entire reason evals-pkg exists, and it is the failure that would otherwise be invisible until every install got slower. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ions/ Pure relocation per the task 010 map: event-store/ becomes events/; artifacts/ folds into storage/; views/, telemetry/, quality/, session/ and task-store/ fold into projections/. 216 files moved, 1073 import specifiers rewritten, both typechecks clean. The rewrite is arithmetic, not textual. Each specifier is resolved against its file's OLD directory, mapped through the move table, then recomputed relative to that file's NEW directory — because a file that itself moved needs its specifiers changed even when the target did not, and the textual-prefix approach gets exactly that case wrong. vi.mock paths are rewritten too: a stale mock path silently mocks nothing. Retargeting the oracles is most of the work, and is the part a "pure move" hides. The live depcruise rule is repointed in the same change — leaving it naming event-store/ would have silently disabled the domain-core boundary rather than failing. protected-suites.json is REGENERATED through its own --regenerate engine rather than text-substituted, after a hand rewrite produced entries the discovery walk did not agree with. check-protected's KEEP_CLASS_AREAS and KEEP_CLASS_EXPLICIT, and the allowlists in check-module-intent, check-single-workflow-fold and check-query-upcast-choke-point, all keyed on src-relative paths that the src/-prefixed sweep did not reach. The layer map is now regenerable: tools/audit/measure-layer-map.mjs joins the other four measure-* instruments, because a map that must be hand-edited after every move is a map that will be wrong after some move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ated Two scanner self-tests build synthetic trees under the old directory names, so they kept proving the scanners work on a layout that no longer exists. Root suite is back to fully green: 177 files, 1789 tests, 0 failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The MCP suite is back to its pinned baseline: 26 failures, all
merge-orchestrate/store.race, ZERO from this move. Root suite 177 files / 1789
tests green, both typechecks clean.
Retargeting was four distinct classes, not one, and only the first was a string
substitution:
1. Quoted file paths in registries and tests. Restricted to specifiers ending
in a real source extension after finding that `task-store/${task.taskId}`
is an EVENT STREAM ID — a blanket rewrite would have silently repointed
every task stream while every test still passed.
2. Segment-built paths: path.resolve(__dirname, '..', 'views') is invisible to
any specifier regex.
3. DEPTH arithmetic inside files that moved a level deeper. The codemod fixed
their imports but not their `../../../..` walks, so two tests silently
resolved outside the repo.
4. Layer VOCABULARY. layerOf() reads the first path segment, so folding five
directories into projections/ merges five layer identities into one.
That last one produced the finding worth keeping. The projections allowance is
now the widest in the table — 15 targets — because the layer carries the union
of five directories' import surfaces. Every one of those edges existed before;
they were invisible while the five had separate names and their coupling read as
ordinary cross-layer traffic. That the read side reaches orchestrate and
workflow at all is the real result. Phase 1 is a pure move with zero semantic
edits, so it is RECORDED rather than narrowed, and the row is what keeps it
measurable.
Editing .exarchos/invariants.md broke the contract authority freeze, which is
the freeze working: the catalog is a pinned authority and its digest is
re-approved by running the generator, never by hand-editing a digest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
no-domain-core-to-io-adapters is severity: 'error' and really executed by the static-analysis gate. Its `from` side is a path REGEX naming directories, so a rename does not break it — it EMPTIES it, and an empty rule passes forever while looking exactly like a rule being honoured. The half-dead case is the one worth catching: `(events|workflow)` stays syntactically valid when one half is deleted, and the surviving half keeps the match count non-zero, so counting matches cannot see it. The test therefore checks that every directory the alternation NAMES exists. Kill-probe: reverting the alternation to `event-store` fails exactly that assertion. Deliberately does not shell out to depcruise — it needs ~4GB and the real gate already runs it. What can go wrong here is the regex ceasing to describe the tree, which is checkable directly. Seeded violations are built from REAL live module paths rather than invented strings, because a synthetic path can satisfy a regex no actual file would, and the negative half is asserted too: a rule that flags everything is as useless as one that flags nothing. Phase 1 renames both sides of this rule at least three more times (013, 018, 019). This is what makes each of those loud. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
topology/ becomes workflow/topology/: the phase-contract loader and staleness signal are workflow-primitive data, not a layer of their own. Small move — 19 specifier rewrites across 13 files — and the interesting part is again the oracles. topology was a declared FOUNDATION LEAF in the layer table, so folding it made two rows stale at once: its own leaf row, and `pruner -> topology`. Both had to move together, and the phantom-cover half of the seam census is what surfaced it — an allowance no live edge exercises is exactly as wrong as a missing one. The depcruise liveness test from 012a earns its keep here: this move leaves the `workflow` half of that rule's alternation intact, and the test confirms rather than assumes it. MCP suite back to its 26-failure baseline with zero non-baseline failures; root suite 178 files / 1799 tests green; both typechecks clean. 48 directories now, down from 55 at the start of Phase 1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
core/ folds into dispatch/core/ — context.ts and dispatch.ts ARE the single
dispatch function L5 describes — and schemas/, sdk/ and shared/ fold into
contract/, all three being contract surface. 486 specifier rewrites from the
codemod plus 562 from the path sweep. 44 top-level directories now, from 55 at
the start of Phase 1.
This move found two REAL regressions the previous waves' oracles could not, both
in the same blind spot: strings that are module paths but sit in no import
position, so the codemod never resolves them.
- `vi.doUnmock('../core/dispatch.js')` and the ARGUMENT of
`vi.importActual('../core/dispatch.js')`. The codemod rewrites vi.mock and
vi.doMock; it did not rewrite these. The un-mock therefore named a module
that no longer exists, silently left the per-file dispatch mock in force,
and the CLI end-to-end tests ran against `{ mocked: true }` — returning
success with empty data rather than failing. Typecheck cannot see it; both
tests passed at task 013 and failed here, which is what made it findable.
- A fixture written into a temp dir under the package root, whose import
specifier the codemod "corrected" for the fixture FILE's new depth even
though the fixture is written somewhere else entirely.
Both argue for the same instrument, and it is now written: a scan that resolves
every quoted relative specifier against its own file. It reports 100 candidates
of which most are deliberately-synthetic fixture paths, so it is a diagnostic
rather than a gate — but it is what turned an invisible mock into a two-line fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six move tasks remain (015-020) and both tools are needed for each. The codemod carries its own blind spot in its header, because that is the part that cost a real regression: it rewrites imports but not module paths sitting in non-import positions, and a stale vi.doUnmock leaves a mock in force while the affected tests keep PASSING. The specifier scan is a diagnostic rather than a gate — a good share of its 97 current hits are synthetic fixture paths that are supposed not to resolve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s the regroup safe The plan names the reason this task needs a snapshot at all: a dropped registration COMPILES CLEAN. Typecheck passes, the import graph is intact, and the action answers UNKNOWN_ACTION only at runtime — which no unit test covering that handler in isolation will ever notice. So the net goes in BEFORE the move, capturing the pre-move truth: 123 registered action ids. It covers both halves of the trap, because they fail independently. The registry can advertise an action no composite routes (the known UNKNOWN_ACTION gap), and a composite can route one the registry never advertises. The orphan check reads real `case` branches out of the five composite files and is given a positive and a negative control, so a scan that silently matched everything would fail. The kill probe is part of the suite rather than a one-off: a snapshot nobody has proved can fail is one that eventually gets regenerated to match a regression. The 82-file capability regroup itself is NOT in this commit. The clean families classify readily (gates 44, team 15, review 13, merge 13, tasks 11, vcs 10) but 102 files resist a prefix rule — `plan-coverage`, `mutation-adequacy` and `design-completeness` are gates under names that do not say so — and placing those is per-file judgment, not a regex. Doing it badly would bury the locality defect under a taxonomy nobody trusts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…etarget UNFINISHED
INCOMPLETE AND KNOWN RED. Committed rather than reverted because the move itself
is correct and large, and the remainder is a known, mechanical list.
Done and verified:
- 383 files moved out of the flat orchestrate/ directory into
verbs/{gates,review,tasks,team,vcs,merge}/ plus the pre-existing
doctor/init/invariants/onboard/pure/worktree subtrees. 22 files stay at
verbs/ root: the dispatcher, two shared state helpers, and the cross-cutting
integration tests that belong to no single capability.
- Both typechecks clean.
- The registration snapshot is GREEN: all 123 action ids survived. That is the
proof the plan asked for, since a dropped registration compiles clean.
- The reachability graph regenerates to a BYTE-IDENTICAL content digest
(sha256:bd62c535…), so the contract surface is provably unchanged.
- 1062 + 1404 + 323 path/specifier rewrites across three sweeps.
Remaining: 14 oracle files beyond the 26-failure baseline, all holding
`orchestrate` as a layer token or a path constant the sweeps did not reach —
dev-catalog-content, layer-boundaries-seam, reachability dispatch-routes and
kill-fixtures, poc.acceptance, four gates/*, task-decomposition (x2),
prepare-delegation, retirement-safety, suite-invariants.
Two ledger areas already had to move with the code (`orchestrate/` -> `verbs/`
in effect-ledger and the effect-provider map) — the reachability regenerator
fails CLOSED on that drift, which is how both were found.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ck to target
The 82-file capability regroup (`orchestrate/` -> `verbs/{gates,review,tasks,
team,vcs,doctor,init,invariants,merge}/`) landed as wip in 3722bfc with its
oracles unfinished. This closes them.
MCP suite: 47 failures / 23 files -> 26 failures / 9 files, which is exactly the
pre-existing baseline (all merge-orchestrate + store.race, zero move-induced).
Root suite: 8 failures / 5 files -> 178 files / 1799 tests fully green.
The retargets fell into the four known classes:
depth arithmetic — files that moved one level deeper computed a repo root
one short, landing on `servers/` (task-decomposition, prepare-delegation,
gates/static-analysis, and the fixtures test's plan corpus).
path constants — reachability's dispatch-routes and kill-fixtures named
`orchestrate/composite.ts` and `orchestrate/mutation-adequacy.ts`. The kill
fixture copies the router's imported sibling into a scratch tree, so the copy
now mirrors the router's own `./gates/` specifier rather than sitting flat.
layer vocabulary — `layerOf()` reads the FIRST path segment, so the layer is
now `verbs`. The allowance table already said so; the unit test and two
planted fixtures still said `orchestrate`.
registers keyed by path — the DR-30 debt register and the INV-13 scope glob.
Two findings worth naming, both cases of a sweep rewriting a string it should
not have:
The sweep collapsed five distinct debt-register entries into duplicates
(execute-merge, probes, invariants/parity, onboard/index, static-analysis
.parity all exist twice in the tree under different families). Cardinality is
restored at 298 with zero duplicates, so the ratchet is intact.
Two oracles had gone VACUOUS rather than red. `new-project-removed` imported
`../orchestrate/new-project.js`, which now rejects because the directory is
gone — it would pass even if the module resurrected as a sibling. And
`sidecar-removal` forbade the token `from '../orchestrate/sidecar-lookup`,
unmatchable after the rename. Both now assert against the real location, and
the sidecar token is path-agnostic so a future move cannot silently disarm it.
Generated inventories regenerated through their own engines, never by
substitution: layer map (its MAP key is the live directory name, so
`orchestrate` -> `verbs`), reachability graph, protected-suites, test inventory
(after `git add` — the oracle sees tracked files only). The invariant catalog
edit re-entered the authority freeze via authority-lock-cli.
The verb-registration snapshot's 123 action ids are byte-identical; only its
provenance `tree` stamp moved. No handler stopped registering.
Synthetic fixture strings that merely contain the word `orchestrate` inside
generated file CONTENT are deliberately left alone — the token there is an
arbitrary placeholder, not a claim about the tree, and rewriting it would move
the resolved target those gates assert ownership against.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cli-commands/` becomes `lifecycle/`: 18 files, 23 specifier rewrites through the arithmetic codemod. Both typechecks clean, root suite 178 files/1799 tests green, MCP suite at its exact 26-failure baseline. The oracles that keyed on the old directory: effect-ledger owns two AREA rules (`cli-process-owner`, `cli-fs`) whose keys are path prefixes. The ledger fails closed on drift, so a missed key is a hard stop rather than a silent hole — retargeted to `lifecycle/`. The composition-root allowlist named `cli-commands/subagent-stop.ts`. Its sibling entry `cli-commands/assemble-context.ts` names a file deleted before task 015 and is dropped. The failure message had been restating the allowlist by hand and had drifted from it in both directions — naming the dead file while omitting three live entries — so it is now DERIVED from ALLOWLIST and the header comment points at the set instead of copying it. A third guard had gone vacuous the same way the two in task 015 did: the #1476 enforcement-excision sweep forbids `cli-commands/guard`, `.../gates` and `.../subagent-context`, none of which could match again after the rename — a resurrected handler would land under the new name unseen. Retargeted rather than broadened, because a bare `gates` token would now collide with the real `verbs/gates/` directory. Also fixes a latent feedback loop in the reference census, surfaced by regenerating it. The instrument reads every tracked .json, including its own report, and the report embeds referrer paths verbatim — so on the run after any write it counted ITSELF as a live `config` referrer of the very subtrees it measures, flipping two of them from deletion-cleared to still-referenced. Its output is now excluded from its own scan, on the same reasoning as the existing sibling-reference exclusion: a record OF references is not a reference. The first fix attempt reproduced the loop one level up, because the explanatory comment quoted the subtree paths and prose in that file is scanned too; the comment now names none. Generated artifacts regenerated through their own engines: layer map (its MAP key is the live directory name), embedded runtimes, reference census, protected-suites, test inventory (after `git add`). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured before starting the move rather than discovered during it. Task 030 was written as "the largest single mechanical operation in the plan". It is not mechanical. 138 of the 907 movers derive a filesystem path from their own location, across 291 occurrences, and two different things hide in that number. A test anchored at the repo root only needs more `../`. A test whose anchor IS its subject needs the opposite: `adapter-direction.test.ts` scans its own directory precisely because that directory is `src/adapters/`, so moving it makes it scan the test tree, find nothing, and pass. That would be the third guard this workflow has turned vacuous rather than red, so the rule is written down as a computation instead of a judgement call — preserve what each expression resolved to before the move. The other four corrections: rewrite on the AST, because one file carries `import.meta.url` inside template strings that are code samples the test asserts on; closure runs both directions, because a traveler is imported from outside the move set; `protected-suites.json` is at a different path, does not exist, and its guard runs in neither CI nor package.json; and the relocation ledger is a field inside the inventory baseline that the generator re-emits empty on every run, not a new file. Also states the verification that catches the failure mode premise 1 describes: per-test status diff across the move, never a pass count. A test that goes vacuous leaves the totals untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
…ot paths
Task 030. The spec called this the largest mechanical operation in the plan and
the pre-flight measurement corrected that to "not mechanical". Execution
corrected it again: the rule the spec settled on — preserve what each path
expression resolved to before the move — is right for a directory anchor and
wrong for the file itself.
Three subjects do not stay put. A test that reads its own source travels with
the file, so preserving the old path aims at nothing. Fixtures travel in
LOCKSTEP, because the mirror mapping keeps the test-to-fixture offset identical,
so `join(__dirname, '__fixtures__')` wants no edit at all. And a bare anchor used
to scan its own directory has no literal segment to adjust, so it silently comes
to mean the new directory. The rule that covers every case is PRESERVE THE
TARGET, NOT THE PATH: leave anchors alone, recompute the literal segments
against where the target ended up, and pin the declaration outright where there
is no literal to recompute. Eight anchors are pinned; the other 145 expressions
just had their segments recomputed, which also keeps the resulting code
idiomatic instead of wrapping every anchor in a URL round-trip.
Only `adapter-direction` failed loudly when this was got wrong — it carries a
companion "the rule has a real subject" assertion. The other four went vacuous,
which no per-test diff can see. That asymmetry is the argument for the
denominator assertions this tree keeps adding.
Verified per-test, not by count: `vitest --reporter=json` before and after, every
test id mapped through the move and required to keep its status.
Four things the move exposed that were already broken:
- `vi.doUnmock` was absent from the codemod's mock-path list. A missed unmock
is SILENT — the module stays mocked and the assertions measure the stub.
- The composed-path Stryker smoke test has been red since task 019's fold
rewrote `servers/exarchos-mcp/src/` to `src/` and collapsed one assertion
into a contradictory duplicate of another. `vitest.config.ts` and the CI
step both exclude it by LITERAL path, so the move revived it into the
default lane and the contradiction surfaced.
- The CI path filters never named `tests/**`. After this move a PR touching
only a test matches neither `root` nor `mcp`, and a skipped required job
reads as passed (#1711).
- The 925 movers had never been type-checked ANYWHERE: the root config
excludes `**/*.test.ts`. Putting them under `tests/tsconfig.json` surfaces
2,801 errors across 482 files. That debt is excluded and guarded rather than
paid here — see below.
Reconciled with the move, each following its files rather than being dropped:
- `PROTECTED_ROOTS` drops `src` (it now holds zero keep-class suites, and the
non-empty-denominator test refuses a root that contributes none) and gains
`tests/unit`. The guard gets a live host in `npm run protected:guard`; it is
deliberately not a CI gate yet, because this change-set touches all 53
protected files by moving them.
- The relocation ledger gains 925 entries, and the generator now carries it
forward and REFUSES to write when a file from the previous baseline is gone
with nothing to explain it. Seeded with a short ledger it names the 25
unexplained losses instead of laundering them into a clean baseline.
- `GUARD_SUITE_ROOTS` keeps naming `src/`, because the MODULES never moved —
`selfTestCandidates` follows the suites into the mirror instead.
- The DR-30 corpus gains `tests/unit` and `tests/integration` as mandated
roots and splits the `src` floor of 728 between them in the observed ratio
(717 + 11), so the obligation is the same one at a new address.
- The depcruise rule drops a `pathNot` that had stopped excluding anything,
and gains an assertion that the governed set holds no test file.
`tests/tsconfig.json` excludes `unit/**` and `integration/**`. That holds the
line exactly where it stood before the move — those files were unchecked then
too — rather than relaxing the shared strictness for the tiers that are green.
`TestsTsconfig_ExcludedTiers_OnlyShrink` is the tooth: a tier added to that list
fails there first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
Task 031. Two of its premises were spent and one was wrong. The `src/` half was
already discharged by task 030 (zero co-located tests remain there); the
`scripts/` half is 87 files, not 77 — 48 `.test.ts` and 39 `.test.sh`.
The wrong one matters more. The Files field named `tests/unit/`, but that
directory is collected by the `core` PROJECT, not `unit` — task 030 put the
product's own tests there so they would keep the `bun:sqlite` alias and the 60s
Windows headroom. Routing 43 tooling suites there would have handed them a 12x
timeout budget and a different pool through a glob that already existed, and
nothing would have noticed: a tier change is invisible to a per-test status diff
because every test still passes. They go to `tests/scripts/` (the `unit`
project, beside the `tests/evals/` tree it already collects) and the five
`scripts/core/` guards to `tests/core/scripts/`, which `core` already collects.
Every suite keeps the project that ran it.
The bash suites need exactly one edit each, and it is not a path rewrite. All 39
self-anchor as `$(dirname "${BASH_SOURCE[0]}")` and reach only STAYERS through
it — their subject gate in `scripts/`, the repo root one level up. Inserting the
new->old segment straight after the `dirname` pins the anchor, and
`REPO_ROOT="$SCRIPT_DIR/.."`, every `$SCRIPT_DIR/<gate>` and every bare `cd`
downstream keep their original meaning with no edit of their own.
Verified twice over, because one oracle cannot see both halves. Per-test:
`vitest --reporter=json` before and after, every id mapped through the move —
13,581 -> 13,582, zero missing, zero status changes, the one addition being the
guard below. Per-suite: the 39 `.test.sh` belong to no vitest project, so their
oracle is an exit-code diff over all 45 in the repo — identical, 25 passing and
20 failing exactly as before.
`selfTestCandidates` was the load-bearing find. Four call sites pair a gate with
its self-test by constructed path, all `existsSync`. Task 030 added the
`src/` -> `tests/unit/` arm for exactly this reason, and without a `scripts/`
arm every gate under `scripts/` silently stops pairing — an unpaired gate
reports as "no self-test", not as an error, so a whole tree of guards goes quiet
with nothing red. Fixed, and pinned by a test that asserts the denominator as
well as the mapping.
Task 030's typecheck-debt exemption does not carry over: this debt is payable,
and `TestsTsconfig_ExcludedTiers_OnlyShrink` exists to make adding a third
exemption fail first. 73 errors across 16 files. `allowJs` (with `checkJs` off)
lets the checker infer types from the `.mjs` gates the suites drive, which
erased 49 of them — an untyped module also erases the contextual type of every
callback handed to it. The other 19 were narrowed. Net -5 casts against the
near-saturated DR-14 ratchet: three `as` assertions were what widened the
fixtures out of the signatures they feed.
Two guards were found lying, both invisible until a typechecker read the file:
- `guard-inventory.test.ts` asserted against `liveInventory.artifactsWithSelfTest
?? []`. There is no such field, so the expression was `[]` on every iteration
and the loop asserted nothing. The `?? []` is what made it silent rather than
a crash.
- a `GuardHost` fixture carried `pathFiltered: false`, a property the type does
not have (it carries `pathFilterKeys`). It could not fail in either direction.
Reconciled with the move rather than dropped:
- `tests/scripts` joins `TIER_OWNER` as a `unit`-owned tier, which is what makes
the destination choice above enforceable instead of merely intended.
- the test inventory asserts `scripts/` holds NO test file, the same way 030
left `src/`. A root removed from the list stops being watched; a root asserted
empty starts being enforced.
- the relocation ledger gains 87 entries, read back from git's own rename
detection rather than recomputed.
- `ScanRoot.excludePrefixes` keeps the five relocated core guards out of the
DR-30 corpus. Relocation INTO a governed root manufactures debt exactly as
relocation out of one discharges it, and the corpus rule is that membership
survives a move unchanged in both directions.
- `scripts/__fixtures__/comment-hygiene/` deliberately does NOT travel: it is
named by a production policy glob, the knip allowlist and an eslint-rules
test, all by repo-root-relative string. Moving it would have blanked that
glob silently, since the gate reading it is not enabled yet.
Two defects fixed on the way past. `lint-envelopes.test.sh` created
`src/orchestrate/` with `mkdir -p` and removed only the leaf, leaving a directory
that stopped existing when `orchestrate/` became `verbs/` — enough to red six
assertions in a suite that never ran it. And `comment-policy.mjs` carries
`// @ts-check` while sitting in no tsconfig at all, so its two real type errors
had never been read by anything.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
…er found
Task 032. 86 files across seven directories fold into `tests/`, and `test/`
ceases to exist. Task 029 pre-declared five of the seven destinations, so
`core`, `process`, `migration`, `smoke` and `e2e` land in trees already
collected by the projects that were collecting them.
The two it did not pre-declare are the whole of the judgement, and the spec
named the wrong home for both. `test/fixtures/` is not fixtures — it is a helper
LIBRARY, each `X.ts` paired with its own `X.test.ts`, 11 of them collected by
`unit`. The Files field routed it to `tests/support/`, whose declared owner is
`null` precisely BECAUSE vitest must collect nothing there; that move would have
stopped all 11 running while every one of them went on reporting passed. It
becomes `tests/helpers/`, a `unit`-owned tier. `test/setup/` splits by what each
file is: `package-scripts` and `vitest-config` assert repo structure and join
`tests/architecture/`, while `global.ts`, `preflight.ts` and its self-test are
support modules and join the helpers.
Verified the same two ways as 031, because neither oracle sees both halves.
Per-test: 13,582 -> 13,583 ids, zero missing, zero status changes, the one
addition being the guard below. Per-suite: all 45 `.test.sh` exit codes byte
identical. The failing set is the exact known baseline throughout — 9 files, 26
tests.
Moving `vitest-config.test.ts` under `tests/` put `vitest.config.ts` into a
typechecked program for the first time, and it turned out to be reporting three
live outages rather than pedantry. Vitest reads `coverage`, `fileParallelism`
and `passWithNoTests` ONLY at the root — they are its `NonProjectOptions` — and
`benchmark` belongs to the test config rather than beside it:
- the `core` project's `coverage` block was ignored, so `test:coverage` fell
back to the default reporters, `coverage/coverage-summary.json` was never
written, and the BLOCKING ratchet in ci.yml that reads it had no artifact at
all. Confirmed by running it and finding only html/clover/json; confirmed
fixed by finding the summary written after the hoist.
- `outcome` and `acceptance` had been running their files CONCURRENTLY while
declaring `fileParallelism: false`. For a tier whose reason to exist is real
OS and git state, that is the hazard the setting was written against. Both
now use `poolOptions.forks.singleFork`, which is the per-project form.
- `benchmark` never wrote `benchmark-results.json` for the gate that consumes
it.
Seven CI path-filter globs had been dead since the DR-4 block, and one more died
here. `skills/**`, `agents/**`, `command-aliases/**`, `commands/**` and
`rules/**` match nothing now that those roots are `rendered/` — which is named
in no filter — so a PR touching only generated output stopped flipping `root`,
and `render:guard` stopped running on the PR that caused the drift. That is the
exact gap the filter's own comment says it closes. The DR-22 guard meant to hold
it asserted only that the filter CONTAINED each glob, never that any glob
matched a file, so it passed the whole time.
`Filters_EveryGlob_MatchesAtLeastOneTrackedFile` is the missing tooth: probed
against the pre-fix workflow it names all eight, and none after.
Two more guards were found lying, both invisible until the move typechecked
them. `SagaStep` is a discriminated union, and four suites probed
`s.error !== undefined` instead of narrowing on `kind` — so `.error?.message`
rendered "undefined" where it promised a message. And a stub carrying the
comment "satisfies SagaToolClient directly, no cast needed" did not satisfy it:
its hand-written signature was narrower than the SDK's.
Reconciled with the move rather than dropped:
- the DR-30 corpus's `test/core` and `tests/core` roots become ONE — declaring
both would walk every file twice, the same double-count the `src` entry
already records — carrying the mandate the first one had and the SUM of the
two floors, so neither half can be quietly emptied behind the other.
- the test inventory asserts `test/` is gone as a root AND absent from disk,
which catches it being recreated rather than merely refilled.
- `HISTORICAL_PATH_REWRITES` retargets its `servers/exarchos-mcp/test/` hop.
- `.exarchos/invariants.md` INV-7 follows its witness, re-approved through the
authority-lock CLI rather than by editing a digest.
One thing found and left: `BuildIdentity_GeneratedPathAllowlist_IsNotABlanketEscape`
opens by requiring its probe scope to be git-clean while other suites plant
edits in the real repo concurrently. It sits outside the documented 26-test
baseline, passes standalone, and flipped red->green across this move without
being touched.
A note for the next mover: a virtual path defeats "preserve the target". The
codemod pointed `FIXTURE_PATH` — documented as "inside this directory", naming a
file that does not exist — at the dissolved root, because a target that never
existed has no answer to "where did this resolve before". Such a path follows
its DIRECTORY, not its literal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
Task 033. The last three top-level trees DR-5 owns move into the one test
tree: benchmarks/ (86 files), evals/ (33 — datasets, not tests) and
docs/evals/ (114). All three roots cease to exist, and docs/ loses the last
thing standing between it and the VitePress skeleton DR-7 reduces it to.
The destinations were already declared by task 029, so all 16 collected tests
land in the same `unit` project they were in before — verified against the
resolved config, not the config source. Every relative specifier travelled in
lockstep: the icpc runner is sibling-only and the graders keep their depth, so
zero import rewrites were needed and the detector that says so was checked
against its own denominator (64 files, 92 specifiers) rather than trusted for
returning nothing.
Verified per-test, not by count. 0 status changes across 13,584 ids; the
failing set is the exact known baseline (26 tests / 9 files, merge-orchestrate
+ store.race), zero induced. The one missing id and two new ones are a
deliberate test rename matched by its replacement, plus one new guard.
- `docs/**/runs/**` was one move from vacuous, in the direction that matters.
The 69 captured artifacts under runs/ drive a `process.exit` module-load
harness; reaching a worker they take it down rather than fail. Under docs/
no glob reached them anyway. Landing them under tests/ makes the exclusion
load-bearing at the same moment a docs/-anchored glob stops matching.
Re-anchored on `**/runs/**` and covered by a guard that asks the resolved
runner what it collects. Probed: seeded, it names the 6 leaked artifacts.
- A slash-anchored census cannot see a segmented path.
`path.join(REPO_ROOT, 'evals', 'reliability')` carries no `evals/`
substring, so the sweep that found all 75 other referrers called it clean
and four tests went red. Only the per-test diff named them. The second
sweep for a quoted bare segment found 26 more, 25 correctly unaffected
(directory-name predicates, matched at any depth).
- Task 031's rewrite had edited a test NAME inside the oracle. The inventory
baseline claimed `passes the full tests/scripts/...` while the source still
said `scripts/...` — a blanket literal rewrite catching a path that
happened to sit in a test title. The source is what changed here, so the
two agree; the baseline was also three guards stale.
- Typecheck debt paid, not excluded: 127 errors over 59 files → 0. Two more
latent defects fell out. `computeVerdict` opened on a CE branch that could
never fire (SampleVerdict has no `ce`), and `RunnerDeps.loadArm` advertised
a wider parameter than any implementation it is injected with accepts.
Twelve EventEmitter casts in the executor tests collapsed to one at the
factory: net -11 against the DR-14 ratchet.
- Two populations are NOT code, and saying so is a category statement rather
than a debt exemption. The captured runs are evidence — editing one to
satisfy a compiler falsifies the measurement. The three `tasks/*/oracle.ts`
import `./impl.ts`, materialized per run and deliberately absent so the
graded agent never sees the answer. `TestsTsconfig_ExcludedTiers_OnlyShrink`
now holds the debt list and the not-code list separately.
benchmark-gate.yml needed no retarget — it reads a src/ baseline and `npm run
bench`, neither of which touches these trees. Nine corrected premises are
recorded in the spec beside the task.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
…rule
Tasks 034 and 035.
034 set out to confirm nothing was lost and found two defects in the
reconciliation itself first.
- The check the task describes passes vacuously. Every move task regenerates
the baseline, so `files` already holds POST-move paths — filter it by a
former root and the set is empty, and a reconciliation over nothing is
green without checking anything. Written that way it reported `src/` clean
while asserting over zero entries. The population has to be the relocation
ledger's `from` side, the only surface that still remembers where a test
started.
- Task 032 never appended its relocations: 86 renames out of `test/` are
absent from the ledger, invisible for exactly the reason above. Backfilled
from `git diff-tree -M` on 13dfb3b rather than recomputed, so the ledger
records what happened rather than what should have. The generator re-emits
`relocations: []` on every run, and it will do this to the next task that
forgets.
- The kill probe drove a DIFFERENT filter from the check it claims to prove.
The real one followed a relocation to its destination and required the
destination to exist; the probe asked only whether a relocation was
present, so it stayed green regardless. Both now call one `unaccountedFor`.
The end-to-end audit against the task 002 capture: 63 ids differ across 26
files, every one a rename with its file intact. No test file and no test case
was lost between task 002 and the consolidated tree.
Reaching that needed two corrections to the comparison. The path chain cannot
reach task 002 at all — it would need every intermediate task's move table, and
`move-table.mjs` covers only 019, so composing what exists strands 467 of 1,138
mid-chain. And the declared id carries `runner`, which is not stable across 019:
the nested vitest package dissolved, so `vitest:nested` ceased to exist and all
10,986 of its tests read as lost. The reconciliation is on `(suite, name)`;
shell suites, which contribute no cases, reconcile at file granularity.
The 63 are recorded in `tools/audit/test-inventory-reconciliation.json` and
re-checked every run — `from` must stay absent and `to` must stay present — so
re-renaming or deleting one fails rather than quietly re-opening the gap.
Probed: pointed one destination at nothing and it names the pair.
`docs/` is not yet test-free. `docs/schemas/workflow-state.schema.json.test.sh`
is live and DR-7 re-homes it with the rest of `docs/schemas/` in task 038. It
is pinned BY NAME rather than dropped from the watch list, so 038 has to come
back and say so, and a second test appearing under `docs/` fails now.
035 replaces the co-located-tests rule in CLAUDE.md and AGENTS.md with the
centralized convention. Asserting the old sentence is gone would be half a test
— it passes on prose that says nothing — so the tier list is parsed back out of
each document and required to equal the tiers on disk in BOTH directions.
Probed by creating an undocumented tier: it names the drift. AGENTS.md is stale
well beyond this (its table still lists servers/, skills/, commands/, rules/ as
live directories); that is task 054's rewrite and is left alone rather than
half-corrected.
0 status changes across the suite; 6 new guards; the failing set is the exact
known baseline (26 / 9). typecheck 0 across all three programs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
The `tools/` consolidation was measured, applied, typechecked green across all
four programs — and reverted, because it still left 101 vitest failures and 19
shell-suite regressions. None of them were the move. All of them were the
reference tail, and three of its four classes are invisible to any path-shaped
search:
- bare parent walks. 23 sites of `resolve(SCRIPT_DIR, '..')`, which reached
the repo root from `scripts/` and lands inside `tools/audit` from
`tools/audit/gates/`. There is no literal to match, only a depth that moved.
- segmented joins. 40 sites of `join(root, 'scripts', 'x.mjs')` — the class
that cost task 033 four red tests, here at ten times the count.
- 27 shell harnesses computing SCRIPT_DIR inside a `cd "$(dirname ...)"`
expression, plus 12 more that walk through it to the repo root and need
their destination decided one at a time.
Two sites a blanket regex takes and must not: `./types.js` in
codegen-runtimes.ts is inside the template literal of the file it GENERATES, so
it is relative to src/install/runtimes/; and `$MOCK_REPO/scripts/...` is the
layout of a throwaway repo the installation test builds.
The genuinely open question is the published contract, which the task does not
mention: manifest.json ships `scripts/` as an installed component and files[]
publishes it. `target` must stay `scripts` — it is the user-facing ~/.claude
layout that V1_SYMLINK_NAMES knows — but every available `source` either shrinks
the shipped set or publishes tools/audit's 2.4 MB inventory baseline. That needs
a decision, checked against the acceptance project's fresh-install suite.
Also recorded: the tree is 103 files rather than 151; the destination map and
its collision proof; that get-exarchos needs no published-location work (the URL
is a Pages path, copied from the repo at build time); a divergent duplicate pair
already sitting under the destination; and that the guard-liveness baseline has
11 files of headroom left before it trips on tree-size drift.
Reverted rather than committed red. Nothing in the tree changes here except the
spec.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
…dead negations
Task 036's premise 6 asked what the installed `scripts/` component is for.
It is for nothing. Nothing reads `manifest.components.core` — the array is
inert, so `target: scripts` never materialized and `~/.claude/scripts` is
absent on a machine running 2.12.0-preview.3. `plugin.json` declares no
`scripts` key either. The one runtime tie is V1_SYMLINK_NAMES, which
deletes a v1 leftover rather than requiring one; it stays.
So the component and the `files[]` entry are removed rather than
repointed. Repointing `source` publishes a fiction whichever value it
takes, and `tools/audit/gates` — what the reverted attempt used — would
have shrunk the shipped set 88 -> 40 with nothing positioned to notice.
That left the three negations excluding nothing: `scripts/` was the last
shipped root carrying fixtures or test files, and `npm pack` is
byte-identical with and without all three. They are retired on the same
rule that retired `!**/trigger-tests`. Keeping test-only paths out of the
tarball is now asserted against the artifact in installer-verify, which is
strictly stronger — it names get-exarchos.ps1.test.ps1, which the .sh/.ts
negations could never match.
Measured, not assumed: tarball 351 -> 263 files, every dropped path under
scripts/, none added. get-exarchos.{sh,ps1} is unaffected — docs.yml
copies it from the repo path to Pages, so the npm tarball was never its
distribution channel.
Both guards were kill-probed: a seeded core component and a seeded tarball
leak are each named, and the acceptance fresh-install suite stayed green
through the first — it asserts a component source exists, so it cannot
police what premise 6 named it to police.
Full root suite sits at the known 9-file / 26-test baseline with zero
induced failures; typecheck clean across root + conformance + tests; lint
clean; versions in sync.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
…NCOMPLETE)
WIP on the staging branch, deliberately committed rather than reverted: the
move and its four hardest reference classes are done and verified, and the
remaining tail is small, enumerated, and root-caused. The previous attempt was
reverted at 101 regressions with no diagnosis; this one stands at 24 with each
cause named.
Moved 103 files: scripts/ (90) into tools/audit/{,core,lib,gates,__fixtures__,
__shims__,test-fixtures,tsconfig-strictness} and tools/release/ (15 build and
publish scripts), plus eslint-rules/, renovate-config/ and migrations/ under
tools/. Destination map unchanged from the measured one; collision proof clean.
Regressions 75 -> 24. What closed them, and what each cost to find:
* One digest. `.exarchos/invariants.md` cites lint-inv6.mjs three times;
retargeting it moved the invariant-catalog digest and BLOCKED contract
generation, which surfaced as ~20 unrelated-looking failures across the
compiler, bindings, reachability and CLI-seam suites. Re-approved with
`authority-lock-cli` (never by hand); exactly one line of the lockfile
changed, which is the proof nothing else drifted.
* Parent walks, in four spellings. Three syntax-specific detectors each
missed a different one: separate `'..'` arguments, dots combined into one
string (`resolve(HERE, '../..')`), a base that is a nested call rather than
an identifier, and the shell/PowerShell forms. The combined form is what
made the repo root resolve to `tools/`, so a guard reported
`tools/src/adapters/cli/cli.ts`. Replaced by a sweep that reports every
`..` in every moved file — complete by construction rather than by pattern.
* A file skipped as binary. `authority-census.ts` carries 6 NUL bytes in test
payloads, so the codemod's binary guard passed over it and left four stale
rows. Nothing else would have caught this: it is a source file that does not
read as one.
* Empty denominators. The cast census and the advisory ratchet both scanned
`scripts`, which now resolves nothing — the failure mode where a gate goes
green over an empty set. Retargeted, and the cast budget is unchanged, which
confirms the same files are counted at their new paths.
* `tsconfig.scripts.json` (premise 5) now covers `tools/audit` AND
`tools/release`, with its coverage guard asserting a non-empty denominator
per root rather than over the union — one prefix cannot name a tree that
split in two.
* CI path filters name `tools/**` in both lanes, so a PR that weakens a gate
triggers the lane that runs it.
Verified: typecheck clean across root + conformance + tests AND
`tsc -p tsconfig.scripts.json`; lint clean; versions in sync.
STILL RED (24, all diagnosed, none mysterious):
* check-enforcer-wiring (9) and guard-inventory (5) — their internal
derivations (gate to self-test pairing, synthetic fixture trees) still
assume a flat `scripts/`. Needs retargeting inside the guards, not a path
sweep.
* advisory-registry (5) — the synthetic fixtures couple concrete seed paths
to `scripts/**` globs; moving either half alone breaks the other. Both
directions were tried and reverted rather than left half-edited.
* test-inventory (3) — wants a regeneration with relocations appended, which
must happen after `git add` since the oracle sees tracked files only.
* build-release-manifest (2) — not a real failure: its clean-arm probe needs a
git-clean worktree and cannot run against uncommitted moves. Expected to
clear on this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
Finishes the move committed at e8aafe5. All 21 remaining regressions attributable to task 036 are closed; the full root suite goes 46 -> 26 failed, and the 26 that remain are the known pre-existing merge-orchestrate + store.race red that predates this branch. The suite also gained 63 EXECUTING tests (13546 -> 13609). guard-inventory's 62 were not passing before — they were failing to collect, which is worse. What was actually wrong, by cause rather than by symptom: * SPLIT-BRAIN PREFIX (9 + 5). check-enforcer-wiring recognized primaries under `tools/audit/gates/` but ENUMERATED them under `scripts/`, so every primary on disk read as unlisted and every manifest entry as missing from disk. The same shape in advisory-registry: the scanner had moved to `tools/`, half its fixtures had not, and the scan reported zero softening sites — a result indistinguishable from "clean". Both now derive the prefix from ONE exported constant (`PRIMARY_DIR`, `ENFORCEMENT_PRIMARY_DIR`) that the regex, the enumerator and the synthetic fixtures all read, so the three cannot drift apart again. `EnforcerWiring_EnumeratorAndRecognizer_AgreeOnThePrimaryPrefix` asserts the crossing directly; kill-probed by reintroducing the defect. * FAIL-CLOSED SCAN ROOT (1 file, 62 tests). guard-inventory's channel 3 pointed at the deleted `scripts/core`. It threw rather than reporting an empty channel — the fail-closed design working — but took the whole file's collection with it. * FROZEN SPEC CITATIONS. Channel 2 reads a dated spec that cites `scripts/` paths; those resolved to nothing, silently dropping guards from the inventory (knip-diff, guard-inventory itself). Added task 036's map to HISTORICAL_PATH_REWRITES rather than editing the spec, which is the existing mechanism for exactly this. The flat files split two ways that no single prefix expresses, so both targets are declared and the tree disambiguates by existence probe. * A DEPTH CHANGE WITH NO PATH LITERAL. validate-no-legacy.sh resolved `$SCRIPT_DIR/audit/knip-diff.ts`; from `tools/audit/gates/` that is `tools/audit/gates/audit/`. knip-diff read as unreachable. * SELF-TEST PAIRING. The mirror map is now one ordered table (SELF_TEST_MIRRORS) instead of a chain of `else if` arms, with a guard asserting every arm still pairs a real file — an arm whose tree moved keeps returning candidate paths, and "unpaired" reads as "no self-test", not as an error. * TWO EXEMPTIONS DISCHARGED, not re-dated. lint-inv6 and lint-test-first-drift were excused because their sources sat under `scripts/**`, outside the `root` path filter that arms the only job running them. Both entries rejected the fix (widening `root` also arms the flaky #1699 Windows lane). Task 036 moved the sources INSIDE the filter without widening it: the trade was never re-made, it was made moot. * TEST INVENTORY. Appended the 3 eslint-rules/renovate-config relocations rather than regenerating — the generator emits no `relocations` key at all, and the ledger's `from` side is the only surface that remembers where a test started (task 032 lost 86 entries this way). Added both roots to FORMER_TEST_ROOTS so the reconciliation actually checks them; `migrations/` is deliberately absent, having carried no test file. Verified: full root suite at the 26-failure pre-existing baseline; typecheck clean across root, conformance, tests AND tsconfig.scripts.json; lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
…s 040, 040a, 041)
DR-3. The layering census could not EXPRESS two of the rules it was assigned,
and governed only 12 peripheral leaves. All three are addressed together
because 040a and 041 are unreachable without 040's model change.
TASK 040 — the model.
* `layerOf` resolved a module to its FIRST PATH SEGMENT, so any edge between
two nested siblings collapsed to `parent -> parent` and died on the
intra-layer skip. It now resolves to the LONGEST DECLARED id that owns the
module, threaded from the same allowance table the census judges against —
the resolver and the rule set cannot disagree about what a layer is.
Single-segment rows keep their exact previous meaning, which is why the
change lands with ZERO stale allowances.
The spec predicted this would expose `adapters/mcp -> adapters/cli`. The
live tree carries the edge in the OTHER direction — `adapters/cli/cli.ts ->
adapters/mcp/mcp.ts` — and it had been structurally invisible.
* Root-level files were excluded from the edge set outright, which made
`registry.ts` — the largest module in the tree — ungovernable: no allowance
could reach it and no edge to it could be forbidden. The exclusion is
replaced by a stated `ROOT_LAYER` policy. 203 inbound edges became visible;
the four layers that reach it now say so.
TASK 040a — the two censuses riding along.
This module carries THREE censuses, and work scoped to the layering table has
twice come close to migrating it alone. Both other rules' declared paths were
verified to still resolve, and their vacuity teeth (EMPTY_SEAM_DENOMINATOR
x2, UNRESOLVED_DECLARATION_STORAGE, STALE_SOURCE_ADAPTER,
EMPTY_SDK_SEAM_DENOMINATOR, SDK_SEAM_MODULE_ABSENT, STALE/EXPIRED exemption)
were already individually tested. What was missing is the assertion binding
them: BothSeams_VacuityCheck_FailsOnAnEmptyRuleSet, plus a live-denominator
FLOOR — the teeth only bite at zero, so a denominator that shrank to two
modules would still read "non-empty" while governing nothing.
One consequence recorded rather than hidden: a test asserted the layering
census saw NOTHING for a root-file import, as the contrast justifying two
separate censuses. That contrast is gone. They stay separate for a better
reason — a layer allowance is unconditional, while DR-1's rule fires only for
a module that is already a declaration CONSUMER, which is what makes its
population self-maintaining and is not expressible as an allowance row.
TASK 041 — the core, admitted in ascending width.
18 layers promoted, 12 -> 30 governed. Each `allow` is the EXACT measured
outbound surface, never a wildcard, so both teeth are live on day one. Order
is ascending width because a promotion that starts at the widest row invites
one blanket allowance that governs nothing.
The widths ARE the finding and are published as such: `verbs` reaches 19 of
30 layers, `dispatch` 15. Phase 1 moves code without changing meaning, so
these are RECORDED, not narrowed — the rows buy the ratchet and make the
numbers quotable, which is the first step to reducing them.
LayerRule_SeededViolation_FailsAndNamesTheRule seeds one violation per rule
and requires the census to fail NAMING it: "the census is green" says nothing
about whether any individual row can still reject, and a table of 30 rows is
worth exactly its teeth.
Cycle-gate overlap settled by measurement, not replacement: cycle-gate.ts
already implements the phantom-baseline tooth (failure mode (c), via
`phantomBaselineEntries`) and already kill-probes it in isolation, naming the
offending edge. depcruise still reports no "this rule matched nothing", so
the bespoke tooth is justified and `import-cycles.ts` stays.
Also fixed a latent trap: two EXIT PROOF tests scanned the live tree with NO
declared ids while judging against the real table. That agrees only while every
id is a single path segment — the first nested id would have made its rows look
phantom for a reason unrelated to the tree.
Verified: full root suite at the 26-failure pre-existing baseline (13616 tests,
+7); census 1130 edges / 0 diagnostics; typecheck clean across root,
conformance and tests; lint clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
… (042-046)
Phase 1 terminal block. DR-1, DR-3, DR-10, DR-11. Four live defects found, all
of the same shape: a guard that had stopped matching anything and was therefore
passing forever.
TASK 042 — audit configs and glob liveness.
* THREE CODEOWNERS PATTERNS MATCHED ZERO FILES. `scripts/`, `skills/` and
`commands/` owned 424 files between them at the last capture and had
silently fallen through to the `*` rule. Retargeted onto tools/, rendered/,
content/ and tests/.
* THE LIVENESS SUITE ONLY READ A FROZEN CAPTURE, which makes it a statement
about a JSON document rather than about the repository — a guard could
evaporate the moment after a capture and every assertion stayed green until
someone re-measured by hand. That is exactly what happened above. It now
runs the measurer and asserts against the LIVE tree; kill-probed by seeding
a dead pattern.
* THE MEASURER ITSELF HAD GONE STALE, which is the same failure one level up.
`lint:eslint-cli-glob` hard-coded `src/**/*.ts` while the lint script had
moved on, and `lint:test-first-drift` counted `commands/` + `agents/`,
retired by DR-4. Both now READ the config they describe and throw rather
than guess. 626 -> 777 and 126 -> 130, both differences real.
* knip: five dead globs (`scripts/**`, `test/**`, `src/**/*.test.ts`,
`src/__tests__/**`) retargeted onto the post-036 tree. Widening `project`
to the whole test tree surfaced 13 findings that had never been measured:
four dead exports DELETED, eight allowlisted (three eval stubs read by
path, three HIDDEN oracles that grade.ts copies into the sandbox, two
hand-run CLIs), and one stale entry retargeted — `InitOutput` had named
verbs/doctor/schema.ts since before the verb split. `tests/evals/**/runs/**`
stays excluded as captured evidence. ALLOWLIST_BUDGET 105 -> 113 in the same
commit, as that ratchet requires.
* ESLint scope widened to `tools/**` in BOTH halves. The trap the spec names
is real and was live in reverse: `eslint tools/audit/**` reported 38 files
"ignored because no matching configuration was supplied" and exited 0.
Findings over the newly-linted directories were MEASURED before widening,
not assumed: zero.
TASK 043 — the top-level contract, and 17 directories nothing could see.
An empty directory is invisible to git and to every tracked-file census, so it
survives every move task. The new contract reads the FILESYSTEM and found
`caller-identity-test/` plus 16 more under src/ — residue of task 030's suite
lift and the DR-4 split. All removed, fail-closed (refuse if any file or any
tracked path is beneath).
`caller-identity-test/` kept coming BACK: a unit test passed a relative
`stateDir`, so dispatch materialised it in the repo root every run. Fixed at
the root with a temp dir. The contract also holds on a BUILT tree — revision 1
specified an assertion omitting `dist/`, which would have failed for everyone
who had ever run a build.
TASK 044 — already implemented, and its map was stale.
`layer-map.test.ts` already asserts the 11 -> 9 relation in both directions
(its own header says so). Removing the empty directories exposed two stale
`exception` entries describing 44 and 3 test files task 030 had already moved;
entries dropped and counts re-derived, 28 -> 26.
TASK 045 — case exactness, and a proof resting on a leftover.
Checks every declared entry point against a case-SENSITIVE listing, walking
segment by segment — `existsSync` answers the question a case-insensitive
filesystem wants to answer, which is the environment where the defect is
introduced.
It flagged `manifest.json` bundlePath `dist/exarchos-mcp.js`, which no build
step emits (build-binary.ts records that emission path as retired). Not dead,
though: only its BASENAME is load-bearing, `generateMcpEntry` uses exactly
that, and nothing ever opens the path — `installBundle` has no production
caller. So it is excluded from existence checking with the reason stated, and
what IS load-bearing is asserted instead.
Separately, `relocation-proof.test.ts` WOULD ALREADY HAVE FAILED ON A FRESH
CLONE. Its header claims the probe root is virtual; that was true of files and
false of the directory, and TS module resolution needs the directory to exist.
It was resting on an empty leftover git cannot represent. Overriding
`directoryExists` makes the overlay genuinely virtual.
TASK 046 — Phase 1 terminal gate.
The CI workflow already hosts every step the task lists, on Linux and Windows.
Verified locally instead of re-adding: full suite, typecheck across all four
programs, lint, render guard, knip-diff, cycle gate (0 cycles),
lint:invariants (0 findings), lint:envelopes, test-inventory reconciliation.
Verified: full root suite back at the 26-failure pre-existing baseline (13563
passed, +13 net); typecheck clean across root, conformance, tests and
tsconfig.scripts.json; lint clean over the widened surface.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
…047, 052) DR-9, DR-2. The two Phase 2 gates that do not depend on the decompositions — and 047 is the safety net those decompositions need before they start. TASK 047 — the snapshot tasks 048-051 will be verified against. The dangerous failure in a decomposition is a SILENT one. Drop a tool action while moving a block of declarations and the result type-checks, lints, and passes every behavioural test that does not happen to exercise it. Change a hash input and every existing row in the event store stops matching the ids the new code computes, with nothing red anywhere. Both are invisible to the compiler by construction, so both need a recorded baseline. Captured: 5 composite tools (4 visible + 1 hidden), 123 actions, 171 persisted event types with lifecycle and tier. INV-5d checked against its own numbers rather than against the snapshot, so both would have to be wrong to pass. `RegisteredActions_DroppedRegistration_FailsTheSnapshot` mutilates the live shape two ways — a dropped action and a dropped tool — because a comparison that passes for every input satisfies the stability assertions just as well. TASK 052 — the rule that keeps `orchestrate/` from happening again. It once held 83 files flat. Nothing permitted that and nothing noticed it: a directory gains one file at a time and no single commit looks wrong. Cap is 25 own-level non-test files, and three directories exceed it today — `verbs/gates` 39, `workflow` 35, `workflow/admission` 30. They are exempted, and the exemption is PREDICATED as the task requires: each states why the breadth is or is not honest, and PINS the count it was granted at. An exempt directory may shrink freely and may not grow, so the exemption is a debt with a ratchet rather than an amnesty. `workflow`'s entry says plainly that it is the orchestrate/ failure in miniature. Seeded-overflow probe included, because a cap no input can violate is decoration. ALSO — the relocation ledger nearly lost 1,128 entries again. Adding four test files meant regenerating the inventory, and the regenerated artifact came back with `relocations: []`. Reading the generator suggests otherwise — it calls `mergeRelocations(prior.relocations ?? [], incoming)` — but `prior` is populated only from an explicit `--relocations` argument, so a bare run drops the lot. Task 032 lost 86 entries to exactly this. Merged by hand with assertions that refuse to write a short ledger or lose a file. That reconciliation then caught a stale rename destination: its `to` id embeds the SUITE path, and task 036 moved `get-exarchos.sh` from `scripts/` to `tools/release/`, so the pinned id stopped existing while the case never moved. Retargeted, with the new destination asserted live before rewriting. Verified: full root suite at the 26-failure pre-existing baseline (13572 passed, +9); typecheck clean across root, conformance and tests; lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
DR-9. `registry.ts` was 4,587 lines — the largest module in the tree and the one every other description of the action surface is a projection of. It is now 29 modules under `src/registry/`, split along seams the file already had: the vocabulary an action carries (hints, gate-metadata, annotations, types), the machinery that projects declarations into schemas and descriptions, and the action lists themselves, one module per action family. `src/registry.ts` stays as the published path. Every one of the ~50 consumers imports `./registry.js` and none of them changed; the barrel re-exports and declares nothing. WHAT MAKES THIS SAFE TO BELIEVE. The identifier snapshot is the oracle: same 5 composite tools, same 123 actions, same ORDER. Order is not incidental — it is what a client sees from `describe` — so the two aggregate modules state their concatenation order in one place rather than letting import order decide it. THE SPLIT WAS MECHANICAL, THE IMPORTS DERIVED. Every module is a verbatim line range of the original; nothing was retyped. Imports were then computed from the TypeScript parser's identifier references rather than written by hand, because a missing import is a compile error but a wrong one can still compile against a same-named symbol from somewhere else. THE LAYER MODEL PREDICTED THIS AND SAID SO. The `<root>` allowance row carried a note that it existed to make decomposing `registry.ts` "a measurable change rather than an invisible one". It did exactly that: moving the declarations out turned the root layer's `config` and `runtime` edges into stale cover, and both had to be dropped. `registry` is now a governed layer of its own, and its allowance is WIDE by nature — an action declares the schema of what it accepts, so the declarations reference shapes owned by nearly every layer they describe. Those edges are references to schemas, not calls into behavior, which is what makes the width honest here and would make it alarming on a layer that runs. FOUR ORACLES WERE PINNED TO A PATH AND WOULD HAVE GONE VACUOUS. The vacuity allowlist, the outputSchema census, the lifecycle-verb parity grep and the authority live-proof each read `src/registry.ts` as source TEXT. Pointed at a barrel, three would have passed by finding nothing to object to. All four now enumerate the registry DIRECTORY, so a further split cannot silently shrink their denominator — and scoping one of them to `actions/` alone proved the point immediately, losing the three shared `describe` sites (107 against 110) until it was widened to the whole tree. ALSO: a stale prose budget became honest. `artifact-dir-literals` recorded one `docs/specs/` mention in registry.ts; the real number is four. Its comment stripper pairs `/*` with the next `*/` across the whole file, and over 4,587 lines one mis-paired block blanked the region three of them sat in. Measured both ways before re-pinning: identical text, one surviving line before, four after. Verified: full root suite at the 26-failure pre-existing merge-orchestrate / store.race baseline (13,571 passing); typecheck clean across root, conformance and scripts; lint clean over the new tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
DR-9. With registry.ts already split, these are the other five hotspots the complexity measurement named. 16.4k lines across six files becomes 2.9k across six published paths plus ~80 modules behind them. Every published path is unchanged, so no consumer moved. sqlite-backend.ts 2,932 -> 2,497 + sqlite/ (7 modules) workflow/tools.ts 2,062 -> 26 + handlers/ (10 modules) views/tools.ts 2,285 -> 37 + handlers/ (22 modules) guard-inventory.ts 2,359 -> 296 + guard-inventory/ (16 modules) build-skills.ts 2,157 -> 64 + build-skills/ (13 modules) `sqlite-backend.ts` keeps its class; what moved out is everything that is not the class — wire types, DDL, prepared-statement shape, the error family, the driver predicates. Splitting a 2,400-line class is a different refactor with a different risk, and this one was scoped to leave behaviour alone. EACH FILE'S ORACLE, RUN. The event-store serialization fixture still passes across three processes, which is the acceptance condition for touching the append path at all. `npm run build:skills` re-renders the tree byte-identically — zero drift in `rendered/`, which is the whole claim for the renderer. WHAT THE SPLIT SURFACED, AND WHY IT IS WORTH THE CHANGE: A self-invocation guard would have silently disabled the renderer. Moving `main()` into a submodule left `if (import.meta.url === argv[1])` in a file that is never executed — `package.json` runs the barrel. The guard would have been inert, `npm run build:skills` a no-op that exits 0, and the rendered tree quietly stale. Its own comment describes surviving that exact failure once before on Windows. It now lives in the file that is actually run. The release manifest read `SCHEMA_VERSION` out of the backend by regex. The barrel re-exports the name but does not declare it, so the pattern stopped matching — and the manifest builder THREW rather than shipping a manifest with a wrong schema version. It is repointed at the declaring module, which is the honest target for a regex: a re-export can never satisfy it. A parent-walk went one level short. `REPO_ROOT` in the guard inventory is `resolve(HERE, '..','..','..')`, and one directory deeper that resolves to `tools/` — a REAL directory, so every channel would have scanned a tree with no guards in it and reported a clean inventory of nothing. Hop counts fail this way by default, so the fix is not just the fourth `..`: the module now confirms the root by manifest NAME at import and throws if it is wrong. Two import cycles, both broken by extraction rather than baselined. The placeholder validator and the renderer each needed the other; the error formatters they shared became their own module. Same shape for the build driver and its entry point. Four oracles followed their subject rather than their subject's old path: the `action: 'set'` grep exemption (now on the set handler), the AtomicAppender consumer census, the legacy-HSM importer list — which narrowed from "the workflow surface" to the transition handler specifically, a more precise fact than it could state before — and the artifact-literal allowlist. The guard-liveness baseline is re-captured deliberately, not quietly: six surfaces grew with the new files, none dropped to zero, and the diff was read before it was written. Verified: full root suite at the 26-failure pre-existing merge-orchestrate / store.race baseline (13,572 passing — the same number as before this branch's Phase 2 work); typecheck clean across root, conformance and scripts; lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
DR-3. The boundary rule `no-domain-core-to-io-adapters` has been RED on this branch since the servers/ fold, with three errors and no task owning them. The Phase 2 terminal gate requires every gate green, so it had to be answered rather than carried. All three were one mistake wearing two hats: three PURE modules were filed under `adapters/`, and the domain core had to reach into the IO facade to get at them. None of the three imports anything first-party — two import nothing at all — so none of them was ever an adapter. adapters/json-schema.ts -> utils/json-schema.ts (55 lines, imports zod) adapters/channel/priority.ts -> events/channel/priority.ts (an event-type table) adapters/channel/delivery.ts -> events/channel/delivery.ts (a delivery algebra) The delivery split is the one worth stating plainly: the ALGEBRA is a pure decision function the event core is entitled to own, and the EMITTER is the transport it must not reach into. Filing both under `adapters/` is what forced the core across the boundary. They are now on opposite sides of it, and the required-delivery census names both — its point is that a required delivery cannot be silently swallowed, which is true of the algebra and the transport alike. WHAT THE MOVE MEASURED. `json-schema.ts` was single-handedly responsible for FIVE layers appearing to depend on the IO facade. With it moved, `runbooks`, `projections`, `describe`, `events` and `workflow` all lost their `-> adapters` edge, and the stale-cover tooth of the layering census reported each one. Those five rows are narrowed here. Four layers gained `utils` in exchange, which is a foundation leaf that imports nothing — the trade is five edges into an IO facade for four into a leaf. That is the layering census working exactly as its own header describes: the ratchet named every row that had to change, in both directions, and the allowance sets are the measured surface rather than a guess. ALSO — the relocation ledger only followed ONE hop. It is append-only by design, so a test file relocated into `tests/` and later moved within it is recorded as two entries; the reconciler read the first, found the intermediate path gone, and reported the test lost. It now walks the chain, bounded by a `seen` set because a ledger can contain a cycle by mistake. Ledger 1,128 -> 1,132 entries with file and case totals unchanged, so nothing was lost — which is what the reconciliation exists to prove. Verified: `depcruise` reports ZERO dependency violations over src + tools (984 modules, 3,077 dependencies) — the first clean reading on this branch. Layering census clean in both directions. Full root suite at the 26-failure pre-existing merge-orchestrate / store.race baseline; typecheck clean across root and conformance. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
…s (052, 053, 057, 058) DR-8, DR-9, DR-12. The four Phase 2 tasks that check the registers rather than the code — each one an instrument that fails by going quiet. TASK 053 — the dead-code detector is proven to be LOOKING. `knip.json` was already one workspace over src/tools/tests, but nothing asserted it. A detector aimed at a directory that no longer exists reports no dead code and reads exactly like a clean tree, and every other check in that file is downstream of it having looked at something. Three checks now: the two workspaces really did collapse to one, every source root of the tree is inside the project glob, and every declared glob has a root that exists. Seeded a stale root (the dissolved server package) and both of the last two named it, so the guard bites rather than decorates. The allowlist budget stays pinned AT its count, so a finding must be resolved rather than absorbed. TASK 057 — three catalog references pointed at a barrel. The existence guard passed, and would have kept passing: the decomposition left every published path in place, so `src/registry.ts` still resolves — to thirteen lines of re-exports. A reader following the citation lands on a list of `export … from`. The three are repointed at the modules that DECLARE what each invariant constrains: the action schemas, the assembled tool registry, and the economy hints. A resolution check cannot tell that apart, so a second one was added: a reference to a TypeScript module must name one that declares something. Seeded a barrel citation and it named the exact entry. TASK 058 — the four fail-open registers, censused. CODEOWNERS, `files[]`, the plugin manifest and the protected-suite register all fail the same way: a stale entry produces no error, ownership quietly falls through to `*`, and the packaged set silently shrinks. The new census counts what each declared pattern actually matches against the live tracked tree. It carries per-REGISTER denominators, not just a total. The whole-census count is satisfied by CODEOWNERS alone, so a register whose file was renamed would drop out of the audit entirely and every check below it would pass on no input — which is the same failure the census exists to find, one level up. Its suite was first written to `tools/conformance/tests/`, which the spec names and which NO vitest project collects. It would have passed by never running. Moved beside its subject under `src/`, where the conformance project looks, with the reason recorded in the file. TASK 052 — the locality exemptions now say something true. All three pinned directories are unchanged, and the reasons claimed a decomposition would fix them. It could not: splitting a composite surface into a subdirectory replaces one large file with a small barrel, and own-level counts do not see subdirectories. Reducing those numbers means moving modules OUT of that level, which is different work. Stated plainly rather than left as a remedy that has already run. ALSO — the relocation ledger and the contract lock, both handled deliberately. The inventory was regenerated through its `--out` path so the 1,132-entry ledger survived (totals up by exactly the one new suite). Editing the invariant catalog moved the contract authority digest, which surfaced as ~25 unrelated-looking contract failures; re-approved through `authority-lock-cli`, and the lockfile diff is a single line — which is the proof nothing else drifted. Verified: full root suite at the 26-failure pre-existing merge-orchestrate / store.race baseline; typecheck clean across root and conformance; lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
…4, 055) DR-10, DR-1. Three root instruction files still described the pre-refactor layout — `servers/exarchos-mcp/`, a root `scripts/`, a root `skills/` — and had done for the whole structural refactor. Every agent reading them was being sent to directories that had been dissolved, confidently and by the file whose job is to orient it. REWRITTEN: `AGENTS.md` (whole code-organization table and the server section), `CLAUDE.md` (build commands, architecture, conventions), `CONTRIBUTING.md` (skill-authoring paths), `docs/ARCHITECTURE.md` (the artifact-kind table, the directory counts, the test-only classification). ADDED: a `README.md` for each of the six structural directories. Each states what belongs — and, the part that does the work, what does not. "This holds the source" answers nothing a reader could not guess; the boundary is what stops the next arrival landing in the wrong place. `rendered/README.md` says in as many words that the tree is generated and that a hand edit is reverted by the next build. THE CHECKS, BECAUSE PROSE ROTS SILENTLY: Every documented `npm run <script>` is a real script. A reader following a stale instruction gets "npm ERR! Missing script" and no clue which half is wrong. No instruction file names a dissolved directory. The removed roots are asserted to be genuinely absent FIRST, so the list cannot rot into forbidding a path that came back — a guard wrong in the opposite direction is still wrong. Documentation may state a rule only where something enforces it. The table pairs each claim with its enforcer and fails if the enforcer is gone, which is how a guarantee quietly becomes an aspiration. README coverage ENUMERATES the tree rather than counting it. A seventh directory cannot be added without a README, which a count-based check would have allowed — the count being the one the author updated. The doc guard caught its first offender immediately, in a README written minutes earlier: `tools/README.md` listed `eslint-rules/` bare, which reads as the dissolved root rather than the live `tools/eslint-rules/`. Qualified, with the reason recorded. Verified: the whole architecture tier green (27 files); full root suite at the 26-failure pre-existing merge-orchestrate / store.race baseline; typecheck clean. Test inventory regenerated with the 1,132-entry relocation ledger intact and totals up by exactly the new suites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
DR-11. The phase is finished when the checks that govern it all run and all
pass. The failure this guards against is not a red gate — it is a gate that
stopped being invoked, which looks like success from every angle except the one
that counts.
MEASURED, NOT ASSERTED:
static analysis 4/4, INCLUDING import boundaries — red on this branch
since the servers/ fold and green for the first time
depcruise 0 violations (986 modules, 3,083 dependencies)
knip 113 findings, all allowlisted, budget pinned at the count
typecheck clean across root, conformance and scripts
lint / quality clean
render:guard rendered/ byte-identical to content/
runtimes:guard embedded runtime table in sync
full root suite 13,598 passing against the 26-failure pre-existing
merge-orchestrate / store.race baseline
DOGFOODING CONTINUITY, VERIFIED LIVE. This refactor's own workflow was
initialized before Phase 1 and rehydrated after Phase 2 with its phase
unchanged and its recorded spec artifact still resolving. That is the claim the
task makes, and it was checked by doing it rather than by reasoning about it.
WHAT THE GATE TEST CAN AND CANNOT SAY. Whether the suite is green on a clean
clone is CI's job and is not assertable from inside a working tree. What IS
assertable is that CI still declares the jobs that would find out — both
platforms, the aggregator a branch rule can require, and every gate the phase
depends on existing as an invocable npm script. A gate that exists as a file
but not as a script is reachable only by someone who already knows it is there.
The identifier snapshot's own denominator is checked too: counts AND per-tool
rows, because a snapshot carrying counts and no rows satisfies every count
assertion and compares against nothing.
Windows is named explicitly because it is where this repository's portability
defects surface — path separators, file-URL comparison, concurrent-rename
EPERM. A matrix that quietly lost the Windows leg would take the whole class
with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
…modules The decomposition tasks carry an added acceptance criterion: no extracted module names a planning ordinal in a comment. Four modules now satisfy it — `hints`, `phases`, `gate-metadata` and `schema-builders` are clean against the Stage 1 classifier. Each rewrite states the constraint instead of citing the artifact. The budget comment no longer cites a requirement number to explain why some actions get a higher ceiling; it says they are the intentional detail paths and gives the measured worst case. `resolveEconomyBudget` no longer defers to a requirement for its failure direction; it says the runtime seam fails OPEN, and why — a broken budget must not stop an action from answering. One rewrite gained something. The `prepare_review` phase set carried a bare citation of a "binding trap"; stating it in words says what the trap IS — an action whose phase set exactly equals the plan-structure binding is treated as a canonical plan gate, so matching it would silently promote a non-blocking provisioning surface into the gate population. That is the fact a future reader needs, and it was previously only reachable by finding the issue. REMAINING: 570 findings across 71 modules, measured. The classifier runs in observe mode, so this is a debt with a number rather than a blocked gate. Also removes a stray zero-byte `=95` at the repository root — a shell redirect from a malformed `awk` range. The top-level contract caught it, which is what that guard is for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
…, 037) DR-7. The destination decision this was blocked on is made: `lvlup-sw/docs`, keyed under `exarchos/`, with the source path preserved verbatim underneath — so `docs/audits/foo.md` is `exarchos/docs/audits/foo.md` there. A reader at the destination sees where a document came from without a lookup table, and the mapping is mechanical enough that mounting a subtree back is a per-directory symlink rather than a translation. MOVED: 51 files, 4.1 MB — docs/audits (41), docs/bugs (8), docs/refactors (1), docs/market (1). Pushed to lvlup-sw/docs@cbe7cb5. NOT MOVED: 462 files across twelve subtrees. Every one carries live referrers — docs/designs 119, docs/plans 66, docs/rca 39, docs/architecture 36. Moving them today converts working links into broken ones in bulk, so they stay until their referrers are retargeted or retired. The eligible set is read from the census at manifest-generation time rather than listed in the tool, so the tool cannot drift from the measurement that governs it. PRESERVATION IS PROVEN, NOT ASSERTED. `tools/audit/prose-manifest.json` records a source path, a destination path, a byte length and a SHA-256 for all 51 files. The copy was reconciled against it before a single file was deleted, and against the REMOTE tree afterwards — 52 blobs under the key, 51 manifest entries, zero absent. The reconciliation was probe-tested by appending one line to a transferred file: it reported a digest mismatch. A document that arrives DIFFERENT has to fail exactly like one that fails to arrive, or the gate is decoration. THE SYMLINKS ARE LOCAL AND UNCOMMITTED, DELIBERATELY. `npm run docs:mount` creates them; `.gitignore` covers them. A committed symlink stores its target as content, so it would hard-code one machine's layout into every checkout and dangle for everyone else — and a dangling link is worse than an absent directory, because a tree walk sees an entry and fails on read instead of skipping it. The script also refuses to replace a real directory: a subtree not yet relocated, or holding local work, must not be silently shadowed. TWO GUARDS WENT VACUOUS THE MOMENT THE FILES LEFT, AND ARE FIXED. The census ratchet listed the four as "cleared for deletion" on a referrer count of zero — which stays true forever once a directory holds nothing. Emptiness and cleanliness are indistinguishable to a referrer count; only a file count tells them apart. `RELOCATED` now names what has gone and asserts it holds nothing, `CLEARED_FOR_DELETION` means "clear AND still here" and is correctly EMPTY today, and a denominator check fails if the census stops seeing files at all. The same regeneration surfaced that `docs/evals` had been re-homed to `tests/evals/` long ago while the frozen capture still claimed it held files. The test asserted every re-home target still had content, so it would have failed for having succeeded. Verified: full root suite at the 26-failure pre-existing baseline (13,608 passing); typecheck clean; the four links resolve and the content is readable at its original paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
DR-7. `docs/` was 511 tracked files. It is now 91, and every one of them is READ by something. THE RULE INVERTED, WHICH IS THE POINT. Eligibility used to be "nothing points at this subtree". Measured, that blocked 462 files on 362 references — and 200 of those were a path in a COMMENT, a citation rather than a dependency. 128 pointed into `docs/designs/` or `docs/plans/`, which `.exarchos/ comment-policy.json` already forbids on the stated grounds that "the document may move out of this repository". The gate was preserving links another rule wanted deleted, and the cost was the entire exodus. So the tool now names what STAYS and relocates everything else. A retained path earns its place by being read — by the program, by a test, or by a user who was handed the path — and each entry states which. A document added under `docs/` is relocatable by default and has to be argued in. RETAINED, and why: ARCHITECTURE.md and system-design.html describe the system that exists; `docs/specs` is the LIVE workflow artifact directory that running workflows write to; `docs/architecture` is read by nineteen invariant-catalog reference keys and by the contract-seam doc test; `docs/guides` is printed into a user's scaffolded config by the shipped product, so those paths must resolve for someone who does not have this repository; schemas and assets are data, not prose, and are re-homed separately. THE RETENTION SURVEY MISSED A CONSUMER, AND THE SUITE CAUGHT IT. `invariants-v3-contract-seam.md` is cited by no catalog key, so a survey of the invariant references cleared it — and `contract-seam-doc.test.ts`, which asserts the document enumerates every exported v3 schema type, went red. It is retained, with that stated as the reason: a citation survey is not a consumer survey. THREE INSTRUMENTS FAILED IN THE SAME DIRECTION AND ARE FIXED. The ignore patterns were written with a trailing slash (`docs/audits/`), which matches a DIRECTORY — and a mount is a SYMLINK, which git treats as a file. They matched nothing, and four links were committed with mode 120000 carrying one worktree's relative path: resolvable for its author, dangling everywhere else. Untracked, patterns corrected, and a test now fails on any tracked symlink under `docs/` by checking the MODE. Regenerating the manifest AFTER deletion enumerates a tree with nothing left in it and produced an EMPTY record — which reconciles clean against a destination that lost everything. It cost the first batch's 51 entries before the guard existed. The manifest is now merged rather than replaced, so it is a standing record of everything ever relocated, and a shrink is refused unless asked for explicitly. The census counted `prose-manifest.json` as a live referrer of every path it records — the record of a departure supplying that departure's own justification for being blocked. Excluded, for the reason the census already excludes its own report. THE CENSUS INVERTED WITH THE GATE. It no longer decides what may leave; it asks whether what REMAINS is genuinely referenced, which fails in the direction that matters — a retained subtree nothing reads is one that should have left. Its `docs/audits` assertion was retired: that subtree has gone, and its referrer count is now noise from a comment in the test that explains the symlink bug. Verified: 467 of 467 reconciled against the destination before deletion, and against the remote tree after; full root suite at the 26-failure pre-existing baseline; typecheck clean; the whole architecture tier green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
…ocated DR-7. `docs/` held 511 tracked files. It holds two: `system-design.html` and a README. 554 documents are in `lvlup-sw/docs` under the `exarchos/` key. THE TEST IS NOW OPERATION, NOT CITATION. The previous pass kept anything a reference key or a scaffolded config NAMED. Checked, almost none of it was read: the invariants loader parses `references:` into a `string[]` and NOTHING in `src/` consumes it — the only consumer is a test asserting the paths resolve. So the catalog's architecture notes, the guides, `ARCHITECTURE.md`, the accumulated specs and the skills-authoring guide were all citations with an enforcement mechanism attached, not dependencies. WHAT WENT, beyond the previous pass: `docs/specs` (40 — workflow ARTIFACTS; the directory stays a write target, its accumulated output does not), `docs/architecture` (13), `docs/guides` (10), `docs/assets` (21), `ARCHITECTURE.md`, `skills-authoring.md`, `tool-reference.md`. RE-HOMED RATHER THAN RELOCATED: `docs/schemas/` held a JSON Schema and the shell test that checks it. Nothing else reads the schema, so both are test data and now live at `tests/scripts/schemas/`. The inventory REFUSED to write a baseline that lost the test until the relocation was declared, which is the guard doing its job. CITATIONS THAT SURVIVE THEIR DOCUMENT. The catalog's 21 relocated `references:` keys are rewritten as `lvlup-sw/docs:exarchos/…` — a cross-repository citation. It cannot resolve locally and is not meant to; it says where the document went. Two tests now accept that form and still fail on a LOCAL path that stopped existing, which is the case worth catching. WHAT WAS POINTED SOMEWHERE HONEST INSTEAD: The shipped SDLC baseline referenced `docs/guides/authoring-invariants.md` — a path into THIS repository that a consuming project never has. It was unresolvable for its audience before the move. It now names the command, which is available to anyone holding the catalog. `vocabulary-lint` scanned `docs/architecture` and `docs/guides`. Those are a symlink mount when present and absent otherwise, so keeping them would make the scan's reach depend on whether a developer had run `docs:mount` — green on one machine and green-for-another-reason elsewhere. The root instruction files stop pointing at `ARCHITECTURE.md`. It restated the directory contract and layer map that `layer-map.json` and its test already assert from the live tree — a second copy of a machine-checked fact, which is the kind that goes stale silently. TWO TOOL BUGS FOUND BY USING THEM. `transfer` walked the whole standing manifest and tried to re-copy files removed in an earlier pass; it now skips sources that are gone and says how many. `generate` enumerates tracked files, so running it AFTER a deletion produced an empty record that reconciles clean against nothing — it now merges rather than replaces, and refuses to shrink without `--allow-shrink`. Verified: 554 of 554 reconciled against the destination before deletion; full root suite at the 26-failure pre-existing baseline; typecheck clean; both architecture tiers green (53 files). The contract authority digest moved with the catalog edit and was re-approved through `authority-lock-cli` — a one-line lockfile diff, which is the proof nothing else drifted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
`documentation/` held 46 hand-written pages describing an Exarchos several refactors had moved past. They are removed rather than migrated: a stale page outranks the source in a search result, which makes it worse than no page. What survives is the machinery — the config, one index, and `public/`, where the deploy workflow stages the bootstrap installers so the README's install one-liner resolves to a stable URL. It now lives under `docs/`, the site's own manifest is gone, and its toolchain is a root devDependency, so `documentation/` stops being a top-level entry. Two of the 46 were not stale and did not die with the site: six harness runtime descriptors cite the facade rationale, and the call-macro note is the only record of that transition. Both went to lvlup-sw/docs@d51f25c alongside the rest of the prose exodus, and the six citations now point at the mounted path. `docs/` is also where `docs:mount` links several hundred relocated internal documents. VitePress follows those symlinks — verified, not assumed: with the exclusion removed the build follows them and dies compiling an ADR whose prose contains a bare `<...>`. That corpus fails loudly by luck; one that happened to parse would simply be published to a public site. So the config excludes what is mounted by reading the tree for symlinks, which cannot go stale the way a hard-coded list would, and is empty on CI where nothing is mounted. The guard is proved able to fire: seeding a clean, compiling corpus behind an exclusion that misses it makes the build SUCCEED and both assertions name the leak by subtree. Also removes `=54`, a zero-byte file committed by a shell-redirect typo in 92bb723 — a root entry DR-1 requires to be classified or gone. ## Changes - `documentation/` removed; `.vitepress/`, `public/` and a new `index.md` under `docs/` - site toolchain folded into the root manifest; `docs:dev`/`docs:build`/`docs:preview` and `docs.yml` retargeted - `srcExclude` derived from the mount symlinks, plus `README.md`, which VitePress would otherwise treat as a second index - the site skeleton added to the prose-exodus RETAINED list, which is what refused it until it was argued for - top-level contract, directory READMEs, build graph and the onboard doc sweep no longer name `documentation/` ## Test Plan - `tests/integration/docs-site-skeleton.test.ts` — builds through `npm run docs:build`, pins the published page set to exactly the home page and 404, and fails by name when a mounted subtree leaks - full root suite: 13,616 passing, 27 failing — the 26 pre-existing merge-orchestrate/store.race baseline plus one pre-existing onboard install failure that predates this branch - typecheck clean across all three programs; lint clean; `render:guard` reports no drift Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
`src/lifecycle/install-skills-bridge.js` is authored JavaScript, not compiled output. It is `.js` deliberately: tsc runs `allowJs: false` and must never resolve it, while bun's `--compile` bundler must, or `installSkills` and its lazy `@inquirer/prompts` import never reach the single-file binary. It was tracked from v2.9.0 until 0341396 folded `servers/` into the repo root. That move landed it under `src/`, where `.gitignore`'s `src/**/*.js` — written for TypeScript output — matched it and git dropped it. The deletion is visible in that commit and nowhere else; nothing failed, because every machine that had built the tree already had the file on disk. A fresh clone does not. `npm run build` runs `build:binary`, and bun stops at `Could not resolve: "../../lifecycle/install-skills-bridge.js"`, so release.yml and eval-gate.yml both fail at the bundle step on a clean checkout. Reproduced here by deleting the local copy and rebuilding, and fixed by restoring it: the bundle goes back to 940 modules. The content is the last tracked revision with the import paths its two moves should have carried — `../install/install-skills.js`, `../install/runtimes/`, and a runtimes directory that resolves to `content/harness/runtimes` instead of a repo-root `runtimes/` that no longer exists. The `.d.ts` beside it had already been updated for that path while the `.js` sat untracked, which is the clearest evidence the two had drifted apart. Not fixed here: `tests/unit/lifecycle/install-skills-bridge.test.ts` still fails to collect, because vite-node does not map the bridge's `.js` specifiers onto their `.ts` targets the way tsc and bun do. That is a resolver gap in the test runner rather than a defect in this module, it predates this change, and fixing it means touching resolution for every project in the workspace. ## Changes - `.gitignore` negates the compiled-output rule for the bridge and its `.d.ts`, with the reason stated at the rule - `src/lifecycle/install-skills-bridge.js` restored, with the import paths corrected for its current location - `src/lifecycle/install-skills-bridge.d.ts` tracked alongside it ## Test Plan - `bun run tools/release/build-binary.ts` bundles 940 modules and compiles; it fails outright without this file - `tests/scripts/build-binary.test.ts`, `tests/scripts/build-release-manifest.test.ts` and `tests/process/compiled-binary-mcp.test.ts` execute rather than aborting in `beforeAll` - full root suite unchanged at 13,616 passing / 27 failing, all pre-existing Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR
|
Important Review skippedToo many files! This PR contains 1842 files, which is 1692 over the limit of 150. To get a review, reduce the PR to 150 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to Pro+ to raise the limit. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1158)
📒 Files selected for processing (1842)
You can disable this status message by setting the |
|
Phase 2 isolated for review. #1817 is mostly Phase 0/1 renames (3,737 files); CodeRabbit skipped it. The only intended semantic range is isolated as #1819 (190 files, 10 commits) stacked on the Phase 1 tip:
The later docs/prose exodus (037–039) stays on this branch; it is relocation, not product semantics. #1819 is a review vehicle, not a merge path to |
Summary
The repository structure overhaul: 65 tasks across three phases, taking the tree
to the six top-level directories DR-1 states and making each rule that describes
it something a test can fail on.
The work is sequenced rather than parallel, and the phase boundaries are load-bearing:
local-only failures named, a test inventory keyed on a path-independent id so a
move of 1,141 files can prove it lost none, and a guard-liveness census. Four of
those oracles were found to be uncollected by construction, matching no vitest
project include; they passed by never running.
the published layer architecture,
scripts/andtest/dissolved into one testtree, authored
content/split from generatedrendered/, and repo automationconsolidated into
tools/.documentation. Six hotspots went from ~16.4k lines to ~2.9k across unchanged
published paths, and import-boundary lint reports zero violations over 986
modules for the first time on this branch.
Roughly 550 planning documents left the repository for
lvlup-sw/docsand mountback on demand, and this final task reduced the stale VitePress site to a build
skeleton — so
docs/is now two documents, a published skeleton, and a mount point.This is a draft opened for review, not for merge. The comment-hygiene fold
(570 findings across 71 modules, classifier in observe mode) is the one declared
criterion still open.
Changes
content/ docs/ rendered/ src/ tests/ tools/, plusbinding/andhooks/, whose paths an external contract fixes. A root entry isnow either declared with a reason or a test failure.
tests/, every tier mapped to exactly one vitestproject, with a contract test that fails if a tier is collected by none or two.
content/<domain>/renders per-runtime intorendered/,and one
render:guardreplaced three drift checks.read a frozen capture were repointed at the live tree; several that had gone
vacuous were made able to fail again.
gated on reconciling those digests against the destination. Nothing under
docs/is tracked unless it is argued for by name.documentation/removed; the skeleton lives underdocs/with itstoolchain in the root manifest.
Test Plan
named: 26 are the
merge-orchestrate/store.racecluster recorded in thePhase 0 baseline as local-only and green on CI, and one is an onboard install
test whose bridge module the runner cannot resolve. None are attributable to
this branch, and the baseline exists precisely so that claim is checkable
rather than asserted.
npm run typecheckclean across all three programs;npm run lintclean;npm run render:guardreports no drift; the binary bundles 940 modules.seeded a violation and confirmed the assertion names it. The most recent
example: the docs site's mount-leak guard only proves anything on a corpus that
compiles, because the real corpus makes the build die instead.
npm run validatefails on this machine becausecheck-measured-premisesre-derives premises out ofdocs/specs, which is amount, against a path that no longer exists. That directory is absent on CI, so
the gate passes there; unmount to reproduce CI's result.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Ee4wM4YXKJaW8LmH5eDPkR