Skip to content

RESEARCH — Server-ledger cutover: task-view patch-server replaces ledger-cli (gates + guard port)

RESEARCH — Server-ledger cutover: task-view patch-server replaces ledger-cli (gates + guard port)

Section titled “RESEARCH — 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.1} RESEARCH
  • Session: S322 (authored 07/06/2026)
  • Provenance: bl-244 (S315, S321) promoted to ID-90 at S322; Liam directive S321, carried from S315 “ID-20 ledger-substrate sub-track kickoff”. The closed ID-20 record delivered the mirror/render surface only — NOT this cutover; no server-side ledger mutation home existed before this Task.
  • Ownership lineage: ID-68 PRODUCT.md Invariant 42 boundary table (line 387–397) assigns “Ledger patch-server substrate (design, write-time gates port, cutover)” to ID-20 — separate sub-track, own spec chain, parallel cmux; ID-68 consumes its cutover as Gate 2. ID-90 now carries that ownership (ID-20 closed as render-surface only); the Orchestrator should treat ID-68’s “ID-20” gate referents as re-pointed to ID-90.
  • Identity hygiene: this document lands on a public-track repo. Client identity tokens and the ICO registration reference are FORBIDDEN strings here; the denylist is referred to generically as “the configured client denylist” throughout.

Run against the knowledge-hub GitNexus index (47,894 symbols / 300 execution flows):

  • gitnexus_query({query: 'ledger task-list mutation', repo: 'knowledge-hub'}) returned "processes": [] — zero execution flows. Only standalone definitions matched: Function:scripts/ledger-cli.ts:run (lines 2332–3539), Function:scripts/ledger-cli.ts:nextId (644–674), Function:lib/ledger/record-mutate.ts:insertRecord (114–156), Function:lib/ledger/record-mutate.ts:removeRecord (162–192), Function:lib/ledger/scoped-serialise.ts:walkTaskList (112–153), Function:lib/ledger/scoped-serialise.ts:resolveSpliceCollection (306–341), plus unrelated Python pipeline symbols.
  • gitnexus_query({query: 'ledger-cli gates budget record-set write'}) returned empty ("processes": [], "definitions": []) with the warning "FTS indexes missing — keyword search degraded".
  • Interpretation: the ledger-mutation surface participates in no indexed execution flows — it is a self-contained CLI entry point outside the app’s call graph. This is an isolated-surface finding, not a greenfield disclaimer: the symbols exist and are indexed, but nothing in the product runtime calls them.
  • gitnexus_context({name: 'insertRecord'}) — incoming calls: run and promote, both in scripts/ledger-cli.ts; outgoing: extractId, existingIds, reparse, rebuildDetected (all lib/ledger/record-mutate.ts); "processes": [] (no flow participation). 2 direct callers.
  • gitnexus_context({name: 'applyPatches'}) (lib/ledger/patch-apply.ts:357–373) — incoming: fieldPatchMutation (scripts/ledger-cli.ts); outgoing: applyTaskListPatches / applyRoadmapPatches / applyBacklogPatches; "processes": []. 1 direct caller.
  • ast-dataflow importers --module 'scripts/ledger-cli'18 importers, all test files (__tests__/scripts/ledger-cli-*.test.ts plus ledger-cli.test.ts), importing run, ParsedArgs, parseArgs, readRecordInput, nextId, renderSchema, subcommandHelp, mirrorReminderFor, __setRegenRunnerForTest, assertRecordSet. Zero production TypeScript importers — every production consumer invokes the CLI as a subprocess (shell), not as a module.

scripts/ledger-cli.ts (4,367 lines) is the sole sanctioned mutation path for the three KH workflow ledgers (docs/reference/{task-list,product-roadmap,product-backlog}.json), built (ID-35) as “a thin dispatcher over the task-view v0.2.0 patch primitives vendored into lib/ledger/” (header, lines 6–8). The task-view fork has since grown a full per-record patch server (packages/server/patch-server.ts, tag v0.3.1-task-view), which already implements atomic writes, optimistic concurrency, record CRUD, an atomic cross-ledger promote transaction, and in-process scoped mirror regeneration. ID-90 cuts ledger mutation over to that substrate, ports the KH-side write gates into it, ports the id68-branch client-name guard into it (Inv 42), and thereby releases two ID-68 holds ({68.30} Gate-2 ledger-subset release; {68.27} ledger-guard holdback).

Empirical verification note (OQ-3 / Q-EX2 discipline)

Section titled “Empirical verification note (OQ-3 / Q-EX2 discipline)”

No third-party external-library APIs are cited in this document beyond Bun/Node built-ins. The substrate surface (task-view) is first-party and was verified empirically against the pinned clone at .cache/task-view-v0.3.1-task-view, tag v0.3.1-task-view = commit 1644742762df3f202ab2a7bd3507aab5758fdd30 (grafted shallow clone; git describe --tags confirms), the same tag pinned by .github/workflows/ci.yml:988 (TASK_VIEW_TAG: v0.3.1-task-view). All file:line citations below were read from that clone on 07/06/2026.


§1 WHAT EXISTS — scripts/ledger-cli.ts mutation surface

Section titled “§1 WHAT EXISTS — scripts/ledger-cli.ts mutation surface”

1.1 Subcommand inventory (19 dispatch arms + 1 alias)

Section titled “1.1 Subcommand inventory (19 dispatch arms + 1 alias)”

Dispatch switch inside run() (scripts/ledger-cli.ts:2332–3539); USAGE block at lines 1064–1118. Alias: show-taskshow (SUBCOMMAND_ALIASES, line 895).

SubcommandArity / formDispatch site (line)Write pathGates applied
show<ledger> <id>2347read-onlynone
get<ledger> <id> [field]2375read-onlynone
schema[ledger|recordKind]2412read-only (derived from Schema.shape, {35.22})none
flip-task<taskId> <status>2425scoped field-patchrecord-set (∅ delta), budget (mutatedField), discipline warnings, de-ID†
update-task<taskId> <field> <value>2467scoped field-patchsame
flip-subtask<taskId.subId> <status> (legacy <taskId> <subId> <status>)2551scoped field-patchsame
update-subtask<taskId.subId> <field> <value>2613scoped field-patchsame
append-journal<taskId.subId> <text>2700scoped field-patch (append to details)record-set (∅), de-ID†; details is budget-exempt
add-subtask<taskId> <json | --title …>2773scoped record splicerecord-set (+1), budget (create mode), auto-id
add-subtasks<taskId> --file <json|-> (bulk)2898scoped splice ops folded left ({65.6})record-set (add-many), per-record budget
update-backlog<itemId> <field> <value> (--append for notes)3047scoped field-patchrecord-set (∅), budget
update-roadmap<themeId> <field> <value> (--append for notes)3146scoped field-patchrecord-set (∅), budget
open-task<json | --title … [--effort-estimate …]>3235scoped record splicerecord-set (+1), budget, auto-id, withCreateDefaults
create-backlog<json | --title … --type … --track …>3235 (shared arm)scoped record splicesame
create-theme<json | --title …>3235 (shared arm)scoped record splicesame
delete-backlog<itemId>3328whole-file only (no scoped delete path, {65.2}/{65.3})record-set (−1)
delete-subtask<taskId.subId>3380whole-file onlyrecord-set (−1)
promote<backlogId> <taskJson | --file | --title …> [--capability-theme <id>]3470 → promote() 3551–3955scoped splice + staged two/three-ledger commitrecord-set ×2(+roadmap ∅), budget (task), de-ID† ×3
update-umbrella<umbrellaId> --add-tasks|--remove-tasks|--reorder <csv>3510 → 3957+self-contained raw-UTF-8 write to umbrellas.json (NOT via commitMutation; lines 3957–3984)Zod (UmbrellasSchema), de-ID†; budget N/A; mirror regen N/A

† de-ID gate exists only on the id68 branch lineage — see §3. On canonical, the write paths carry the other gates only.

Flags (VALUE_FLAGS 287–313, BOOLEAN_FLAGS 316–331; unknown flags REJECTED, lines 375–379): --dry-run --pretty --whole-file --scoped --force --append --no-regen-mirrors --regen-mirrors --ledger-dir <path> --file --id --title --description --status --depends --priority --notes --test-strategy --status-note --type --track --effort-estimate --capability-theme --add-tasks --remove-tasks --reorder.

Output contract: single-line JSON envelope — success {ok:true, subcommand, result, warnings?, mirrorStale?, mirrorStaleReason?} to stdout exit 0; error {ok:false, subcommand, error, detail?, issues?} to stderr exit 1 (CliResult 150–165, emit() 1120–1157). stdout purity is contractual ({35.44}): regen child output is routed to fd2 (lines 1741–1752) so ledger-cli … | jq works. Error vocabulary: schema-error, walk-error, duplicate-id, record-not-found, budget-exceeded, record-set-violation, unknown-theme, … (USAGE 1114–1118).

All gates run before any byte is written; both rejection gates exit 1 with nothing written.

  1. Record-set-preservation gate — “the delta gate” ({35.16}; lines 1315–1486). RecordSetDelta = none | add | add-many | remove (line 1333). checkRecordSet (1442) derives the post-write id-set from the bytes about to be written (re-parsing the serialiser output, line 1357 collectionIds) and asserts it equals the pre-write set under the intended delta (assertRecordSet 1413) — so a serialise-side drop/duplicate is caught one step before it lands. Applied per write in commitMutation (2044–2054) and twice (or three times) in promote (task-list +1 at 3819, backlog −1 at 3828, roadmap ∅ at 3839–3848).

  2. Write-time budget gate ({35.17}; lines 1488–1649). checkBudget (1613) measures the CHANGED record’s budgeted fields against LEDGER_BUDGETS using grapheme counts (Intl.Segmenter, 1589–1594, {35.31}). Two modes ({35.26}): mutatedField set (update-*) → only that field can reject, other over-budget fields surface as soft warnings; mutatedField undefined (create/add/promote) → first over-budget field is fatal. --force downgrades the rejection to a (forced) budget-exceeded: warning (1957–1963 in commitMutation; 3862–3871 in promote). Budgets are plain data, never Zod .max() (1498–1501) so the live over-budget ledger keeps parsing. subtask.details is intentionally absent from the registry → exempt (the append-only journal home).

  3. Promote gate — atomic cross-ledger transaction (promote() 3551–3955). Preserves the documented task-view/packages/server/ledger-transaction.ts algorithm (header comment 3543–3550): validate-everything-first → stage-both (durable temps via stageAtomicWrite) → commit-last, ADD side first then sync renames (3928–3936), so a kill between renames yields a benign transient duplicate, never a lost update. --capability-theme ({35.39}A) adds an optional third (roadmap) leg: unknown theme rejects with unknown-theme before any bytes are touched (3649–3656); the theme’s linked_tasks[] push is idempotent (3722–3740).

  4. De-ID gate (client-name guard) — id68-branch only. guardClientName at all three write paths (commitMutation, promote staged writes, umbrella write). Full treatment in §3; it is NOT on canonical (verified: grep guardClientName scripts/ledger-cli.ts on canonical-pipeline-setup returns nothing; git log --all -S guardClientName finds 245c35ac (CI twin), 52ef3d5b, ceff8d8f, plus self-referential doc commits).

Scoped writes are the GLOBAL DEFAULT since ID-65.5 (“ratified default #4”): every mutating command derives scoped: !flags.wholeFile; --scoped is a no-op back-compat alias; --whole-file is the explicit opt-out (lines 180–195, 1085–1094). The primitive is KH-authored lib/ledger/scoped-serialise.ts (NOT vendored from task-view — lib/ledger/README.md “KH-authored (NOT vendored)” section): scopedSerialise (field edit), scopedSpliceSerialise (record insert/remove), escapeSerialise (whole-file conforming emit). Rationale per that README: the naive JSON.stringify(detected.data, null, 2) re-emit is non-conforming on two axes — Zod-reparse key reorder, and raw UTF-8 vs the on-disk \uXXXX escaping — which “turns a single-field edit into a ~1600-line diff (verified: ~1417 lines on the live task-list.json)” and collides with sibling cmux terminals editing the same shared file. After the OQ-LS-2 (S270) normalisation, scoped and whole-file paths are byte-compatible (ledger-cli 69–77; serialise() 1251–1263). On-disk format verified directly: task-list.json carries escapes and ends …]\n}\n (hexdump, 07/06/2026).

Default-ON after every write ({35.18}): maybeRegenMirrors (1778–1791) shells bash scripts/regen-mirrors.sh synchronously (seam regenRunner, 1741–1752; child stdout → fd2). --no-regen-mirrors suppresses (batch mode); failure is a post-write alert, not a rollback ({35.32} discriminated reminders, 1159–1189). scripts/regen-mirrors.sh clones task-view at the ci.yml-parsed TASK_VIEW_TAG into the tag-keyed cache .cache/task-view-<TAG> (the very clone this research reads), bun installs once, then runs node bin/task-view.js --check against each of the three ledgers — --check writes the mirror set in place (runRegenCheck, apps/server/index.ts:224–261) and drift is caught by git status/CI git diff --exit-code. CI gate: ledger-mirror-parity job, ci.yml:936–1006.

FileLinesKey exportsNotes
lib/validation/task-list-schema.ts287SubtaskSchema (L64), TaskSchema (L98), TaskListSchema (L168), parseTaskListWithWarnings (L230), TaskListWarning (L191)task.id STRING / subtask.id NUMBER (Taskmaster mandate — ledger-cli 679–686); warnings are soft (budget + 25-subtask ceiling)
lib/validation/roadmap-schema.ts276RoadmapThemeSchemathemes[] model (post-Phase-B)
lib/validation/backlog-schema.ts271BacklogItemSchemaitems[]
lib/validation/work-status.tsWorkStatus master enum (done, pending, in_progress, blocked, deferred, cancelled, spec_needed, imp_deferred, needs_research, …); per-surface subsets via .exclude()
lib/validation/umbrellas-schema.tsUmbrellasSchema (update-umbrella only; NOT vendored into task-view)

These four (excl. umbrellas) are vendored INTO task-view at packages/schemas/src/ — see §2.5.

lib/validation/ledger-budgets.tsLEDGER_BUDGETS (single source of truth, plain data): task.description 1500, task.status_note 300, subtask.description 250, subtask.testStrategy 300, theme.description 1500, theme.notes 300, item.title 80, item.description 500; subtask.details intentionally absent (unbudgeted). Canonical prose contract: docs/reference/task-list-discipline.md §2 (per-field table) + §3 (limit classes: soft-warn at parse via parseTaskListWithWarnings, hard-reject at CLI write time per {35.17} unless --force; record-set gate {35.16} noted alongside). The module header explicitly forbids moving budgets into Zod .max() because that would also “diverge the vendored lib/validation/*-schema.ts from task-view’s source (watched by task-view-vendor-drift.yml)” — i.e. budgets must stay out of the vendored schema layer in any port.


§2 SUBSTRATE — task-view patch-server v0.3.x surface

Section titled “§2 SUBSTRATE — task-view patch-server v0.3.x surface”

Cached clone: .cache/task-view-v0.3.1-task-view, tag v0.3.1-task-view = 1644742762df3f202ab2a7bd3507aab5758fdd30 (upstream github.com/liam-jons/task-view, permanent fork of Plannotator v0.19.18). Server code: packages/server/ (~4,000 lines core); entry: apps/server/index.ts via bin/task-view.js (Node shim spawning Bun). Also ships as a Claude Code plugin (.claude-plugin/plugin.json, /task-view slash command).

2.1 HTTP API (dispatcher buildFetchHandler, packages/server/patch-server.ts:1229–1291)

Section titled “2.1 HTTP API (dispatcher buildFetchHandler, packages/server/patch-server.ts:1229–1291)”
EndpointHandler (line)BodySuccessFailure modes
GET /handleGetRoot (208+)SSR viewer HTML
GET /client.js(1241)progressive-enhancement bundle
GET /api/ledgerhandleGetLedger (433){ok, kind, data, mirrorDir, mirrorDirName, mtime}500 read; 422 unknown-document-name
GET /api/ledger/record/:idhandleGetRecord (468){ok, kind, record, mirrorFilename, mtime}404 record-not-found
PATCH /api/ledger/record/:idhandlePatchRecord (526){patches: FieldPatch[], baseMtime}{ok, newMtime, recordId, mirrorsWritten, …}409 mtime-mismatch (589–601); 400 walk-error/invalid body; 422 schema-error; 500 write-failed / mirror-regen-failed
POST /api/ledger/recordhandlePostRecord (719){baseMtime, record}201 {ok, newMtime, recordId, …}409 mtime-mismatch / duplicate-id; 422 schema-error
DELETE /api/ledger/record/:idhandleDeleteRecord (874){baseMtime}{ok, newMtime, recordId, mirrorsDeleted}404; 409
POST /api/ledger/transactionhandlePostTransaction (1051){op:"promote", sourceBacklogId, taskRecord, taskListBaseMtime, backlogBaseMtime}only "promote" supported (1070–1079){ok, newTaskId, removedBacklogId, taskListMtime, backlogMtime, …}409/404/422/400/500 incl. no-sibling-ledger
POST /api/ledger/regenhandlePostRegen (1157){baseMtime?}{ok, mirrorDir, mirrorsWritten, mirrorsDeleted, mtime}409 if stale baseMtime supplied; 500 regen-failed

Server factory: startPatchServer (1307–1343) — one server per ledger file (PatchServerOptions.ledgerPath, 108–115), Bun.serve, loopback-only bind enforced (resolveServerHostname throws on non-loopback; PRODUCT inv 44 / TECH §5.8; remote.ts confirms TASK_VIEW_REMOTE has no bind effect). Lifecycle wrapper startTaskViewServer (ledger.ts) adds port retry (MAX_PORT_RETRIES = 5) and waitForExit(); the idle/browser-close shutdown was removed — the server runs until explicitly stopped. CLI: task-view [<ledger.json|mirror.md>] [--no-browser] [--port N] [--check] (apps/server/index.ts:57–99), with CWD ledger scanning, fail-on-load validation, and ?record= deep-linking.

  • Optimistic concurrency: every mutating request carries baseMtime (ISO 8601 of the client’s last-seen file mtime); the server stats the canonical before applying and returns 409 mtime-mismatch when on-disk mtime is strictly newer (patch-server 578–601). No file locking (§5.7) — the second writer loses. This is a capability the CLI lacks: ledger-cli is unguarded read-modify-write per invocation (its safety today comes from minimal-diff serialisation + git, not from concurrency control).
  • Validation oracle: same vendored Zod layer — applyPatches walks all patches then does ONE whole-document Zod re-parse; insertRecord/removeRecord run duplicate-id pre-checks (extractId, record-mutate.ts:68–74) + whole-document re-parse. These are literally the same primitives KH vendors at lib/ledger/ (byte-faithful per lib/ledger/README.md; FieldPatch {fieldPath: string[], newValue: unknown} identical on both sides — verified by side-by-side read).
  • Field reach: task-list patch paths are ['tasks', taskId, field] and ['tasks', taskId, 'subtasks', subId, field] (patch-apply.ts:14, walk at 76–170). Known-field allowlists derive from Schema.shape (line 44) — subtasks IS a known Task field, so a whole-subtasks[]-array replacement via PATCH is expressible (the only substrate route today for subtask add/remove).
  • Atomic writes: atomicWriteFile (write-temp + rename, pid+timestamp+random temp names); two-phase stageAtomicWrite/commitStagedWrite for the transaction.
  • Promote: promoteTransaction (ledger-transaction.ts) — validate-everything → stage-both (fsync) → commit-last, ADD side first; residual two-rename window documented honestly (header, lines 33–50). Sibling ledger resolution is directory-scoped (resolveTransactionSiblings via scanForLedgers). Note: ledger-cli’s promote is independent CLI glue over the vendored primitives that cites ledger-transaction.ts as the algorithm of record (ledger-cli 3543–3550) — the substrate version is the original.
  • Mirror regen: in-process and scoped — PATCH and CREATE regenerate only the touched record’s mirror (generateRecordMirror, patch-server 671, 828); DELETE runs full regen for orphan deletion. Superior to the CLI’s shell-out-to-full-regen model.

The patch-server has no middleware/plugin seam. Gates port as code into the four mutating handlers (handlePatchRecord, handlePostRecord, handleDeleteRecord, handlePostTransaction), all of which share the shape: parse body → readCanonical → mtime check → primitive mutation → serialise → atomicWriteFile → scoped mirror regen. The natural insertion points are (a) post-mutation/pre-serialisation for the budget gate (it inspects the changed record object), (b) post-serialisation/pre-atomicWriteFile for the record-set gate and the client-name guard (both operate on the bytes about to be written vs the prior on-disk bytes — exactly rawText from readCanonical vs serialised). Configuration (denylist env, force/override semantics) must arrive via server options or env since there is no per-request flag surface equivalent to --force yet.

2.4 CLI subcommand → substrate capability map

Section titled “2.4 CLI subcommand → substrate capability map”
ledger-cli subcommandSubstrate equivalentFidelity
show / getGET /api/ledger/record/:id (whole record; field projection client-side)partial (no single-field read)
schema— nonegap
flip-task, update-task, update-roadmap, update-backlogPATCH record/:id with one FieldPatch ['tasks', id, field] (or theme/item path)full
flip-subtask, update-subtaskPATCH record/:taskId with ['tasks', taskId, 'subtasks', subId, field]full
append-journalPATCH on ['tasks', taskId, 'subtasks', subId, 'details']client must read-concatenate-write (replace semantics), raced-guarded only by baseMtimepartial (no append op)
add-subtask, add-subtasks, delete-subtaskonly via whole-subtasks[]-array PATCHgap (no nested-record CRUD; no auto-id)
open-task, create-theme, create-backlogPOST /api/ledger/recordpartial (no auto-id, no withCreateDefaults, no named-flag assembly)
delete-backlogDELETE /api/ledger/record/:idfull
promotePOST /api/ledger/transaction op:"promote"partial (no --capability-theme roadmap third leg)
update-umbrella— none (detectSchema knows exactly three document_names: ‘Knowledge Hub Task List’, ‘Knowledge Hub Roadmap’, ‘Product Backlog’ — detect-schema.ts:40–44)gap
--dry-run, --force, --pretty, envelope/exit-code contract— nonegap
mirror regenbuilt-in, scoped, superiorsubstrate advantage
(no CLI equivalent)mtime optimistic concurrency, 409 contractsubstrate advantage

2.5 Vendored-schema relationship (bidirectional)

Section titled “2.5 Vendored-schema relationship (bidirectional)”
  • KH → task-view: the four schema sources lib/validation/{task-list,roadmap,backlog}-schema.ts + work-status.ts are vendored into task-view packages/schemas/src/. KH’s .github/workflows/task-view-vendor-drift.yml watches exactly those four paths and emits a non-blocking ::warning:: (ratified OQ-T2) on substantive normalised drift against the v0.3.1-task-view release assets.
  • task-view → KH: the four mutation primitives packages/server/{atomic-write,detect-schema,patch-apply,record-mutate}.ts are vendored into lib/ledger/ (ID-35.10); the same workflow’s second step clones task-view at the pinned tag and diffs them (non-blocking).
  • KH-only, never vendored: lib/ledger/scoped-serialise.ts (explicitly excluded from the drift list per lib/ledger/README.md) and lib/validation/ledger-budgets.ts (its header: “NOT in the vendor-drift watched-paths list”).
  • Consequence for ID-90: any gate port into the substrate changes task-view, requiring a new tag + TASK_VIEW_TAG bumps in ci.yml (line 988) and the regen-mirrors cache key; if budgets or scoped serialisation move upstream, the vendor-drift watch lists and the “never vendor” annotations need a deliberate revision.
  • Minor doc drift found: lib/ledger/README.md still cites pinned release v0.2.0-task-view; ci.yml and regen-mirrors resolve v0.3.1-task-view. Fold a README pin correction into the cutover.

§3 GUARD PORT — ledger-client-name-guard.yml + scripts/check-ledger-client-name.ts

Section titled “§3 GUARD PORT — ledger-client-name-guard.yml + scripts/check-ledger-client-name.ts”

3.1 Where the guard lives (and does not live)

Section titled “3.1 Where the guard lives (and does not live)”
  • CI twin commit: 245c35ac7ca22e7104fb591e620514f8300a3d3c — “feat(ID-68): {68.13} guard-CI — ledger client-name CI lint (delta, redacted, secret-injected)” — adds .github/workflows/ledger-client-name-guard.yml (72 lines), scripts/check-ledger-client-name.ts (241 lines), __tests__/scripts/check-ledger-client-name.test.ts (108 lines), and a 7-line export-only edit to scripts/ledger-cli.ts. Contained only in branch id68-relocation-phase23 (git branch --contains verified); NOT an ancestor of canonical HEAD (git merge-base --is-ancestor 245c35ac HEAD → false).
  • Mutation-guard commit: 52ef3d5b (id68-relocation-phase23; cherry-pick twin ceff8d8f on id68-backup-pre-s301a3) — adds guardClientName() to all three ledger-cli write paths (at 245c35ac’s file state: clientNameDenylist line 1913, countClientNameHits 1928, guardClientName 1947; call sites: commitMutation 2138, promote staged writes 3996/4002/4009, umbrella write 4327).
  • Boundary (Inv 42): {68.15}‘s details records the settled boundary verbatim: “the ledger client-name write guard (ledger-client-name-guard.yml + scripts/check-ledger-client-name.ts, commit 245c35ac) is id68-relocation-phase23-branch-only — do NOT cherry-pick it here; it ports into the ID-20 patch-server substrate (Inv 42 boundary)”. That port target is now ID-90.

3.2 Current parse/check logic (at 245c35ac)

Section titled “3.2 Current parse/check logic (at 245c35ac)”
  • clientNameDenylist(): string[] — reads process.env.KH_CLIENT_NAME_DENYLIST, splits on commas, trims, lowercases, drops empties (ledger-cli@245c35ac:1913–1919). Read at call time; empty/unset → guard inactive locally.
  • countClientNameHits(content, denylist): number — per-term regex-escaped global case-insensitive ('gi') match count over raw serialised bytes (1928–1938).
  • guardClientName(subcommand, prev, next, warnings)net-new delta semantics: reject only when after > before (existing references persist; a sanitising write that decreases the count passes); KH_LEDGER_ALLOW_CLIENT_NAME=1 downgrades to a warning and allows (for de-ID journals that legitimately describe a removal) (1947–1976).
  • check-ledger-client-name.ts (CI twin) — pure evaluator evaluateLedgers diffs every docs/reference/*.json between --baseline (PR base SHA; default origin/main) and HEAD using the SAME two exported functions (“single source of truth … so the CI lint and the mutation guard can never drift” — file header). Redacted output: never echoes a matched term; reports paths + counts only. Exit codes: 0 pass; 1 net-new hit (PR-blocking); 2 infra/misconfig — an unset denylist in CI is fail-loud, unlike the local guard’s benign no-op.
  • ledger-client-name-guard.yml — PR lint on docs/reference/*.json (+ self/guard/cli paths), fetch-depth: 0, secret injected via env: never inline ${{ }}, baseline = PR base SHA. Branch-protection required-check registration was deferred to the {68.9} flip operator step.

3.3 The adaptation REQUIRED: comma-separated → full canonical JSON

Section titled “3.3 The adaptation REQUIRED: comma-separated → full canonical JSON”

{68.15} (status: done; journal block 2026-06-07T10:00:54Z) widened the KH_CLIENT_NAME_DENYLIST secret itself to the full canonical denylist JSON — per identity-guard.yml (canonical, lines 19–27): tokens[].{value, class, case_insensitive} + exclusion_patterns[], canonical copy in the private docs-site repo at ops/identity-denylist.json, synced 2026-06-07T09:52:32Z, “4 tokens + 4 carve-out exclusion patterns”. The widening is the same secret the ledger guard consumes (“one rotation point”) and was an accepted desirable widening: the ledger guard will also reject non-name identity strings (infrastructure host, domain, support address, historical ICO registration reference, retired hook name — all referred to generically here).

Consequence: the 245c35ac clientNameDenylist() comma-split parser is now wrong against the live secret — it would treat the entire JSON blob as garbage tokens (split on the commas inside JSON), silently mis-counting. This was caught at {68.15} and routed to bl-244 (“Discovery routed: ledger-guard comma-parser vs JSON secret shape -> bl-244 notes”), which became this Task. The ported guard must:

  1. Parse the env value as JSON; validate shape (tokens non-empty array — mirroring identity-guard.yml’s jq -e '(.tokens | type == "array") and (.tokens | length > 0)', line 89).
  2. Extract tokens[].value, honouring per-token case_insensitive (identity-guard iterates per token with a per-token case flag, lines 384–390 — the current single 'gi' regex must become per-token).
  3. Decide exclusion_patterns[] applicability for ledger content (identity-guard applies them as carve-outs for parameterised-config keys and slug examples; the byte-delta ledger guard may or may not need them — open question OQ-7).
  4. Preserve: net-new delta semantics, redaction (never echo a token — the substrate’s HTTP error responses must be redacted the same way), fail-loud-in-CI vs inactive-locally split, and the override escape hatch.
  5. Relocate the 7 synthetic-denylist tests (check-ledger-client-name.test.ts, 108 lines) plus the 3 mutation-guard tests (ledger-cli-client-name-guard.test.ts, per 52ef3d5b’s message) into the substrate’s test suite, adapted to JSON-shaped fixtures.

A reasonable transitional parser accepts BOTH shapes (JSON-first, comma fallback) so local .env.local values and the CI secret can migrate independently — flag for {90.3} TECH.


4.1 Caller inventory (production surfaces — all invoke via shell, none via import)

Section titled “4.1 Caller inventory (production surfaces — all invoke via shell, none via import)”
ConsumerSiteSubcommands usedBreak-on-replace exposure
workflow-orchestration skill (Orchestrator, every SDLC session, daily).claude/skills/workflow-orchestration/SKILL.md:390 (“The Orchestrator owns ledger writes for status transitions, journal-block appends, Subtask additions, and Task opens”), :415–422 (canonical promote invocation incl. --capability-theme)promote explicitly; implicitly the full status/journal/add surface via the field-discipline sectionHighest — live sessions mutate ledgers daily; any contract change (argv, envelope, exit codes) lands mid-session
update-roadmap-backlog skill (Curator write layer).claude/skills/update-roadmap-backlog/SKILL.md:10,24 (“All writes route through bun scripts/ledger-cli.ts — never Edit”), full subcommand catalogue at 139–539 (schema, get, create-theme, create-backlog, open-task, update-roadmap, update-backlog, flip-subtask, append-journal, update-umbrella)broad write surfaceHigh
triage-finding skill.claude/skills/triage-finding/SKILL.md:24,64 (read-only get/show/schema affordance), 119/164/246/305/312 (documents downstream write semantics + budget gates)reads directly; writes only via update-roadmap-backlogMedium (doc-accuracy)
workflow-curator agent.claude/agents/workflow-curator.md:102,129,139,237–256,320–325 (ALWAYS via ledger-cli; never raw Edit; create-theme/create-backlog/open-task/add-subtask/add-subtasks)create/promote-adjacent writesHigh
task-planner agent.claude/agents/task-planner.md (budget-gate language; references add-subtask/update-subtask HARD budgets)authoring constraintsLow (doc-accuracy)
handoff / start-session skillsread docs/reference/task-list.json directly (start-session SKILL.md:71; handoff SKILL.md:19) — no ledger-cli invocationnoneNone (but they read the canonical file — file location moves matter)
CI — ledger-mirror-parityci.yml:936–1006; clones task-view @ TASK_VIEW_TAG: v0.3.1-task-view (988), runs --check ×3, git diff --exit-code over docs/reference/{tasks,roadmap,backlog}mirror parity on the committed ledgersMust be re-homed/retired when ledgers leave the public repo
CI — build-gateci.yml:1097–1105 — whole-repo tsc includes scripts/ledger-cli.ts (added after a 4-session undetected type break)typecheckKeeps guarding the CLI while it exists
CI — task-view-vendor-driftwatches the 4 schema files + 4 vendored primitives (§2.5)drift remindersTag bumps + watch-list revision in cutover
scripts/regen-mirrors.sh (invoked BY ledger-cli per write); ledger-renormalise.ts + ledger-normalise-oqls2.ts (format-maintenance siblings emitting the same conforming format); ledger-sweep-s269.ts (historical)byte-format contractFormat change = these need revisiting
Tests18 __tests__/scripts/ledger-cli-*.test.ts files importing run/ParsedArgs/… (ast-dataflow, §Context)full CLI behaviour suiteMigrate/retire with the CLI; guard tests move with ID-90 per {68.27}
Docsdocs/reference/task-list-discipline.md (§3 names the CLI as enforcement point), lib/ledger/README.md, docs/runbooks/ci.mdprose accuracyUpdate at cutover

4.2 What breaks if the CLI is replaced naively

Section titled “4.2 What breaks if the CLI is replaced naively”
  1. Byte format — the substrate’s serialiseLedger (patch-server.ts:201–205) emits JSON.stringify(data, null, 2): raw UTF-8, Zod-reparse key order, no trailing newline. The first substrate write to a live ledger produces a near-whole-file diff (the exact ~1417-line defect class scoped-serialise was built to kill), breaks byte-compatibility with the renormalise scripts, and pollutes the git history the ledgers still live in until {68.30}. The scoped/escape serialisation must port into the substrate write paths BEFORE any production write.
  2. The Orchestrator’s JSON-envelope contract — skills parse stdout envelopes and exit codes; the substrate speaks HTTP. Without a façade, every skill/agent surface above breaks simultaneously, mid-session.
  3. Gates vanish — budget, record-set, and (id68) client-name gates do not exist server-side; an unported cutover silently drops prevent-at-source enforcement that task-list-discipline.md §3 documents as the contract.
  4. Capability holes — auto-id, create defaults, subtask CRUD ergonomics, journal append, --capability-theme, umbrellas, dry-run, schema discoverability (§2.4 gaps).
  5. CI parityledger-mirror-parity compares committed mirrors against --check regen; the server’s in-process regen must stay byte-identical to --check output (same generator, so low risk — but tag skew between the server KH runs and TASK_VIEW_TAG would desync mirrors).

4.3 Hold-release statements (explicit, per brief)

Section titled “4.3 Hold-release statements (explicit, per brief)”

(a) PLAN {68.4} Gate-2 ledger-subset hold on {68.30}. Sources: ID-68 PLAN.md §4 row “Phase 5 … cross-Task: ID-20 cutover for the Gate-2 ledger release” (line 110) and §7 row “30 | ID-20 patch-server cutover (Gate 2) before the ledger-subset release | Hold blocked for the release step; other gates assemble meanwhile” (line 185); {68.30} details G2: “ID-20 cutover journal reference — CROSS-TASK GATE (Orchestrator: hold blocked if ID-20 has not recorded its cutover); THEN release the record-27 holdback: git rm the ledger JSONs + mirrors from public … -> AC-C1 closes fully”. With the referent re-pointed to ID-90, the evidence that releases the hold is a recorded ID-90 cutover journal block establishing, with commit SHAs: (i) the patch-server substrate is the recorded sole write path for the three ledgers (gates live server-side); (ii) the ledger-cli mutation surface is retired or frozen read-only; (iii) the consuming skill/agent surfaces (§4.1) are repointed; (iv) the ledgers + mirrors have a private-side home and the parity gate has been re-homed or retired — so that {68.30}‘s git rm of docs/reference/{task-list,product-backlog,product-roadmap,product-retros,umbrellas}.json + docs/reference/{tasks,backlog,roadmap,retros}/ (the PC-14 holdback set per {68.27}) does not break the live writer or CI.

(b) The {68.27} ledger-guard holdback. Sources: {68.27} details HOLDBACK (a): “LEDGER SUBSET (PC-14): … STAY until the ID-20 cutover is recorded done (Gate 2; release rides record 30). Ledger guard tests move with ID-20, not here”; PLAN §7 row “27 | Ledger guard tests move with ID-20, not this Task | Holdback encoded in details” (line 187); {68.15} BOUNDARY (do-not-cherry-pick, §3.1). The evidence that releases it is: the client-name guard’s functional equivalent live in the substrate — JSON-denylist parsing per §3.3, net-new delta semantics, redacted error surfaces, fail-loud CI posture (or a ratified decision that the CI twin is unnecessary once the ledgers are private — OQ-6), with the 245c35ac/52ef3d5b test set relocated and green in the substrate suite — recorded in the ID-90 journal. At that point the id68-branch guard files are formally superseded and are never merged to canonical (Inv 42 satisfied).


Gap analysis — CLI capability with no substrate equivalent yet

Section titled “Gap analysis — CLI capability with no substrate equivalent yet”

Ordered by cutover criticality:

#GapSeverityNotes
G1Conforming byte format (escaped \uXXXX, on-disk key order, trailing newline; minimal-diff scoped writes)Blockerscoped-serialise.ts is KH-authored, not in task-view; substrate serialiseLedger is non-conforming on all three axes (§4.2.1)
G2Budget gate ({35.17}) + --force analogue + grapheme countingBlockerLEDGER_BUDGETS must NOT enter the vendored Zod schemas (ledger-budgets.ts header); port as server-side plain-data config
G3Record-set/delta gate ({35.16}) on bytes-about-to-be-writtenBlockersubstrate has duplicate-id insert check only; no post-serialisation id-set assertion
G4Client-name guard (de-ID gate, §3) incl. JSON denylist parserBlocker (Inv 42 is the point of the port)env-config + override semantics needed server-side
G5Subtask-level CRUD (add-subtask/add-subtasks/delete-subtask) + auto-id (nextId) + withCreateDefaultsHighsubstrate route today = whole-subtasks[] PATCH; no id allocation
G6append-journal append semanticsHighserver PATCH is replace; append = client read-concat-write under baseMtime (or a new server op)
G7Promote --capability-theme (third, roadmap leg)Mediumsubstrate transaction is strictly two-ledger
G8update-umbrella / umbrellas.jsonMediumoutside detectSchema’s three known document names; raw-UTF-8 byte format differs from the ledgers too
G9Operator/agent contract: JSON envelope, exit codes, --dry-run, --pretty, schema/--help discoverability, dotted-id formsHigh for skillsargues for a CLI façade rather than raw HTTP in skill briefs
G10Discipline soft warnings (scoped parseTaskListWithWarnings surfacing)Lownice-to-have on write responses
G11Multi-ledger service modelMediumone ledgerPath per server instance; three ledgers ⇒ three instances or an extension (transaction endpoint already resolves directory siblings)

Substrate advantages gained at cutover (for completeness): mtime optimistic concurrency (the daily multi-session clobber hazard the CLI cannot address), in-process scoped mirror regen (no clone/shell-out), the original promote transaction, a single long-lived writer process as a natural gate-enforcement chokepoint, and the existing viewer/editor UI on the same server.


Zero-downtime constraint: the ledgers are mutated daily by live orchestrator sessions; at no point may a session find itself without a working sanctioned write path.

Section titled “Option A — Adapter-first, two-phase (façade preserved) — RECOMMENDED shape”
  1. Phase 0 (upstream task-view; zero KH impact): port G1 (scoped/escape serialisation) + G2 + G3 + G4 into the patch-server write paths; add the minimal missing ops (G5/G6 as server affordances or documented client recipes); cut tag v0.4.x-task-view. KH ledgers untouched; live sessions unaffected.
  2. Phase 1 (KH, non-breaking): bump TASK_VIEW_TAG (ci.yml:988, vendor-drift, regen-mirrors cache key). Convert scripts/ledger-cli.ts internals to an HTTP client against a localhost patch-server (spawn-on-demand or session-started), preserving argv surface, JSON envelope, and exit codes byte-for-byte, behind an env flag (e.g. KH_LEDGER_SERVER) defaulting OFF. Skills/agents need zero edits.
  3. Phase 2 (flip + soak): default the flag ON; soak across ≥1 full SDLC session with ledger-mirror-parity green and diff-shape spot checks (scoped one-line diffs preserved).
  4. Phase 3 (retire + release): record the cutover journal (releases {68.30} G2 + {68.27} holdback per §4.3); update the six doc/skill surfaces and migrate/retire the 18 CLI test files; ledgers + mirrors then leave the public repo at {68.30}, with the parity gate re-homed alongside them.
  • Risk profile: lowest blast radius; instant rollback (flip the env flag); the dangerous change (byte format, G1) lands and is testable upstream before any production write. Residual risks: transitional dual-implementation drift (mitigated by making the façade a thin transport — all gate logic server-side from Phase 1), and the façade temporarily masking server-side error shapes (mitigate: pass-through error vocabulary mapping 409→mtime-mismatch etc.).

Port gates + guard + serialisation upstream, then in a single KH wave: stand up the server as sole writer, rewrite all skill/agent surfaces to call HTTP (or a new thin client), retire ledger-cli, re-home ledgers, release the holds.

  • Pros: one coordinated move; no transitional dual path; releases the ID-68 holds in the fewest sessions; no façade to later unwind.
  • Cons / risk: every consumer in §4.1 changes simultaneously against a daily-mutated ledger — a single defect (byte format, envelope mismatch, server-lifecycle hiccup) stalls all live SDLC sessions; skill-doc rewrites and live behaviour must flip atomically across multiple parallel cmux sessions; rollback requires reverting many surfaces at once. Violates the spirit of the zero-downtime constraint unless executed inside a single quiesced session — which the parallel-session reality makes hard to guarantee.

Option C (variant of A) — Server-side gates, KH-side guard interim

Section titled “Option C (variant of A) — Server-side gates, KH-side guard interim”

As Option A, but the client-name guard stays a KH-side façade check until the ledgers relocate. Rejected as the primary recommendation: it contradicts the ratified Inv 42 boundary (“ports into the patch-server substrate”) and leaves the guard outside the chokepoint the cutover creates; only worth considering if upstream-tagging cadence becomes the bottleneck.


#QuestionOwnerDecision needed by
OQ-1Gate-hosting split: do budget/record-set/de-ID gates land in upstream task-view code (new tag per change) or in a KH-side server-wrapper layer? Inv 42 reads as upstream; but scoped-serialise.ts and ledger-budgets.ts are deliberately KH-only today and their vendor-drift annotations would need revisionLiam (ratify at {90.2}/{90.3})before {90.3} TECH
OQ-2Server lifecycle: long-lived per-session daemon (started by start-session? cmux? launchd?) vs spawn-on-demand per mutation; and one instance per ledger (current model) vs a multi-ledger bind extension{90.3} TECHbefore {90.4} PLAN
OQ-3Post-cutover private home for the five ledger JSONs + four mirror dirs (private docs-site repo? operator-local?) and where ledger-mirror-parity (or its successor) runs once they leave the public repoLiam + Orchestrator (interacts with ID-68 Phase 5 timing)before {68.30} executes
OQ-4append-journal semantics server-side: add a first-class append op vs client read-concat-PATCH under baseMtime{90.3} TECHbefore {90.4}
OQ-5umbrellas.json in or out of ID-90 scope (no substrate support; different byte format; no mirrors){90.2} PRODUCTat PRODUCT ratification
OQ-6Does the CI twin (ledger-client-name-guard.yml) still need a port target once the ledgers are private, or does the substrate mutation-time guard suffice? (The guard’s purpose shifts from public-repo lint to write-time hygiene)Liambefore {68.27}/{68.30} release steps
OQ-7Do exclusion_patterns[] from the canonical denylist JSON apply to ledger byte-delta counting, or are they identity-guard-only carve-outs?{90.3} TECH (escalate to Liam if semantics ambiguous)with G4 port
OQ-8Referent re-point: {68.27}/{68.30}/PLAN {68.4} all say “ID-20”; ID-90 supersedes. Amend the ID-68 records’ hold language or record the mapping in the {68.30} journal only?Orchestratorbefore {68.30} dispatch
OQ-9lib/ledger/README.md stale pin (v0.2.0-task-view vs live v0.3.1-task-view) — fold correction into cutover docs passOrchestrator (cleanup line in {90.4})any time

Claim areaEvidence
CLI surface + contractscripts/ledger-cli.ts (4,367 lines): header 2–77; USAGE 1064–1118; parseArgs 349–397; flags 287–331; envelope 150–165 / emit 1120–1157; aliases 895
Delta gatescripts/ledger-cli.ts:1315–1486 (RecordSetDelta 1333, assertRecordSet 1413, checkRecordSet 1442); applied 2044–2054, 3819–3848
Budget gatescripts/ledger-cli.ts:1488–1649; lib/validation/ledger-budgets.ts (LEDGER_BUDGETS values + plain-data mandate); docs/reference/task-list-discipline.md §2/§3
Scoped-write defaultscripts/ledger-cli.ts:180–195, 1085–1094; lib/ledger/scoped-serialise.ts; lib/ledger/README.md (KH-authored section, ~1417-line diff rationale); on-disk format verified by hexdump + escapes in task-list.json (07/06/2026)
Promotescripts/ledger-cli.ts:3551–3955; substrate original packages/server/ledger-transaction.ts:1–120
Mirror regenscripts/ledger-cli.ts:1724–1791; scripts/regen-mirrors.sh; apps/server/index.ts:204–261 (--check); ci.yml:936–1006
Substrate API.cache/task-view-v0.3.1-task-view @ 1644742762df3f202ab2a7bd3507aab5758fdd30 (tag v0.3.1-task-view): packages/server/patch-server.ts (endpoints 13–27, handlers 433–1223, dispatcher 1229–1291, factory 1307–1343, serialiseLedger 201–205, mtime check 578–601); packages/server/{ledger,atomic-write,record-mutate,detect-schema}.ts; lib/ledger/patch-apply.ts:14, 44–49, 76–170 (walk + known fields)
Vendor relationship.github/workflows/task-view-vendor-drift.yml (watched paths + two-direction diff); lib/ledger/README.md provenance table
Guard (CI twin)git show 245c35ac --stat; git show 245c35ac:scripts/check-ledger-client-name.ts (full); git show 245c35ac:.github/workflows/ledger-client-name-guard.yml (full); branch containment via git branch --contains
Guard (mutation)git log --all -S guardClientName52ef3d5b / ceff8d8f; git show 245c35ac:scripts/ledger-cli.ts lines 1895–1976, call sites 2138/3996/4002/4009/4327
Denylist wideningtask-list.json {68.15} details + journal (jq, 07/06/2026); .github/workflows/identity-guard.yml:19–27, 89–91, 373–390 (JSON shape + per-token iteration)
Holdstask-list.json {68.27} + {68.30} details (jq); docs/specs/ID-68-repo-visibility-ip-separation/PLAN.md:107–112, 183–187; PRODUCT.md:387–397 (Inv 42 table)
Consumersgrep sweeps over .claude/skills/, .claude/agents/, .github/workflows/, scripts/, docs/ (file:line cited inline in §4.1); ast-dataflow importers --module 'scripts/ledger-cli' (18 test importers, JSON output 07/06/2026)
Code-intel orientationgitnexus_query ×2 + gitnexus_context ×2 outputs quoted in §Context

Recommendation for {90.2} PRODUCT: spec the cutover as Option A’s two-phase shape — behaviour invariants for (1) byte-format conformance of every server write, (2) gate parity (delta/budget/de-ID with --force-equivalent and override semantics), (3) the preserved operator contract (envelope/exit codes via the façade), (4) optimistic-concurrency behaviour under parallel sessions, (5) the hold-release evidence set of §4.3 as acceptance criteria — with OQ-1/OQ-5/OQ-6 surfaced for ratification before TECH.