TECH — Server-ledger cutover: task-view patch-server replaces ledger-cli (gates + guard port)
TECH — Server-ledger cutover: task-view patch-server replaces ledger-cli (gates + guard port)
Section titled “TECH — Server-ledger cutover: task-view patch-server replaces ledger-cli (gates + guard port)”- Task: ID-90 — Server-ledger cutover — task-view patch-server replaces ledger-cli (gates + guard port)
- Subtask: {90.3} TECH
- Session: S322 (authored 07/06/2026, fresh Planner per Q-PLANNER-2). Amended S335 (10/06/2026, fresh Planner) — surgical Phase 3 R-chain re-sequencing only (PRODUCT invariants untouched); root cause: the {90.21} flip’s
__tests__/setup.tsKH_LEDGER_SERVER='0'force-pin invalidated the spec’s “tests already run server-side” premise. See §Phase 3 amendment note, §T-4, §Testing S335 amendment, and §Verification S335 re-verification. - Predecessors:
docs/specs/ID-90-server-ledger-cutover/RESEARCH.md({90.1}) andPRODUCT.md({90.2}) — both ratified S322. Liam ratified ALL at the PRODUCT gate, including the §Simplicity review’s S2-staged single-source outcome: invariants 58–62 are binding (primitives single-source upstream at Phase 3;scoped-serialise.ts+ledger-budgets.tsrelocate upstream; schemas migrate at {68.30}; the vendor-drift workflow shrinks-then-deletes; the CI twin is NOT ported). - Identity hygiene: this document lands on a public-track repo. Client identity tokens are FORBIDDEN strings here; the denylist is referred to generically as “the configured client denylist” throughout. Denylist inspection for this spec was shape-only (
jqkey/type projection — no token or pattern values were read into this document).
Context
Section titled “Context”PRODUCT.md defines the behaviour (62 invariants); this spec maps each to a concrete change. The cutover converts scripts/ledger-cli.ts (4,367 lines, direct in-process primitives) into a thin transport façade over the task-view patch-server, with the three write gates, the conforming byte format, and four capability extensions ported upstream first (Phase 0), behind the ratified Option A two-phase shape.
Code-intelligence orientation is carried by the ratified RESEARCH §Context / PRODUCT §Context (Inv 2 satisfied there). Targeted supplements run for this document (07/06/2026):
ast-dataflow callers --symbol scripts/ledger-cli.ts:commitMutation→ 12 call sites, all"enclosing":"fn:run", all"confidence":"exact", lines 2443–3443 ofscripts/ledger-cli.ts({"query":"callers","args":{"symbol":"scripts/ledger-cli.ts:commitMutation"},"results":[…12 entries, file scripts/ledger-cli.ts, enclosing fn:run…],"truncated":false}).commitMutationis therefore the single transport seam for every single-ledger mutation; the only write paths outside it arepromote()(ledger-cli.ts:3551–3955) and the self-contained umbrella write (3957–3984,serialiseUmbrellasat 3982).- Canonical denylist shape verified at the private docs-site repo (
ops/identity-denylist.json, shape-onlyjq):tokens[].{value: string, case_insensitive: boolean, class: string, covers_examples?/note?}(4 tokens;case_insensitivetakes bothtrueandfalseacross tokens — per-token handling is mandatory) plusexclusion_patterns[].{pattern: string, reason: string}(4 entries) plusnon_token_classes[],consumers[], metadata. The file’s own comment confirms the ledger guard consumes the same synced secret. identity-guard.ymlcarve-out mechanics read at lines 375–410:CARVEOUT_PATTERNSbuilt fromexclusion_patterns[].patternlowercased (lines 379–382); per-token loop with per-tokencase_insensitiveflag (384–390); each matched line’s content is lowercased and substring-tested against every carve-out pattern — carved-out lines are excluded from the net count (case-statement at ~398–408).docs/reference/umbrellas.json:document_nameis the literal"umbrellas"; top-level keys{document_name, document_purpose, last_updated, related_documents, umbrellas}; 4 umbrella records shaped{id, title, substrate_doc, task_ids[], status, phase}.- Upstream vendored-schema drift discovered: task-view’s
packages/schemas/src/task-list-schema.ts:224–243carries the ID-20-eraparseTaskListWithWarnings(25-Subtask ceiling only). KH’s canonicallib/validation/task-list-schema.ts:28importsFIELD_BUDGETSand surfaces budget soft-warnings. Without a schema re-vendor, invariant 41 cannot hold server-side — addressed by U0. - ci.yml line drift vs RESEARCH: the
ledger-mirror-parityjob now sits atci.yml:1087+andTASK_VIEW_TAG: v0.3.1-task-viewatci.yml:1139(RESEARCH cited 936–1006/988; the file has grown since — same content).regen-mirrors.sh:28–31self-resolves the tag from ci.yml, so it needs no separate bump. The vendor-drift workflow pins the tag attask-view-vendor-drift.yml:86and:214.
Key seams (all read 07/06/2026; substrate from the pinned clone .cache/task-view-v0.3.1-task-view @ 1644742762df3f202ab2a7bd3507aab5758fdd30):
| Surface | Site |
|---|---|
| Façade argv/envelope | scripts/ledger-cli.ts — parseArgs 349, flags 287–331, emit 1120, CliResult 150–165 |
| Façade gates | record-set 1315–1486; budget 1488–1649 (GRAPHEME_SEGMENTER 1589); disciplineWarnings 1686 |
| Façade write seam | commitMutation 1922–2068; promote 3551–3955; umbrella 3957–3984; nextId 645–674; withCreateDefaults 2237 |
| Substrate handlers | packages/server/patch-server.ts — readCanonical 131–143, serialiseLedger 201–205, PATCH 526 (mtime check 578–601), POST record 719, DELETE 874, transaction 1051, regen 1157, dispatcher 1229–1291, factory startPatchServer 1307–1343 (loopback enforced 1308) |
| Substrate lifecycle | packages/server/ledger.ts — startTaskViewServer 182+, MAX_PORT_RETRIES = 5 (39), waitForExit |
| Substrate primitives | detect-schema.ts:38–44 (KNOWN_DOCUMENT_NAMES, exactly three), patch-apply.ts (FieldPatch 81, applyPatches 420), record-mutate.ts (insertRecord 136, removeRecord 192), ledger-transaction.ts (two-ledger promote, fault seam), cross-ledger.ts:27–59 (LedgerSlug slug↔name map), apps/server/index.ts (parseCliArgs 57–80, runRegenCheck 224–261) |
| Guard lineage | git show 245c35ac:scripts/ledger-cli.ts 1895–1976 — note: guardClientName’s rejection/override messages interpolate [${names}] (the joined denylist). This is a redaction defect against invariant 32 and must NOT be ported as-is. |
Decisions (PRODUCT OQ/T table — each: decision, rationale, rejected alternative)
Section titled “Decisions (PRODUCT OQ/T table — each: decision, rationale, rejected alternative)”OQ-2 — Server lifecycle and topology
Section titled “OQ-2 — Server lifecycle and topology”Decision: one singleton, multi-document, loopback daemon per ledger directory, spawned on demand by the façade, discovered via an atomic handle file, health-checked and version-pinned, with idle self-exit.
- Topology: the daemon serves all in-scope documents in the directory (three ledgers +
umbrellas.json) via slug-routed endpoints (U9). One process = one in-process mutation mutex per canonical path, which is the “single long-lived writer as enforcement chokepoint” the cutover exists to create (RESEARCH §Gap analysis, substrate advantages). - Lifecycle: façade’s
ensureServer(ledgerDir)(K3) reads.cache/ledger-server/handle.json({port, pid, version, ledgerDir}), validates withGET /api/healthand a version-vs-pinned-tag check (tag parsed fromci.ymlexactly asregen-mirrors.sh:28–31does), and on miss/stale: takes anO_EXCLlock, kills any stale pid, spawnsbun .cache/task-view-<TAG>/apps/server/index.ts --serve-dir <dir> --no-browser --port 0 --port-file <handle> --idle-exit 30(flags added in U9), waits for the handle file + health 200 under a hard 10 s deadline (invariant 54: fails loudly, never hangs, never falls back to an ungated write). Child stdio routes to the façade’s stderr (invariant 13). Non-default--ledger-dirvalues (tests, the parity harness) get an ephemeral per-invocation server instead of the shared daemon — no handle-file pollution. - Idle self-exit (30 min, façade-supplied) prevents zombie daemons across sessions and guarantees stale-tag servers age out even if the version check is bypassed.
Rationale: parallel cmux sessions write the main checkout’s ledgers (the Orchestrator owns ledger writes), so one per-checkout daemon funnels every mutation through one mutex — closing the cross-process TOCTOU window between mtime check (patch-server.ts:578–601) and rename that no per-invocation design can close. The transaction endpoint already directory-scans siblings (resolveTransactionSiblings, patch-server.ts:1002–1023); a directory-scoped daemon matches the substrate’s own model. Rejected: (a) ephemeral server per mutation — loses single-writer serialisation, reopens the TOCTOU race across two server processes, pays startup per invocation; (b) launchd/system daemon — operator-managed state, stale-version hazard, violates invariant 54’s “operator never manages instances”; (c) four per-document daemons — 4× lifecycle complexity and promote spans documents anyway (invariant 56).
OQ-4 — append-journal server-side semantics
Section titled “OQ-4 — append-journal server-side semantics”Decision: first-class server-side append patch op (U6): FieldPatch widens to {fieldPath, newValue} | {fieldPath, appendText}; the server resolves the leaf at apply time, inside the per-document mutex, and concatenates.
Rationale: journal blocks are the workflow system’s audit evidence — the worst loss class. Under the mutex + apply-time read, an interleaved-append loss is structurally impossible rather than retry-mitigated; invariants 39 + 43 hold by construction, and the retry loop (T-2) only remains responsible for cross-write 409s. Rejected: façade read-concatenate-PATCH under baseMtime — correctness rides entirely on the client retry loop; a retry bug silently drops journal evidence; doubles round-trips under contention; and replicates append logic per client, against invariant 9’s spirit.
OQ-7 — exclusion_patterns[] applicability to ledger byte-delta counting
Section titled “OQ-7 — exclusion_patterns[] applicability to ledger byte-delta counting”The two coherent semantics, from the identity-guard.yml:375–410 study and the canonical shape read:
- Semantics A — apply carve-outs: mirror identity-guard exactly — when counting hits over the bytes-about-to-be-written and the prior bytes, a hit whose surrounding line content (lowercased) contains any
exclusion_patterns[].patternas a substring is excluded from both counts. One unified semantics across all denylist consumers. - Semantics B — do not apply (parse-but-ignore): the ledger guard counts all per-token hits;
exclusion_patterns[]remain identity-guard-only carve-outs. The parser still accepts the field (invariant 29’s shape includes it) but does not evaluate it.
Recommendation: Semantics B. Rationale: (i) the four recorded carve-out reason strings are all anchored to specific repo source files (an env-example placeholder line, a UI component placeholder, parameterised-mechanism config keys) — none describes ledger document content; (ii) the ledger guard is delta-based: pre-existing carve-out-shaped content counts equally on both sides and can never cause a rejection, so carve-outs would matter only for net-new content — and a net-new ledger write embedding a denylist token inside a config-key-shaped string is precisely the write the guard should challenge (the KH_LEDGER_ALLOW_CLIENT_NAME=1 override covers the rare legitimate case, invariant 33); (iii) importing grep-line carve-out semantics into a byte-delta counter adds a silent pass-open path for zero demonstrated need. Rejected: Semantics A — uniformity is not worth a pass-open path in a write-time guard. RATIFIED: Semantics B (Liam, S322 TECH gate) — carve-outs parse-but-ignore in the ledger guard; the override remains the escape hatch.
T-1 — Transitional dual-shape denylist parser
Section titled “T-1 — Transitional dual-shape denylist parser”Decision: JSON-only from day one. The guard parses KH_CLIENT_NAME_DENYLIST strictly as the canonical JSON (shape predicate mirroring identity-guard.yml:89); any set-but-unparseable or wrong-shape value — including a legacy comma-separated string — is a loud configuration error blocking the write (invariant 35). Rationale: the live secret is already full JSON ({68.15} widening); a JSON-first/comma-fallback parser converts every malformed-JSON typo into silent comma-split garbage tokens — the exact mis-count class that created bl-244. Migration is a one-time operator action (sync .env.local from the canonical private file), documented in the P1 runbook line. Rejected: dual-shape transitional parser — violates invariant 35’s fail-loud mandate via the silent semantic flip.
T-2 — Façade conflict-retry policy
Section titled “T-2 — Façade conflict-retry policy”Decision: retry only on HTTP 409 mtime-mismatch; up to 3 automatic retries after the initial attempt (4 submissions max); backoff 50 ms / 150 ms / 400 ms with ±50% jitter. Each retry re-reads via the server (fresh baseMtime), re-derives intent per invariant 43’s intent classes (the server re-runs all gates regardless), and re-submits. Exhaustion → standard failure envelope error: "mtime-mismatch", exit 1, nothing written (invariant 44). Success-after-retry → warnings[] entry mtime-conflict: write succeeded after N retry/retries (invariant 45). Connection-refused gets one lifecycle respawn attempt (K3), separate from the 409 budget, then fails loudly. Rationale: under the daemon mutex, conflicts only arise from a sibling session writing between this invocation’s read and submit — rare and sub-second; 3 bounded retries absorb that without hang risk. Rejected: unbounded retry (hang, invariant 54); zero retries (turns routine parallel-session interleaving into operator-visible failures, regressing Goal G-C).
T-3 — Per-request override transport
Section titled “T-3 — Per-request override transport”Decision: per-request JSON body fields on every mutation body: {dryRun?: boolean, force?: boolean, allowClientName?: boolean, regenMirrors?: boolean} (U10). The façade maps --dry-run/--force/--no-regen-mirrors and the KH_LEDGER_ALLOW_CLIENT_NAME=1 env to these fields per invocation. The server holds no override state between requests (invariants 26, 33). Rejected: HTTP headers — out-of-band to the typed, Zod-validated body; invisible to the OQ-T3 body-shape log discipline; easier to leak into a default client config and thereby become session-global.
T-4 — S2 interim consumption mechanism + lib/ledger deletion sequencing
Section titled “T-4 — S2 interim consumption mechanism + lib/ledger deletion sequencing”Decision: schemas remain canonical in KH until {68.30} (invariant 62) — the interim “consumption mechanism” is the status-quo schema vendoring refreshed at the v0.4.0 cut (U0); no git dependency or tag-pinned vendor read is introduced. lib/validation/ledger-budgets.ts stays in KH through Phase 3 (it is imported by lib/validation/task-list-schema.ts:28 for parse-time soft warnings, which live until {68.30}); its relocated upstream twin (packages/schemas/src/ledger-budgets.ts, U0) becomes the server gate’s registry, and the KH copy joins the vendor-drift schema-arm watch list (R3) — the “deliberate revision of the watched-path list” invariant 59 itself mandates.
Deletion sequencing at Phase 3 is an ordered commit chain. This ordering was re-derived in S335 (see §Phase 3 amendment note) because the original R1 (direct-path removal) → … → R4 (test migration) order is unsatisfiable under the green-per-commit gate: the {90.21} flip added __tests__/setup.ts:55–70, which FORCE-SETs KH_LEDGER_SERVER='0' process-wide, so 16 of the 17 run-importing ledger-cli-*.test.ts suites (248 run() mutation calls) exercise the DIRECT WRITE path (the 17th, ledger-cli-discoverability.test.ts, is read-only). Removing the direct path before migrating those tests fails compilation (four suites import __setRegenRunnerForTest/mirrorReminderFor/assertRecordSet — all R1-deletion export-block targets at scripts/ledger-cli.ts:4639–4653; plus ledger-cli-umbrella.test.ts + ledger-cli-input.test.ts import lib/ledger primitives that R2 deletes) and behaviour (the mirror suite asserts the in-process regenSpy, a direct-path-only seam). The re-derived chain is test-migration-first: R1a (un-pin + re-target the in-process suites to the transport path against ephemeral servers WHILE the direct path still exists → direct path becomes dead code) → R1b (façade hard-cut: now a near-dead-code deletion of the direct write path + flag) → R2 (delete lib/ledger/{atomic-write,detect-schema,patch-apply,record-mutate,scoped-serialise}.ts; retire scripts/{ledger-renormalise,ledger-normalise-oqls2}.ts + tests) → R3 (drift-workflow primitive arm retires; budgets path added to schema arm) → R4 (residual disposition accounting) → R5 (docs) → R6 (journal evidence). R1a/R1b/R2/R3 live in {90.22}; R4/R5/R6 in {90.23}. Rejected: introducing a git-dependency for schemas now — needless mechanism for a window already bounded by {68.30}, and it would prematurely demote the KH canonical that invariant 62 protects. Rejected (S335): keeping the original R1-first order with an exception carve-out for the affected suites — the suites cannot compile against a partially-deleted export block, so no carve-out exists; the migration is a hard precondition, not a parallel concern.
T-5 — Read-path routing under flag ON
Section titled “T-5 — Read-path routing under flag ON”Decision: Phases 1–2: show/get/schema stay façade-local (current in-process read code, both flag states) — byte-identity (invariant 15) is trivial and a read never spawns the daemon. Phase 3 (after R2 deletes the vendored detect-schema): show/get re-route through GET /api/ledger/:slug + GET /api/ledger/:slug/record/:id; the schema subcommand stays façade-local — it renders from Schema.shape off lib/validation/*, which remain canonical in KH until {68.30}. Rejected: routing reads through the server from Phase 1 — couples every read-only invocation to server lifecycle for zero behaviour gain; rejected: keeping a KH-local document-name switch after Phase 3 — a parallel re-implementation of detectSchema against invariant 58’s spirit.
Error-vocabulary note for the Checker (invariant 14): the transport adds exactly one code, mtime-mismatch, as PRODUCT states. The guard port additionally surfaces client-name-guard (rejection) and client-name-guard-config (set-but-invalid denylist, invariant 35) — these are mandated by invariants 28–36/AC-H2 themselves (the codes previously existed only on the id68 branch lineage) and appear only when the guard is configured. Recorded here as a deliberate clarification of invariant 14’s “exactly one”, not a deviation; flag to Liam alongside OQ-7.
Proposed changes
Section titled “Proposed changes”Phase 0 — upstream task-view (new tag v0.4.0-task-view; zero KH impact)
Section titled “Phase 0 — upstream task-view (new tag v0.4.0-task-view; zero KH impact)”- U0 — Schema re-vendor + budgets/umbrellas vendor. Refresh
packages/schemas/src/{task-list-schema,roadmap-schema,backlog-schema,work-status}.tsfrom KH canonical (brings the budget-awareparseTaskListWithWarnings— the drift found in §Context). Addpackages/schemas/src/ledger-budgets.ts(relocated registry:LEDGER_BUDGETS/FIELD_BUDGETS/DISCIPLINE_DOC, plain data; header carries the never-.max()mandate verbatim per invariants 24/59) andpackages/schemas/src/umbrellas-schema.ts(vendored fromlib/validation/umbrellas-schema.ts). - U1 — Conforming serialisation port. New
packages/server/scoped-serialise.ts— port of KHlib/ledger/scoped-serialise.ts(408 lines:escapeNonAscii,escapeSerialise,scopedSerialise,scopedSpliceSerialise) extended with an umbrellas walk (['umbrellas', id, field]). DeleteserialiseLedger(patch-server.ts:201–205). Handler routing:handlePatchRecordfoldsscopedSerialiseleft overcanonical.rawTextperFieldPatch(Zod viaapplyPatchesstays the validation oracle; written bytes come from the parsed-original);handlePostRecord/subtask POST usescopedSpliceSerialise;handleDeleteRecordusesescapeSerialise(result.detected.data)(whole-file conforming — matches the CLI’s {65.2}/{65.3} whole-file deletes, byte-compatible post-OQ-LS-2);promoteTransactionstaged contents: splice (ADD leg),escapeSerialise(REMOVE leg),scopedSerialise(roadmap link leg). - U2 — Budget gate. New
packages/server/gates/budget-gate.ts— port ofcheckBudget+graphemeLength/GRAPHEME_SEGMENTER(ledger-cli.ts:1488–1649), registry from U0. Hook: post-mutation / pre-serialisation. PATCH: each patched field ismutatedField(can hard-reject); untouched over-budget fields soft-warn. Creates (record/subtask/promote task leg): create mode, first over-budget field fatal.force: truebody field downgrades to a(forced) budget-exceeded:warning in the responsewarnings[]. - U3 — Record-set gate. New
packages/server/gates/record-set-gate.ts— port ofcollectionIds/assertRecordSet/checkRecordSet(ledger-cli.ts:1315–1486). Hook: post-serialisation / pre-atomicWriteFilein every mutating handler, on the exact bytes about to land. Deltas: PATCH/umbrella-PATCHnone; POST record/subtaskadd(add-manybulk); DELETEremove; promote per-leg at stage time: task-list+1, backlog−1, roadmap∅. - U4 — Client-name guard. New
packages/server/gates/client-name-guard.ts:parseDenylist()— strict JSON per T-1, shape predicate mirroringidentity-guard.yml:89; per-token regex-escaped matcher honouring each token’scase_insensitiveflag ('g'vs'gi', mirroring identity-guard’s per-token loop at 384–390);exclusion_patterns[]parsed-but-ignored per OQ-7 recommendation (pending ratification); net-new delta over (rawText, serialised bytes); rejection → 422{error: 'client-name-guard', detail: '<delta count + paths only>'}— fully redacted (the 245c35ac[${names}]interpolation is deliberately NOT ported; no token, pattern text, or matched content in any response, log, or warning);allowClientName: truebody field → redacted warning + allow; set-but-invalid env → 500{error: 'client-name-guard-config'}blocking every mutation; unset env → inactive, unless the server runs with--require-denylist(passed by the façade lifecycle whenprocess.env.CIis truthy) in which case unset is the same loud config error (invariant 34). Hook: post-serialisation / pre-write on all four mutating handlers, each transaction leg, and the umbrella path. - U5 — Subtask CRUD + auto-id + create defaults. Extend
packages/server/record-mutate.ts(or siblingsubtask-mutate.ts):insertSubtasks(detected, taskId, subtasks[])(fold-left; per-recordnextIdmax+1 semantics ported from ledger-cli.ts:645–674; duplicate-id pre-check; whole-doc Zod re-parse) andremoveSubtask(detected, taskId, subId). Server-side defaults on create when absent (subtask:status: 'pending',dependencies: []; record: portedwithCreateDefaultssemantics from ledger-cli.ts:2237).POST /api/ledger/recordallocatesnextIdwhenrecord.idis absent. New endpoints:POST /api/ledger/:slug/record/:taskId/subtask({baseMtime, subtasks: […], …overrides}→ 201{ok, newMtime, subtaskIds, warnings?}),DELETE /api/ledger/:slug/record/:taskId/subtask/:subId({baseMtime}). - U6 — Append op.
packages/server/patch-apply.ts:FieldPatchwidens to{fieldPath, newValue} | {fieldPath, appendText}; the walk concatenates at the leaf at apply time. Same op supported inscopedSerialise’s parsed-original application. Servesappend-journaland the--appendforms ofupdate-backlog/update-roadmap. - U7 — Promote third leg.
ledger-transaction.ts:PromoteTransactionInputgainscapabilityTheme?: {roadmapPath, roadmapBaseMtime, themeId}; pre-stage validation rejects unknown themes (422 unknown-theme, nothing staged); idempotentlinked_tasks[]push; three staged temps; commit order task-list (ADD) → roadmap (idempotent link) → backlog (REMOVE) — additive first, removal last, preserving the benign-transient-duplicate property.handlePostTransactionbody gainscapabilityThemeId?+roadmapBaseMtime?. Record-set + guard run per leg at stage time. - U8 — Umbrellas document kind.
detect-schema.ts:KNOWN_DOCUMENT_NAMESgains the fourth literal"umbrellas"(lines 38–42); new union arm{kind: 'umbrellas', data: UmbrellasSchema.parse(…)}. Ripple:mirror-generatorreturns the empty plan for'umbrellas'(no mirror dir — invariant 53);patch-applygains the umbrellas walk;cross-ledger.tsLedgerSluggains'umbrellas'. Umbrella membership edits are field PATCHes on['umbrellas', id, 'task_ids']— record-set delta∅over the umbrella id-set; no budget entries fabricated (invariant 50). - U9 — Multi-document daemon, mutex, lifecycle affordances.
apps/server/index.tsgains--serve-dir <dir>(scan + register all known-document files incl. umbrellas),--port-file <path>(atomic handle write{port, pid, version, ledgerDir}once listening),--idle-exit <minutes>,--require-denylist.patch-server.ts: routes gain the slug segment/api/ledger/:slug/…(bare/api/ledger/*keeps routing to the launch document for viewer back-compat); per-canonical-path mutation mutex (promise-queue) wrapping every mutating handler body; the transaction acquires its two/three paths in fixed lexicographic order.GET /api/health→{ok, version, ledgerDir, documents: [{slug, document_name, path}]}. - U10 — Request/response envelope extensions. All mutation bodies accept
{dryRun?, force?, allowClientName?, regenMirrors?}(T-3). Success responses gainwarnings?: string[]— porteddisciplineWarnings(ledger-cli.ts:1686, with {35.30}warningScopebounding) over the U0 vendored parse helpers, plus budget soft-warns, forced downgrades, and guard-override warnings.dryRun: trueruns the full gate chain and returns the would-be payload with no write, regen, or mtime change (invariant 16).regenMirrors: falseskips regen and reports it. - U11 — Upstream test ports.
packages/server/scoped-serialise.test.ts(port of__tests__/lib/ledger/scoped-serialise.test.ts);packages/server/gates/{budget-gate,record-set-gate}.test.ts(coverage ported fromledger-cli-budget.test.ts/ledger-cli-record-set.test.ts);packages/server/gates/client-name-guard.test.ts— relocation of the 245c35ac 7 evaluator tests + 52ef3d5b 3 mutation-guard tests, JSON-shaped synthetic fixtures only (AC-H2 / AC-I), plus new redaction assertions (response bodies and logs are grepped for the synthetic fixture tokens) and config-error tests (invalid JSON, emptytokens[], legacy comma string → loud error); concurrency suite (mutex serialisation, auto-id race → no duplicate ids, interleaved appends → both blocks present); umbrellas detect/patch suite; transaction third-leg + fault-injection extension. - U12 — Tag. Cut
v0.4.0-task-view+ release assets (schemas bundle now incl.ledger-budgets+umbrellas-schema).
Phase 1 — KH façade (flag OFF by default)
Section titled “Phase 1 — KH façade (flag OFF by default)”- K1 — Pin bumps.
ci.yml:1139TASK_VIEW_TAG: v0.4.0-task-view;task-view-vendor-drift.yml:86and:214;regen-mirrors.shself-resolves (cache re-keys to.cache/task-view-v0.4.0-task-view). Fix the stalelib/ledger/README.mdpin (OQ-9:v0.2.0→v0.4.0). - K2 — Transport client. New
scripts/ledger-server-client.ts: builds requests from aServerIntent(K4); envelope mapping layer — HTTP{error, detail, issues}passes through intoCliResult{ok: false, subcommand, error, detail?, issues?}(codes preserved:schema-error,walk-error,duplicate-id,record-not-found,budget-exceeded,record-set-violation,unknown-theme, …); 409mtime-mismatchis consumed by the T-2 retry loop, surfacing only on exhaustion; 500mirror-regen-failedwithcanonicalWritten: truemaps to{ok: true, …, mirrorStale: true, mirrorStaleReason: 'regen-failed'}(preserving the {35.32} post-write-alert contract);regenMirrors: falseround-trips asmirrorStaleReason: 'suppressed'; serverwarnings[]pass through; retry-success warning appended (invariant 45). - K3 — Lifecycle manager. New
scripts/ledger-server-lifecycle.tsimplementing OQ-2: handle-file discovery, health + version check,O_EXCL-locked spawn, kill-and-respawn on tag mismatch, ephemeral servers for non-default--ledger-dir, child stdio → stderr, 10 s hard deadline. - K4 — Façade flag branch.
scripts/ledger-cli.ts:serverEnabled()readsKH_LEDGER_SERVER(=== '1'in Phase 1).CommitMutationOptionsgainsserverIntent?: ServerIntent(discriminated union:field-patch | record-create | subtask-create | subtask-delete | record-delete | umbrella-patch, each carrying slug/ids/patches); the 12commitMutationcall sites (ast-dataflow, §Context) populate it.commitMutationbranches at the top: flag ON →transportCommit(opts)(K2/K3); flag OFF → existing body, untouched.promote()branches toPOST /api/ledger/transaction(incl.capabilityThemeId). The umbrella arm branches to a slug-routed PATCH.parseArgs, the flag tables (287–331),emit(1120), reads, andschemaare untouched — invariants 10–13, 15, 17 hold structurally. - K5 — Differential parity harness. New
scripts/ledger-differential-parity.ts(invariant 21 / AC-P1): copy the live ledgers +umbrellas.jsoninto two$TMPDIRfixture dirs; run the full mutating-subcommand matrix (× scoped/--whole-file×--force×--dry-run) via flag-OFF against dir A and flag-ON (ephemeral server) against dir B; assert byte-identical files (cmp), deep-equal envelopes (modulo absolute paths and retry warnings), equal exit codes, byte-identical mirrors. CI: a new step inside theledger-mirror-parityjob (ci.yml:1087+), after its existing tag-keyed clone +bun install(reuses the checkout); the step exports a synthetic denylist so the guard arm is exercised. Local:bun scripts/ledger-differential-parity.ts. - K6 — Umbrellas normalisation (invariant 51). One isolated commit: re-emit
umbrellas.jsonviaescapeSerialise(one-off script or an extension ofscripts/ledger-renormalise.ts), with a pre-commit deep-equal parse assertion (content-neutral); the same commit flipsserialiseUmbrellas(ledger-cli.ts:3982–3984) toescapeSerialise— otherwise the next flag-OFFupdate-umbrellawould silently denormalise the file — and updatesledger-cli-umbrella.test.tsbyte expectations.
Phase 2 — default flip + soak
Section titled “Phase 2 — default flip + soak”- F1.
serverEnabled()default flips ON (KH_LEDGER_SERVER=0becomes the explicit rollback switch — invariant 5 preserved). Soak ≥1 full SDLC session per AC-P2: scoped one-line diffs observed on real writes,ledger-mirror-paritygreen, zero gate regressions, ≥1 conflict-retry exercised (live, or synthetic via the U11 concurrency test), rollback drill recorded.
Phase 3 — retirement + single-source (S2-i)
Section titled “Phase 3 — retirement + single-source (S2-i)”Amendment note (S335, escalation-driven). The original Phase 3 chain (
R1 direct-path removal → R2 → R3 → R4 test migration → R5 → R6) was re-derived after two executors (exec-9021, exec-9022-r1) verified a ground-truth change that postdates the spec: the {90.21} default-flip commit (9160d0e3b, Checker-endorsed) added__tests__/setup.ts:55–70, which FORCE-SETsKH_LEDGER_SERVER='0'for the whole Vitest process. The spec premise — “the in-processledger-cli-*.test.tssuites already run server-side post-flip” — is therefore FALSE: of the 17 suites importingrun, 16 exercise the direct WRITE path (248 in-processrun()mutation calls; the 17th,ledger-cli-discoverability.test.ts, is read-only and stays façade-local), four of them importing R1-deletion export-block targets (__setRegenRunnerForTest,mirrorReminderFor,assertRecordSetatscripts/ledger-cli.ts:4639–4653) and the mirror suite asserting the direct-path-only in-processregenSpy. Root cause: the {90.21} pin. Removing the direct path before migrating those tests violates the green-per-commit gate (compile failure + behavioural divergence). R1 is split into R1a (test-migration-first un-pin/re-target) → R1b (direct-path removal as near-dead-code deletion); R4 is reduced to residual disposition accounting. Each commit below still leaves fullbun run testgreen ANDtsccompiling. PRODUCT invariants are untouched — this is sequencing only. Pre-condition commit 0 (ci.ymlexplicit.cache/task-view-<tag>provisioning,ba8976d42) is already merged; sequencing starts after it.
- R1a — Test migration (un-pin + re-target; direct path still present). Remove the
__tests__/setup.ts:55–70globalKH_LEDGER_SERVER='0'force-pin. Precise scope (S335 fix 2): 17ledger-cli-*.test.tssuites importrunfrom@/scripts/ledger-cli; exactly 16 exercise the direct WRITE path and get re-targeted to the transport; the 17th —ledger-cli-discoverability.test.ts— is READ-ONLY (it calls onlyschema/get/show/show-task+ an unknown-subcommand case) so reads stay façade-local per T-5 and it must NOT be wired to an ephemeral server. Each re-targeted write suite runs against the transport path (an ephemeral server over itsbeforeEachscratch--ledger-dir, allocated once per suite, reused across the suite’srun()calls to bound spawn cost — NOT one server per call). Relocate the direct-path-only seam assertions:ledger-cli-mirror.test.ts’sregenSpy/__setRegenRunnerForTestsemantics are acknowledged moved upstream (server-side regen) and the façade form keeps only themirrorStale/suppressed/regen-failedmapping it can observe over the transport;ledger-cli-record-set.test.ts’sassertRecordSetimport drops (byte coverage upstream, façade keepsrecord-set-violationenvelope assertions);ledger-cli-friction-s299.test.ts’smirrorReminderForimport drops (stderr-reminder text is still emitted by the façade’semit()path, asserted via stderr capture, not symbol import).ledger-cli-stdout-purity.test.ts’s__setRegenRunnerForTestimport drops (stdout purity is now asserted over a real ephemeral-server run).lib/ledgerimport drops from Retained suites (S335 fix 1 — these survive R1b and would breaktscat the R2 boundary otherwise):ledger-cli-umbrella.test.ts:35importsescapeSerialisefrom@/lib/ledger/scoped-serialise(used only to build expected bytes) → replace with an inline fixture serialiser or source the byte-conforming expected from the upstream.cachebundle;ledger-cli-input.test.ts:18importsdetectSchemafrom@/lib/ledger/detect-schema→ replace with a structural assertion not tied todetectSchema(this suite testsparseArgs/readRecordInput/nextId, not the document-kind detector, and is itself NOT a write-path suite — it importsparseArgs/readRecordInput/nextId, notrun). Determinism: the transport path readsKH_LEDGER_NOWvialedgerNow()(scripts/ledger-cli.ts:2314–2315); the same seam pins create-time timestamps CLI-side inwithCreateDefaults, so flag-OFF and flag-ON byte outputs match — timestamp-sensitive byte assertions pin the clock exactly as the K5 harness does. Exit state of R1a: the direct write path is DEAD CODE — no test exercises it; no Retained suite imports alib/ledgersymbol that R1b/R2 will delete; the K5 OFF-vs-ON differential (invariant 8 proof) still runs because both paths still exist;bun run testgreen;tscclean. - R1b — Direct-path removal (near-dead-code deletion).
scripts/ledger-cli.tssheds the in-process write machinery: localserialise(), the gate functions (collectionIds/assertRecordSet/checkRecordSetandcheckBudget/graphemeLength/disciplineWarnings,:1426–:1810), theregenRunner/maybeRegenMirrorsshell-out (:1808–:1903),atomicWriteFileusage, theserverEnabled()flag branch (server unconditional —commitMutation+promote+ theupdateUmbrellaself-contained read-mutate-write at invariant 49, which K4 deferred its server wiring to here). (Line ranges are approximate — ~70 lines drifted since S322 authoring; resolve by function name, not line.) The now-unused export-block symbols (__setRegenRunnerForTest,mirrorReminderFor,assertRecordSet) are removed from the:4639–4653block (R1a already dropped every importer). Argv surface, envelopes, exit codes intact (invariant 8). Reads re-route per T-5 (show/get→GET /api/ledger/:slug+/record/:id;schemastays façade-local). The K5 harness (scripts/ledger-differential-parity.ts) converts to transport-only mode in this commit: the OFF arm has no direct path to compare, so the harness runs only the ON arm as a regression check (asserting success + OQ-LS-2-conforming bytes), retained until {68.30} re-homes the gate (TECH §Testing). Exit state: suite already on transport (R1a), so green;tscclean. - R2 — Deletions. Delete
lib/ledger/{atomic-write,detect-schema,patch-apply,record-mutate,scoped-serialise}.ts(+ README delete-or-rewrite as a tombstone pointer); retirescripts/ledger-renormalise.ts,scripts/ledger-normalise-oqls2.ts+ledger-renormalise.test.ts+__tests__/lib/ledger/scoped-serialise.test.ts(their suites live upstream per U11). Also retire the last non-CLIlib/ledgerimporter (S335 fix 1):scripts/ledger-sweep-s269.ts— a historical one-off sweep script (untested, in no CI lane) that importsescapeSerialisefrom@/lib/ledger/scoped-serialise; its sweep was a point-in-time migration already applied, so delete it (or, if a future need is suspected, re-sourceescapeSerialisefrom the upstream.cachebundle rather than the deleted KH primitive).lib/validation/ledger-budgets.tsstays (T-4) with its header annotation revised (no longer “KH-only/never-vendor”; now schema-arm-watched). Pre-deletion gate (S335 fix 1 — covers test + scripts + production importers, not just production):ast-dataflow importers/dead-exportsover eachlib/ledger/*module confirms zero remaining importers acrossscripts/,__tests__/, AND production before anyrm. By this point the importer set has been fully drained: R1a dropped the two Retained-suite imports (-umbrella,-input); R1b dropped thescripts/ledger-cli.tsimports; this R2 step removes/retiresledger-renormalise.ts,ledger-normalise-oqls2.ts,ledger-sweep-s269.ts,ledger-renormalise.test.ts,scoped-serialise.test.ts. The gate must show an empty importer set on the EXACT module list. - R3 — Drift-workflow shrink (invariants 59–60). Remove the four
lib/ledger/*watched paths + the primitive-diff step (task-view-vendor-drift.yml) — the primitive arm retires; addlib/validation/ledger-budgets.tsto the schema-arm watched paths + asset diff (the watched-path revision invariant 59 mandates; arm-level shrink per invariant 60 preserved — no new arms). The schema arm + workflow file deletion ride {68.30}, not this Task. - R4 — Residual disposition accounting. Most test migration is done in R1a (the green-per-commit precondition); R4 now records the residual AC-P3 18-file disposition that is NOT a precondition of R1b: confirm the budget/record-set/scoped-serialise/scoped-create/promote-scoped/bulk-add/delete-subtask/capability-bundle byte-shape coverage is accounted upstream (U11) with thin façade intent-mapping tests retained; confirm
ledger-cli-integration.test.tsre-pointed at the transport; confirm the two module-coupled tests (ledger-renormalise.test.ts,scoped-serialise.test.ts) retired with their modules in R2. The full 18-file disposition table (§Testing) is the accounting oracle. No new direct-path-removal work lands here. - R5 — Docs.
docs/reference/task-list-discipline.md§3 (enforcement point = substrate, façade = operator surface — prose only, invariant 57);docs/runbooks/ci.md; enforcement-point prose inworkflow-orchestration(SKILL.md:390, 415–422),update-roadmap-backlog(SKILL.md:10, 24),triage-finding,workflow-curator.md,task-planner.md— documented invocations themselves remain valid unchanged. - R6 — Journal evidence. ID-90 cutover journal block with commit SHAs covering AC-H1 (i)–(iii) and AC-H2; AC-H1 (iv) recorded as held on the OQ-3 private-home decision (Liam + Orchestrator) — the journal states this dependency explicitly.
S335 execution errata (recorded {90.23} R5, 10/06/2026). The amendment text above was authored ahead of execution and predicted two things that the executors found could not happen as written. These are recorded as deltas — NOT a re-write of the amendment — with the authoritative record being the
{90.22}detailsjournal blocks (the four S335/S336/S337<info added on …>entries). Canonical commits: R1a2f221448b(+ fix-pass1d2412072), R1b5e1ba5714, R2988dd4406, R3156b746e0.
- ERRATUM #1 — Retained-suite re-targeting WHEN-claim (affects this note + §Testing line 216, R1a bullet line 126). The text states the umbrella/promote-scoped Retained suites’ re-targeting from the direct path to the transport “executes in R1a”. It did NOT. R1a’s
setup.tsun-pin routed theirrun()calls through the transport implicitly (ephemeral server per scratch--ledger-dir), butpromoteandupdateUmbrellahad ZERO server branch at R1a (promoteused a staged direct write;updateUmbrellawas a self-contained read-mutate-write with the umbrella-patch intent defined but never constructed — K4-deferred dead intent). Their write functions only got server wiring in R1b (escalation 3, Orchestrator option 2 — absorbed into R1b under invariant 49 + T-5, no third Planner loop). So the WHAT is unchanged (coverage lands on the transport; byte-shape upstream per U11); only the WHEN moved from R1a to R1b. Theledger-cli-promote-scoped.test.ts(10) +ledger-cli-umbrella.test.ts(18) suites were not edited in R1b — R1a’s un-pin already carried them onto the transport once the server branch existed.- ERRATUM #2 — R2 deletion list + R3 primitive arm (affects R2 bullet line 128, R3 bullet line 129). R2 was specified to delete all five
lib/ledger/{atomic-write,detect-schema,patch-apply,record-mutate,scoped-serialise}.ts. The R2-mandatedast-dataflow importerspre-deletion gate founddetect-schema,patch-apply,record-mutateare NOT drained: they are the CLI-side validation oracle (loadLedger→detectSchemaparses+validates before every server intent;fieldPatchMutation→applyPatchesis the field-mutation schema oracle;insertRecord/removeRecordare the create/delete/promote duplicate-id oracle) — the invariant-8schema-error/walk-error/duplicate-idenvelopes depend on them. Escalation 4 / Orchestrator option (A): R2 deleted onlylib/ledger/{atomic-write,scoped-serialise}.ts; the other three are RETAINED with header annotations stating the retained oracle role, disposition rides {68.30}. Consequently R3 kept the primitive-diff arm for the three retained modules (dropping only the deletedatomic-write.tsfrom the watched paths;scoped-serialise.tswas KH-authored and never on the watch list). A further consequence: the T-5show/getread re-route toGET /api/ledger/:slug/record/:idwas predicated ondetect-schemadeletion — withdetect-schemaretained,loadLedgerstill works façade-local, soshow/get/schemastay FAÇADE-LOCAL (no server round-trip). This is a documented deviation from the T-5 prose, a direct consequence of erratum #2.
Proposed-changes ↔ PRODUCT invariants 1–62 (one-to-one)
Section titled “Proposed-changes ↔ PRODUCT invariants 1–62 (one-to-one)”| Inv | Change | Concrete note |
|---|---|---|
| 1 | Migration plan §below | Process invariant: phase ordering + AC-gated exits; Orchestrator-enforced; no single code artefact. |
| 2 | U0–U12 | All Phase-0 changes land in the task-view repo; KH consumes nothing until K1. |
| 3 | K4 | serverEnabled() === '1'; flag-OFF path is the untouched existing commitMutation/promote/umbrella body. |
| 4 | K2+K4, proven by K5 | Same argv → same envelope/exit/bytes via transport; differential harness is the proof. |
| 5 | K4 | Branch at one seam; both paths emit the identical on-disk format (U1 ≡ scoped-serialise) — no migration/cleanup on flip. |
| 6 | K4 | Zero .claude/ edits; AC-P1 git diff check over .claude/skills+.claude/agents. |
| 7 | F1 + Migration plan | P2 entry gated on AC-P1; P3 entry gated on AC-P2 soak evidence. |
| 8 | R1a+R1b | R1a re-targets tests to transport (direct path dead); R1b removes the direct write machinery; façade keeps argv contract. Proven byte-identical through R1a by the still-live K5 OFF-vs-ON differential. |
| 9 | U2/U3/U4 hook placement | Gates execute inside the server handlers; any client (façade or direct HTTP) gets identical gate outcomes. |
| 10 | K4 (no change to dispatch) | run() dispatch (2332–3539) + SUBCOMMAND_ALIASES (895) untouched. |
| 11 | K4 (no change to flag tables) | VALUE_FLAGS/BOOLEAN_FLAGS (287–331) + unknown-flag rejection (375–379) untouched. |
| 12 | K2 | Mapping layer emits via the existing emit() (1120–1157); CliResult shape unchanged. |
| 13 | K2+K3 | Transport diagnostics + spawned-server stdio → stderr; stdout carries only the envelope. |
| 14 | K2 | Code-preserving pass-through map; one transport addition (mtime-mismatch); guard-port codes noted in §Decisions error-vocabulary note (Checker/Liam attention). |
| 15 | T-5 | No change in Phases 1–2 (reads façade-local in both flag states); P3 re-route (R1b) covered by the retained envelope tests already re-targeted to transport in R1a. |
| 16 | U10 + K4 | dryRun: true body field — full gate chain server-side, no write/regen/mtime change. |
| 17 | No change needed | --pretty formatting lives in emit(), façade-local in all phases. |
| 18 | U1 | escapeSerialise/scopedSerialise ported; serialiseLedger deleted. |
| 19 | U1 | PATCH = scoped fold over rawText; creates = splice; ~1417-line class structurally impossible on those paths. |
| 20 | U1 | Both paths emit OQ-LS-2-conforming bytes; no-op round-trip test upstream. |
| 21 | K5 | Differential harness, CI step in ledger-mirror-parity job + local script; OFF-vs-ON through R1a, transport-only from R1b. |
| 22 | U3 | Gate on serialised bytes pre-atomicWriteFile, every handler; record-set-violation, nothing written. |
| 23 | U3+U7 | Per-leg at stage time: +1/−1/∅. |
| 24 | U0+U2 | Registry relocated as plain data (packages/schemas/src/ledger-budgets.ts, never .max()); Intl.Segmenter grapheme counting ported. |
| 25 | U2 | PATCH → per-patched-field mutatedField mode; creates → create mode. |
| 26 | U10+K4 | force is a per-request body field; server holds no override state. |
| 27 | U2 | subtask.details absent from the relocated registry — exempt on every path. |
| 28 | U4 | Wired post-serialisation/pre-write in all four handlers + three transaction legs + umbrella path; no ungated byte route remains. |
| 29 | U4 (T-1) | Strict JSON parser, shape predicate per identity-guard.yml:89; comma parser never applied. |
| 30 | U4 | Per-token regex with per-token case_insensitive flag (identity-guard.yml:384–390 model). |
| 31 | U4 | Net-new delta over (rawText, serialised); equal-or-decreasing passes. |
| 32 | U4 | Counts + paths only; the 245c35ac [${names}] echo deliberately not ported; log/response redaction tested in U11. |
| 33 | U10+K4 | KH_LEDGER_ALLOW_CLIENT_NAME=1 → allowClientName: true per invocation → redacted warning + allow. |
| 34 | U4+K3 | Unset → inactive locally; --require-denylist (passed when CI is set) → loud config failure. |
| 35 | U4 | Set-but-invalid → client-name-guard-config, blocks every mutation in every context. |
| 36 | U11 | 7 + 3 tests relocated to packages/server/gates/client-name-guard.test.ts, synthetic JSON fixtures, green = AC-H2 evidence. |
| 37 | U5+K4 | Server-side nextId port, create defaults, bulk fold-left, per-record budgets (U2 in bulk mode). |
| 38 | U5+U9 | Allocation at apply time inside the per-document mutex; duplicate-id pre-check retained → second writer re-derives or fails loudly. |
| 39 | U6 | Apply-time leaf concatenation under the mutex; prior bytes preserved verbatim. |
| 40 | U7 | Validate-first three-leg staging; unknown-theme pre-stage; additive-first commit order; idempotent link push. |
| 41 | U0+U10 | Budget-aware vendored parse helpers + ported disciplineWarnings populate response warnings[]; façade passes through. |
| 42 | No change needed (extended) | mtime check already enforced (patch-server.ts:578–601 et al.); U5/U7/U9 apply the same check on every new endpoint/leg. |
| 43 | K2 | Per-intent re-derive on retry; append re-derives trivially (server-side op); creates re-derive ids server-side; deletes surface record-not-found. |
| 44 | K2 (T-2) | 3-retry bound; exhaustion → mtime-mismatch, exit 1, nothing written, idempotently re-runnable. |
| 45 | K2 | Retry-count warning appended to the success envelope warnings[]. |
| 46 | U9+K2 | Mutex serialises intra-daemon; retry absorbs inter-read conflicts; concurrency suite (U11) is the regression proof. |
| 47 | No change needed (mapped) | Substrate scoped regen retained (patch-server.ts:671/828; full regen on DELETE); regenMirrors: false (U10) → suppressed; regen failure → K2 maps to mirrorStale/regen-failed, never rollback. |
| 48 | K1 | One tag literal governs CI (ci.yml:1139), regen cache key (self-resolved), vendor-drift (lines 86/214), and the daemon the façade spawns (K3 version check). |
| 49 | U8+K4 | "umbrellas" registered as the fourth KNOWN_DOCUMENT_NAMES literal; update-umbrella → slug-routed gated PATCH; self-contained raw write (updateUmbrella, K4-deferred) retired at R1b. |
| 50 | U3+U4+U8 | Guard always; record-set ∅ over umbrella id-set; no budget entries fabricated. |
| 51 | K6 | Isolated, content-neutral (deep-equal-asserted) normalisation commit + serialiseUmbrellas → escapeSerialise flip in the same commit. |
| 52 | U1+U8 | Umbrellas walk in the ported scoped serialiser → minimal-diff membership edits. |
| 53 | U8 | mirror-generator returns the empty plan for kind 'umbrellas'; no mirrors created. |
| 54 | K3 | Transparent ensure-server: handle file + health + locked spawn + 10 s deadline; fail-loud; structurally no ungated fallback (the flag-ON path has no local write branch; R1b makes the server path unconditional). |
| 55 | No change needed (preserved) | resolveServerHostname throws on non-loopback (patch-server.ts:1308); U9 changes routing, not binding. |
| 56 | U9+U7 | Single multi-document daemon; transaction spans siblings (+ roadmap leg) within one process. |
| 57 | U2/U3/U10 + R5 | Behaviour preserved by the gate ports; §3 prose update at Phase 3 only. |
| 58 | R1b+R2 | Vendored primitives + scoped-serialise deleted from KH (R2, after R1b removes the last importers); gates exist only upstream; façade carries transport, not gate logic. |
| 59 | U0+U2+R2+R3 | Plain-data registry wherever it lives; annotations revised; budgets path added to the drift watch list — deliberate, not silent. |
| 60 | R3 | Primitive arm retires at Phase 3 (paths removed with the files — never watching a non-existent path); schema arm + file deletion ride {68.30}. |
| 61 | No change needed (deliberate absence) + R6 | No ledger-client-name-guard.yml lands in KH CI; R6 journal records the substrate guard as the enforcement point; any private-home lint twin rides OQ-3. |
| 62 | T-4 (no new mechanism) | Schemas stay canonical in KH; U0 refreshes the vendored copies; consumer migration executes at {68.30} per the PRODUCT §(b) enumeration. |
Migration plan (phase boundaries sized for {90.4} decomposition)
Section titled “Migration plan (phase boundaries sized for {90.4} decomposition)”| Phase | Where | Changes | Test strategy | Rollback | Exit gate |
|---|---|---|---|---|---|
| P0 | task-view repo (parallel-cmux sub-track; zero KH impact) | U0 → U1 → {U2, U3, U4} → {U5, U6, U7, U8} → U9 → U10 → U11 → U12. Internal order: serialisation before gates (gates assert on conforming bytes); routing/mutex (U9) before the concurrency suite. | Upstream bun test per slice; U11 suites green; no-op round-trip byte test on copies of the live KH ledgers; fault-injection extension for U7. | Nothing to roll back KH-side; upstream slices revert independently pre-tag. | AC-P0 — tag exists; gates + serialisation + parity affordances in; guard/gate test ports green; any P1-deferred gap named in the exit record. |
| P1 | KH (flag OFF) | K1 (pins) ∥ K6 (umbrellas normalisation) ∥ {K2 → K3 → K4} → K5. K6 lands before any flag-ON umbrella testing. | Full existing suite green flag-OFF (unchanged expectations); K5 harness green flag-ON against scratch dirs; ledger-mirror-parity green at the new tag; new façade unit tests (K2 mapping, K3 lifecycle with mocked spawn seam). | Flag defaults OFF (inert); revert K1 commit restores the v0.3.1 ecosystem; K6 reverts as one isolated commit. | AC-P1 — incl. zero .claude/ consumer edits and the OQ-9 README fix. |
| P2 | KH (flag ON default) | F1 only (one-line default + docs note). | Soak ≥1 full SDLC session; diff-shape spot checks; conflict-retry evidence (live or synthetic); rollback drill (KH_LEDGER_SERVER=0 → verify → flip back) recorded. | Instant: KH_LEDGER_SERVER=0; or revert F1. | AC-P2. |
| P3 | KH | Ordered commits R1a → R1b → R2 → R3 ({90.22}) → R4 → R5 → R6 ({90.23}) — re-derived S335 (test-migration-first; see §Phase 3 amendment note). | Full bun run test after each commit; R1a un-pins setup.ts + re-targets the in-process suites to transport (direct path becomes dead code, suite green); R1b converts the K5 harness to transport-only mode (OFF arm gone); retained façade tests green against ephemeral servers throughout; identity-hygiene grep (AC-I) over all ID-90 artefacts. | git revert of the chain (ledgers still in-repo pre-{68.30}, so the direct path restores wholesale; reverting R1b then R1a restores both paths + the K5 differential); soak evidence precedes, narrowing the need. | AC-P3 + AC-H1(i–iii) + AC-H2 recorded; AC-H1(iv) explicitly held on OQ-3. |
End-to-end flow (flag ON, update-subtask 90.3 status done shape): façade parses argv (unchanged) → builds field-patch ServerIntent → ensureServer(docs/reference) (K3: handle file → health → reuse) → GET /api/ledger/task-list for baseMtime → PATCH /api/ledger/task-list/record/90 {patches: [{fieldPath: ['tasks','90','subtasks',3,'status'], newValue: 'done'}], baseMtime, force: false} → server: mutex acquire → mtime check → applyPatches (Zod oracle) → budget gate (U2) → scoped fold over rawText (U1) → record-set gate (U3) → client-name guard (U4) → atomicWriteFile → scoped mirror regen → {ok, newMtime, warnings} → façade maps to the standard envelope, exit 0.
Testing and validation
Section titled “Testing and validation”Differential parity (invariants 4, 21; AC-P1). K5 as specified: per-subcommand matrix, byte-level cmp on files and mirrors, envelope deep-equal, exit-code equality. Runs in the ledger-mirror-parity CI job (reusing its tag-keyed clone, now with explicit .cache/task-view-<tag> provisioning per pre-condition commit ba8976d42) and locally. The OFF-vs-ON differential requires both code paths to exist, so it stands through R1a as the invariant-8 proof; R1b converts it to transport-only mode (ON arm only, asserting success + OQ-LS-2-conforming bytes), retained as a regression harness until {68.30} re-homes the gate.
Existing 18 CLI test files — Phase 3 disposition (AC-P3). S335 amendment: the re-targeting of the “Retained” suites from the in-process direct path to the transport path executes in R1a (the green-per-commit precondition — the {90.21} setup.ts pin made them direct-path-bound), not in R4. The disposition below describes WHERE coverage lands; R1a/R4 split is WHEN. R4 records the residual accounting only (see §Phase 3 R4).
| Files | Disposition |
|---|---|
ledger-cli.test.ts, -input, -field-editors, -discoverability, -stdout-purity, -promote-input, -friction-s299, -autoid-coercion, -umbrella (9 files total) | Retained as façade tests (argv, envelope, exit codes, stdout purity, input coercion) re-targeted at the transport path against a real ephemeral server. |
ledger-cli-budget.test.ts, -record-set.test.ts | Coverage moves upstream (U11); KH keeps the K5 parity assertions. |
ledger-cli-scoped-create.test.ts, -promote-scoped, -bulk-add, -delete-subtask | Byte-shape coverage moves upstream with the serialiser; thin façade intent-mapping tests retained. |
ledger-cli-mirror.test.ts | Regen semantics upstream; façade keeps the mirrorStale/suppressed/regen-failed mapping tests. |
ledger-cli-capability-bundle.test.ts | Third-leg coverage upstream (U7 suite); façade intent test retained. |
ledger-cli-integration.test.ts | Re-pointed at the transport (full subcommand round-trip). |
ledger-renormalise.test.ts (+ __tests__/lib/ledger/scoped-serialise.test.ts) | Retired with their modules (R2); scoped-serialise suite lives upstream (U11). |
Guard tests (AC-H2): relocated per U11 with synthetic JSON fixtures; green upstream is the {68.27} release evidence; the id68-branch files are thereby superseded, never merged to canonical (Inv 42).
New suites: upstream — gates, serialisation round-trip, subtask CRUD, append interleave, concurrency/mutex, umbrellas, third-leg fault injection, health/lifecycle flags. KH — K2 envelope-mapping unit tests (incl. the mirror-regen-failed → success-with-stale mapping and retry-exhaustion path), K3 lifecycle tests, retained façade suites.
Mock discipline (docs/reference/test-philosophy.md): transport tests run against real ephemeral patch-server instances (port 0, loopback) — the transport contract is the behaviour under test, so fetch is never mocked. Mocking is confined to the K3 process-spawn seam (lifecycle unit tests) and fault-injection seams the substrate already exposes (faultBeforeCommit). Guard fixtures are synthetic only; redaction tests assert the absence of fixture tokens in observable output. Date-sensitive retry/backoff tests pin time per the CLAUDE.md gotcha.
Risks and mitigations
Section titled “Risks and mitigations”- mtime millisecond granularity: two writes inside the same millisecond defeat the strictly-greater check. The U9 mutex closes this entirely intra-daemon; the residual exists only when flag-OFF and flag-ON writers mix during Phases 1–2 — equal to today’s CLI-vs-CLI hazard, never worse; the phased flip minimises the mixing window.
- Vendored-schema drift (found live): the upstream parse-helper drift (§Context) shows schema-direction drift goes unnoticed under the non-blocking reminder. U0 re-vendors at the cut; the vendor-drift diff at K1 verifies; schema single-sourcing at {68.30} removes the class.
- Umbrella denormalisation hazard: a flag-OFF
update-umbrellaafter K6 would have re-emitted raw UTF-8; closed by flippingserialiseUmbrellasin the same K6 commit. - Invariant-14 vocabulary clarification (guard codes) — recorded in §Decisions; Liam confirms alongside OQ-7 at TECH review.
- Bun/tag skew: the façade always spawns from the tag-keyed cache and verifies
/api/healthversion against the ci.yml-parsed tag (K3); idle-exit ages out survivors. - Worktree daemons: handle files live in each checkout’s gitignored
.cache/— worktrees get isolated daemons over their owndocs/reference; ledger writes remain Orchestrator-owned in the main checkout, so contention stays where the mutex is. - R1a ephemeral-server spawn cost (S335). Un-pinning
setup.tsroutes ~248 in-processrun()mutation calls across ~16 suites throughensureServer, which spawns a real loopback patch-server per non-default--ledger-dir. Mitigation: each suite allocates one ephemeral server over itsbeforeEachscratch dir and reuses it across the suite’srun()calls (server lifecycle keyed on--ledger-dir, idle-exit reaps it) — spawn cost is per-suite, not per-call. Vitest runs suites in parallel workers, so concurrent spawns bind to distinct loopback ports (port 0). The CIledger-mirror-parityjob’s explicit.cache/task-view-<tag>provisioning (commitba8976d42) guarantees the server entry resolves regardless of arm order. Residual risk: a slower full-suite wall-clock under R1a; bounded and one-time (R1b removes nothing that re-adds spawns). If suite-time regresses materially, the fallback is a shared per-worker daemon over a common scratch root — but the per-suite-dir isolation that the existing suites rely on argues against it; flagged for the R1a executor to measure, not pre-optimise. - Identity hygiene: all ID-90 artefacts (specs, commits, fixtures, journal) are AC-I-checked against the configured client denylist; this spec refers to the denylist generically throughout; upstream test fixtures are synthetic.
Verification (Q-EX2 / OQ-3 empirical-check discipline)
Section titled “Verification (Q-EX2 / OQ-3 empirical-check discipline)”- Date: 07/06/2026. Scope: no new third-party external-library APIs are cited — the design uses Bun built-ins (
Bun.serve,Bun.file), Node built-ins (node:fs,node:utilparseArgs),Intl.Segmenter(stdlib), andzodonly via existing, already-vendored first-party code (no new Zod API shapes). - Substrate (first-party): every task-view symbol cited was read from the pinned clone
.cache/task-view-v0.3.1-task-view@ tagv0.3.1-task-view=1644742762df3f202ab2a7bd3507aab5758fdd30— result PRESENT for all cited symbols (startPatchServer,readCanonical,serialiseLedger,applyPatches,insertRecord/removeRecord,promoteTransaction,detectSchema/KNOWN_DOCUMENT_NAMES,startTaskViewServer,resolveServerHostname,parseCliArgs,runRegenCheck,slugForDocumentName). - KH side: all
scripts/ledger-cli.ts,lib/ledger/*,lib/validation/*, workflow and test-file citations read from the working tree atcanonical-pipeline-setupHEAD876c68ac— PRESENT. - One BEHAVIOUR_DRIFT-class finding recorded: upstream
packages/schemas/src/task-list-schema.ts:224–243parseTaskListWithWarningslacks the budget soft-warnings present in KH canonical (lib/validation/task-list-schema.ts:28, 212) — handled in-spec by U0 (not an escalation: the drift is repaired by this Task’s own Phase 0).
S335 re-verification (Phase 3 amendment — 10/06/2026). No new third-party external-library APIs are cited by the amendment (the re-derivation re-sequences internal commits only; Q-EX2 external-check scope does not apply). All ground-truth KH symbols underpinning the amendment re-verified PRESENT against the working tree at canonical-pipeline-setup HEAD a216d5b6d and confirmed via gitnexus_context/impact (repo knowledge-hub):
__tests__/setup.ts:55–70—KH_LEDGER_SERVER='0'force-pin PRESENT (added by {90.21} flip9160d0e3b).scripts/ledger-cli.ts:4639–4653export block —assertRecordSet,__setRegenRunnerForTest,mirrorReminderFor,subcommandHelpall PRESENT.- Export-block importers verified by grep:
ledger-cli-mirror.test.ts+ledger-cli-stdout-purity.test.tsimport__setRegenRunnerForTest;ledger-cli-mirror.test.ts+ledger-cli-friction-s299.test.tsimportmirrorReminderFor;ledger-cli-record-set.test.tsimportsassertRecordSet. - Suite-count precision (S335 fix 2): 17
ledger-cli-*.test.tssuites importrunfrom@/scripts/ledger-cli, none setting aKH_LEDGER_SERVERoverride; exactly 16 exercise the direct WRITE path (verified by subcommand: each calls at least one ofopen-task/add-subtask(s)/update-*/flip-*/delete-*/promote/create-*). The 17th,ledger-cli-discoverability.test.ts, is READ-ONLY — itsrun()calls are onlyschema/get/show/show-task+ an unknown-subcommand (frobnicate) case — so it stays façade-local and is NOT re-targeted. 248run()mutation call sites counted across the 16 write suites. - Surviving
lib/ledgerimporters (S335 fix 1 — verified bygrep -rn 'from .*lib/ledger' __tests__ scripts): three importers survive R1b and would breaktscat the R2 boundary —ledger-cli-umbrella.test.ts:35(escapeSerialisefrom@/lib/ledger/scoped-serialise, Retained suite),ledger-cli-input.test.ts:18(detectSchemafrom@/lib/ledger/detect-schema, Retained suite — importsparseArgs/readRecordInput/nextId, NOTrun), andscripts/ledger-sweep-s269.ts:32(escapeSerialise, historical one-off, untested, in no R-chain or CI lane). The other importers (scripts/ledger-cli.ts,ledger-renormalise.ts,ledger-normalise-oqls2.ts,ledger-renormalise.test.ts,__tests__/lib/ledger/scoped-serialise.test.ts) are all removed/retired by R1b/R2 as already specified. Fix: R1a drops the two test imports; R2 retiresledger-sweep-s269.ts. serverEnabled(scripts/ledger-cli.ts:139–141)gitnexus_impactupstream = LOW (1 direct callercommitMutation→run→main; 0 processes affected);gitnexus_contextconfirmscommitMutationas the sole caller. Transport stackserverCommitMutation(:2225) →ledger-server-client.ts(mapSuccess,TransportRequest) →ledger-server-lifecycle.ts(ensureServer,spawnAndWait) all PRESENT. Clock seam:ledgerNow()(:2314–2315) readsKH_LEDGER_NOW; it is consumed bywithCreateDefaults(:2447) for create-timeupdatedAt(S335 fix 3 — the seam pins both flag paths; the earlierserverCommitMutationattribution of the:2456comment was wrong — that comment lives inwithCreateDefaults).- K5 harness
scripts/ledger-differential-parity.tsmain()/runCli()PRESENT; the OFF arm pinsKH_LEDGER_SERVER='0'and the ON arm'1'(lines 266–273), confirming the structural OFF-vs-ON dependency on both paths existing. - Pre-condition commit 0 (
ba8976d42,ci.ymllines 1163–1180.cache/task-view-<tag>symlink provisioning) PRESENT incanonical-pipeline-setup.
Follow-ups
Section titled “Follow-ups”- OQ-3 (post-cutover private home + parity-gate re-home) — Liam + Orchestrator; blocks AC-H1(iv) only; recorded as the explicit hold in R6.
- OQ-8 (ID-68 records’ “ID-20” referent re-point to ID-90) — Orchestrator, before {68.30} dispatch.
- OQ-7 ratification + the invariant-14 vocabulary clarification — Liam, at this TECH review.
- Any future de-façade (skills speaking HTTP directly) is out of scope per PRODUCT §Non-goals; revisit only after the substrate has soaked as sole writer.