Skip to content

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

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

Section titled “PRODUCT — 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.2} PRODUCT
  • Session: S322 (authored 07/06/2026)
  • Predecessor: docs/specs/ID-90-server-ledger-cutover/RESEARCH.md ({90.1}, ratified S322 — Checker PASS_WITH_NOTES, nits fixed inline)
  • Ratified inputs (Liam, S322, at the RESEARCH gate): (1) cutover shape = Option A (adapter-first, two-phase) RATIFIED — this spec describes that shape only; Options B/C remain recorded context in RESEARCH. (2) OQ-1 gate hosting defers to TECH, but with a mandated first-class §Simplicity review herein. (3) OQ-5: umbrellas.json in scope, with document model + byte format aligned to the ledgers where possible. (4) OQ-6 (CI-twin disposition) folds into the simplicity review.
  • 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.

Cut all mutation of the KH workflow ledgers over from scripts/ledger-cli.ts (direct in-process primitives) to the task-view patch-server substrate, porting the three write gates (record-set, budget, client-name guard) and the conforming byte format into the server so a single long-lived writer becomes the enforcement chokepoint — while preserving the operator contract (argv surface, JSON envelope, exit codes) byte-for-byte through a thin CLI façade, gaining optimistic concurrency across parallel orchestrator sessions, and releasing the two ID-68 holds ({68.30} Gate-2 ledger-subset release; {68.27} ledger-guard holdback).

The “user” of this surface (per write-product-spec) is not an end user of the KH app: it is (a) the orchestrator sessions, skills, and agents that invoke bun scripts/ledger-cli.ts … daily and parse its JSON envelopes (workflow-orchestration, update-roadmap-backlog, triage-finding, workflow-curator, task-planner — RESEARCH §4.1); (b) CI jobs that consume the committed ledgers and mirrors (ledger-mirror-parity, task-view-vendor-drift, build-gate); and (c) Liam as operator. This spec describes behaviour from those consumers’ perspectives.

Code-intelligence orientation is carried by the ratified RESEARCH §Context (Inv 2 satisfied there), quoted:

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) […]

gitnexus_context({name: 'insertRecord'}) — incoming calls: run and promote, both in scripts/ledger-cli.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) […] 1 direct caller.

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 […] ast-dataflow importers --module 'scripts/ledger-cli'18 importers, all test files […] Zero production TypeScript importers — every production consumer invokes the CLI as a subprocess (shell), not as a module.

Fresh-Planner spot re-verifications for this document (07/06/2026): grep sweep of all TS importers of lib/validation/{task-list,roadmap,backlog}-schema.ts, work-status.ts, umbrellas-schema.ts and of lib/ledger/* (results enumerated in §Simplicity review); hexdump of docs/reference/umbrellas.json confirming raw-UTF-8 em-dashes (zero \uXXXX escapes), document_name: "umbrellas", and a trailing newline already present; task-view-vendor-drift.yml watched-path list confirmed (4 schema sources + 4 vendored primitives).

The problems this Task solves, in consumer terms:

  1. Unguarded concurrency. ledger-cli is an unguarded read–modify–write per invocation; parallel cmux sessions mutating the same ledger can silently clobber each other. The substrate’s mtime optimistic concurrency (409 on conflict) closes this — but the 409 must be given defined semantics at the façade (Behaviour §H).
  2. Gates live in the wrong place after cutover. The record-set, budget, and (id68-branch-only) client-name gates exist in CLI code; a naive substrate cutover silently drops prevent-at-source enforcement (RESEARCH §4.2.3). They must become properties of the server.
  3. Non-conforming substrate byte format. The substrate’s serialiser emits raw UTF-8, Zod-reparse key order, no trailing newline — the exact ~1417-line-diff defect class scoped serialisation was built to kill (RESEARCH G1). No production write may occur until this is fixed.
  4. Two ID-68 holds block the repo-visibility track until this cutover is recorded ({68.30} Gate-2; {68.27} guard holdback — RESEARCH §4.3).
  5. The bidirectional vendor model may be obsolete post-cutover — Liam’s mandated simplicity question, treated first-class in §Simplicity review.

Figma: omitted — no visual surface (CLI/server/protocol feature).

  • G-A: The patch-server substrate becomes the sole sanctioned mutation path for the in-scope documents, with all three gates (record-set, budget, client-name guard) enforced at that chokepoint.
  • G-B: Zero disruption to consumers — the façade preserves the operator contract exactly; no skill/agent edits are needed to keep working at any phase boundary.
  • G-C: The multi-session clobber hazard is closed with defined, observable conflict semantics.
  • G-D: umbrellas.json joins the substrate’s document model with a byte format aligned to the ledgers.
  • G-E: The two ID-68 hold-release evidence sets (§4.3) are satisfied and recorded.
  • G-F: The post-cutover ownership shape (vendoring, drift workflows, CI twin) is the simplest sustainable one, ratified explicitly rather than inherited by default.
  • Rewriting consumer skills/agents to speak raw HTTP to the server (the façade remains the operator surface; any later de-façade is future work).
  • A write path for product-retros.json (no ledger-cli surface exists today to preserve; the retros ledger travels with the {68.30} private-home set unchanged).
  • Mirror format or mirror content changes (regen must stay byte-identical to today’s --check output).
  • Executing {68.30} itself (the git rm + private re-home is ID-68’s record; ID-90 produces its release evidence).
  • Choosing the post-cutover private home for the ledgers (OQ-3 — Liam + Orchestrator decision; this spec only records the dependency).
  • task-view viewer/editor UI changes; publishing task-view as a public package.

Numbered, individually testable. Gap coverage (RESEARCH G1–G11) is mapped per section and summarised in the coverage table at the end of this section.

A. Cutover phasing — Option A, ratified (zero-downtime)

Section titled “A. Cutover phasing — Option A, ratified (zero-downtime)”
  1. At every moment of the cutover there is exactly one sanctioned, working mutation path for every in-scope document, and no live orchestrator session is ever left without it. Phases land strictly in order 0 → 1 → 2 → 3, and a phase does not begin until its predecessor’s acceptance criteria (§Acceptance criteria) are met.
  2. Phase 0 (gates + format land upstream in task-view, new tag cut) has zero KH impact: no KH production ledger receives a write through the substrate, and no consumer-observable behaviour changes, until Phase 1 begins.
  3. From Phase 1, scripts/ledger-cli.ts is a façade governed by the KH_LEDGER_SERVER environment flag, defaulting OFF. With the flag OFF (or unset), behaviour is the current direct-primitives behaviour, unchanged byte-for-byte: same envelopes, same exit codes, same bytes on disk, same mirror regen.
  4. With the flag ON, the same invocation (identical argv) produces the same observable outcome — same envelope shape and content, same exit code, byte-identical resulting file — with transport via the local patch-server instead of in-process primitives.
  5. Rollback is instant and total until Phase 3: flipping the flag OFF restores the direct path with no migration, cleanup, or data conversion step. A single invocation never mixes the two paths.
  6. Phase 1 requires zero edits to the consuming skill/agent surfaces (workflow-orchestration, update-roadmap-backlog, triage-finding, workflow-curator, task-planner, and any other .claude/ surface that invokes ledger-cli): their documented invocations keep working unmodified in both flag states.
  7. Phase 2 (flag default ON) begins only after Phase 1 acceptance, and Phase 3 (retirement) begins only after a soak of at least one full SDLC session under default-ON with the Phase 2 acceptance evidence collected.
  8. At Phase 3, the direct in-process write path is removed (or demonstrably frozen read-only); the substrate is the sole writer; the CLI façade remains the operator surface with its argv contract intact.
  9. Gate enforcement is a property of the server, not the client: from the first flag-ON production write, any mutation submitted to the server — by the façade or by any other client — is subject to identical gate outcomes. No gate exists only client-side, so no client can bypass a gate by talking to the server directly.

B. Operator contract — preserved through the façade (G9)

Section titled “B. Operator contract — preserved through the façade (G9)”
  1. All 19 subcommands plus the show-taskshow alias are accepted with identical arity and argument forms, including dotted subtask ids (<taskId.subId>) and the legacy three-argument flip-subtask form.
  2. The flag surface (--dry-run --pretty --whole-file --scoped --force --append --no-regen-mirrors --regen-mirrors --ledger-dir --file --id --title --description --status --depends --priority --notes --test-strategy --status-note --type --track --effort-estimate --capability-theme --add-tasks --remove-tasks --reorder) is preserved; unknown flags are rejected with the same error behaviour as today.
  3. The envelope contract is preserved exactly: success emits a single-line JSON {ok: true, subcommand, result, warnings?, mirrorStale?, mirrorStaleReason?} to stdout with exit 0; failure emits {ok: false, subcommand, error, detail?, issues?} to stderr with exit 1.
  4. stdout purity holds: nothing but the envelope is ever written to stdout; all child/regen/diagnostic output routes to stderr, so ledger-cli … | jq works in both flag states.
  5. The error vocabulary (schema-error, walk-error, duplicate-id, record-not-found, budget-exceeded, record-set-violation, unknown-theme, …) is preserved, extended by exactly one new TRANSPORT code — mtime-mismatch (invariant 44) — during Phases 1–2; the guard port additionally introduces client-name-guard and client-name-guard-config, mandated by invariants 28–36 and surfaced only when the guard is configured (Liam-ratified clarification, S322 TECH gate). No existing code is renamed or repurposed.
  6. Read subcommands (show, get including field projection, schema including per-ledger/per-recordKind forms) produce byte-identical output in both flag states.
  7. --dry-run evaluates the full gate chain and reports what would happen, writes nothing, triggers no mirror regen, and consumes no concurrency state — identically in both flag states.
  8. --pretty output formatting is preserved in both flag states.
  1. Every substrate write to an in-scope document emits the conforming on-disk format: non-ASCII characters as \uXXXX escapes matching the current on-disk convention, the existing on-disk key order preserved (never Zod-reparse reorder), and a trailing newline.
  2. A single-field edit produces a minimal scoped diff (the one-line-edit class), never a whole-file re-emit; the ~1417-line-diff defect class (RESEARCH §1.3) must not reappear on any write path.
  3. Scoped and explicit --whole-file writes remain byte-compatible (the OQ-LS-2 normalisation invariant holds server-side): a scoped write followed by a whole-file re-emit of unchanged content is a no-op diff.
  4. Differential equivalence: during Phases 1–2, any supported mutation applied to identical input via the direct path and via the substrate yields byte-identical resulting files. This is the gating proof for Phase 0/1 acceptance and a standing regression check until Phase 3.
  1. Every write asserts that the post-write record id-set — derived from the bytes about to be written, not the in-memory object — equals the pre-write id-set under the intended delta (none | add | add-many | remove). A violation rejects with record-set-violation, exit 1 (HTTP-level equivalent server-side), and nothing written.
  2. Promote asserts the gate per leg before commit: task-list +1, backlog −1, and roadmap when the capability-theme leg is present.
  1. The budget gate runs server-side before any byte is written, against the LEDGER_BUDGETS registry as the single source of truth — which remains plain data, never Zod .max() (so the live over-budget ledger keeps parsing) — using grapheme counting (Intl.Segmenter semantics, not UTF-16 code units).
  2. Both budget modes are preserved: field-update mode (only the mutated field can hard-reject; other over-budget fields surface as soft warnings) and create mode (first over-budget field is fatal) — matching today’s mutatedField semantics exactly.
  3. --force remains a per-invocation escape hatch: it downgrades the budget rejection to a (forced) budget-exceeded: warning in the success envelope and writes anyway. Force never becomes a server-global or session-global state; an unforced invocation immediately after a forced one rejects as normal.
  4. subtask.details remains unbudgeted (the append-only journal home) on every write path.

F. Client-name guard — the de-ID gate (G4; Inv 42 port)

Section titled “F. Client-name guard — the de-ID gate (G4; Inv 42 port)”
  1. The guard is active on every substrate write path for every in-scope document (the three ledgers and umbrellas.json): there is no mutation route — endpoint, transaction leg, or regen side-effect — that writes canonical bytes without the guard evaluating them.
  2. The guard parses KH_CLIENT_NAME_DENYLIST as the full canonical denylist JSON — tokens[].{value, class, case_insensitive} plus exclusion_patterns[] — and validates the shape (tokens present, non-empty array). The 245c35ac comma-split parser is replaced; it must never be applied to a JSON-shaped value.
  3. Per-token case sensitivity is honoured: each token matches case-insensitively or case-sensitively according to its own case_insensitive flag, not a single global regex flag.
  4. Net-new delta semantics are preserved: a write is rejected only when the total denylist-hit count over the bytes-about-to-be-written exceeds the count over the prior on-disk bytes. Writes that hold the count equal or decrease it (sanitising edits) pass.
  5. A guard rejection writes nothing, and every error surface is redacted: no façade envelope, no HTTP response body, no server log line ever echoes a matched token — paths and counts only.
  6. The override escape hatch is preserved: KH_LEDGER_ALLOW_CLIENT_NAME=1 downgrades the rejection to a warning and allows the write (for de-ID journals that legitimately describe a removal). The warning is redacted per invariant 32.
  7. The activation split is preserved: an unset/empty denylist makes the guard a benign no-op in local/interactive use, but is fail-loud in CI context (a distinct infra/misconfig failure, never a silent pass) — matching the CI twin’s exit-2 posture.
  8. A denylist that is set but invalid (unparseable JSON, wrong shape, empty tokens[]) is a loud configuration error that blocks the write in every context. Misconfiguration never silently disables the guard.
  9. The 245c35ac (7 CI-evaluator tests) and 52ef3d5b (3 mutation-guard tests) test sets are relocated into the substrate’s suite, adapted to JSON-shaped synthetic fixtures (never real tokens), and green — this is the {68.27} release evidence (AC-H2).
  1. add-subtask, add-subtasks (bulk, fold-left), and delete-subtask keep working with today’s observable semantics: auto-id allocation (same next-id behaviour as nextId), withCreateDefaults field defaults on creates, and per-record budget checks in bulk mode.
  2. Auto-id allocation is safe under concurrency: two parallel sessions creating records (tasks, subtasks, backlog items, themes) never both succeed with the same id — the second writer’s allocation re-derives against fresh state (see §H) or fails loudly with duplicate-id.
  3. append-journal is strictly append-only in effect: the prior details bytes are preserved verbatim with the new block appended. Under concurrent writers, an append never resurrects a stale base and never drops a sibling’s interleaved append — the final state contains both blocks.
  4. promote preserves the atomic cross-ledger transaction semantics: validate-everything-first; on any rejection (including unknown-theme for --capability-theme) nothing is written to any ledger; a crash mid-commit can yield at worst a benign transient duplicate, never a lost record; the roadmap third leg remains supported and its linked_tasks[] push remains idempotent.
  5. Discipline soft warnings (the parseTaskListWithWarnings set: budget soft-warns and the 25-Subtask ceiling) keep surfacing in the warnings[] array of mutating-command envelopes.

H. Optimistic concurrency — parallel-session contract (substrate capability; façade 409 semantics)

Section titled “H. Optimistic concurrency — parallel-session contract (substrate capability; façade 409 semantics)”
  1. Every substrate mutation is conditional on the document version the writer read when deriving its intent (the last-seen mtime). A write never lands on top of a version the writer has not seen: on conflict the server rejects (409 mtime-mismatch at the HTTP layer) and nothing is written.
  2. What an orchestrator session sees on conflict: the façade absorbs the conflict with a bounded automatic retry — it re-reads the document, re-derives the invocation’s intent against the fresh state, re-applies the gates, and re-submits. Retry semantics per intent class:
    • field set / status flip — the value is re-applied to the fresh record (last-intent-wins for the same field is acceptable and expected);
    • journal append — the new block is re-appended to the fresh details value (invariant 39);
    • create — duplicate-id and auto-id re-derive against the fresh id-set;
    • delete — a record already removed by the other session surfaces record-not-found, never a false success. A retry never reverts or overwrites another session’s interleaved change.
  3. When retries are exhausted (bound defined at TECH; finite, ≥1), the façade emits the standard failure envelope with error: "mtime-mismatch", exit 1, nothing written, and the invocation is safe to re-run verbatim.
  4. When a retry occurred and the write ultimately succeeded, the success envelope records that fact in warnings[] (including the retry count), so conflict frequency is observable during the Phase 2 soak and beyond.
  5. Two parallel sessions mutating different records (or different fields of the same ledger) both land their writes — possibly via transparent retry — and neither write is lost. The CLI-era silent-clobber hazard is closed and must not regress.
  1. Every successful write regenerates the affected mirrors by default; scoped (touched-record-only) regen is acceptable and preferred, provided the output is byte-identical to what a full --check regen at the pinned tag would produce. --no-regen-mirrors suppresses regen (batch mode). Regen failure after a successful write is a post-write discriminated alert (the mirrorStale / mirrorStaleReason envelope fields), never a rollback of the canonical write.
  2. The ledger-mirror-parity CI gate stays green throughout Phases 1–2, and a single pinned task-view version governs the server, CI (TASK_VIEW_TAG), and the regen cache key — no tag skew between what writes mirrors and what verifies them.
  1. update-umbrella (--add-tasks, --remove-tasks, --reorder) routes through the substrate like the ledgers: umbrellas.json is registered as a known document in the substrate’s document-name registry, validated by UmbrellasSchema, and written via the same gated write path — replacing today’s self-contained raw write that bypasses commitMutation.
  2. Gates apply to umbrella writes: the client-name guard always (invariant 28); the record-set gate with delta for membership edits (add/remove/reorder mutate one umbrella record’s task_ids[]; the umbrella id-set itself is unchanged); budgets only if the registry gains umbrella entries (none exist today — adding them is not required by this Task).
  3. Byte-format alignment: a one-time, isolated normalisation commit converts umbrellas.json to the conforming ledger format — non-ASCII as \uXXXX escapes and stable key order (the trailing newline already conforms — verified by hexdump, 07/06/2026). The commit is content-neutral: the parsed document before and after is deep-equal.
  4. After normalisation, umbrella writes are scoped minimal-diff exactly like ledger writes (invariant 19 applies).
  5. Alignment creates no mirror obligation: umbrellas.json has no mirrors today and this Task does not add any; registration and format are the whole of the alignment.

K. Service model and security posture (G11)

Section titled “K. Service model and security posture (G11)”
  1. The operator never manually manages server instances: lifecycle (start, reuse, health, stop — topology per TECH OQ-2) is transparent behind the façade. A façade invocation when no server is running either succeeds (after transparent startup) or fails loudly with an actionable error — it never hangs and never falls back silently to an ungated write path.
  2. The mutation server is loopback-only: it never binds a non-loopback interface and is never reachable off-host (preserving the substrate’s existing enforced posture).
  3. All in-scope documents are mutable within one session without per-document operator action, and promote spans its two (or three) documents atomically regardless of the chosen server topology.
  1. The docs/reference/task-list-discipline.md contract is preserved end-to-end across the cutover: warn-on-parse / reject-on-write-unless---force for soft-warn fields, record-set preservation on every write, uncapped details journal, and the §2 field shapes. At Phase 3 the doc’s §3 enforcement-point prose is updated to name the substrate (with the façade as the operator surface) — a prose update only; the contract itself does not change.

Gap-coverage map (RESEARCH G1–G11 → invariants)

Section titled “Gap-coverage map (RESEARCH G1–G11 → invariants)”
GapInvariants
G1 byte format18–21
G2 budget gate24–27
G3 record-set gate22–23
G4 client-name guard28–36
G5 subtask CRUD + auto-id37–38
G6 append semantics39, 43
G7 promote third leg40
G8 umbrellas49–53
G9 operator contract10–17
G10 discipline warnings41
G11 multi-ledger service model54–56
(substrate advantage) concurrency42–46
(cross-cutting) phasing + chokepoint1–9; mirrors 47–48; discipline 57

Simplicity review — post-cutover ownership of schemas, primitives, gates (mandated; folds OQ-1 context + OQ-6)

Section titled “Simplicity review — post-cutover ownership of schemas, primitives, gates (mandated; folds OQ-1 context + OQ-6)”

Outcome status: RECOMMENDED — Liam ratifies at PRODUCT review.

(a) Why the bidirectional vendoring existed, in plain language

Section titled “(a) Why the bidirectional vendoring existed, in plain language”

Two repos each needed the other’s code at build time, and there was no shared package registry between them (task-view is a private fork, not a published package):

  • task-view → KH: ledger-cli needed the four patch primitives (atomic-write, detect-schema, patch-apply, record-mutate) in-repo so a KH CLI could mutate the ledgers without any runtime dependency on a second repo or a running server. They were copied into lib/ledger/.
  • KH → task-view: task-view needed the four KH Zod schema sources (task-list-schema, roadmap-schema, backlog-schema, work-status) to validate the ledgers and render the mirrors. They were copied into its packages/schemas/src/.

Because both directions were copies, the task-view-vendor-drift.yml workflow existed purely as a non-blocking reminder that a copy had diverged from its source. The entire arrangement was a workaround for one constraint: avoiding a runtime dependency.

The cutover deliberately removes that constraint — it creates the runtime dependency (a running patch-server) as the whole point of the design. Once the server is the sole writer, in-repo copies of the primitives stop being a convenience and become pure maintenance liability: dead code that the drift workflow exists only to babysit.

KH-side consumers of the four vendored schema sources beyond scripts/ledger-cli.ts (grep sweep, 07/06/2026 — mandated before any deletability claim):

  1. lib/ledger/{detect-schema,patch-apply,record-mutate}.ts — the vendored primitives themselves (deleted with the direct path under any single-source option).
  2. lib/backlog/sort.ts — pure backlog ordering helper (type-only import of BacklogItem); consumed today only by its own test.
  3. lib/validation/retro-schema.ts — retro-ledger schema mirroring the roadmap/backlog document shape (imports roadmap-schema for DocLinkSchema, which in turn imports work-status); the retros ledger leaves the public repo in the same {68.30} subset.
  4. lib/validation/umbrellas-helpers.ts + umbrellas-schema.ts — the umbrellas surface (in scope here).
  5. Maintenance scripts: scripts/{roadmap-from-json,roadmap-to-json,backfill-capability-theme,ledger-sweep-s269}.ts, plus format siblings ledger-renormalise.ts / ledger-normalise-oqls2.ts (which import lib/ledger).
  6. ~18 schema/roundtrip/discipline test files under __tests__/.

No app-runtime code (app/, components/, hooks/, contexts/) imports any of them. Conclusion: the schemas are not immediately deletable from KH — but every remaining consumer exists because the ledger documents live in this repo, and each either operates on the ledgers (scripts, tests) or describes documents that leave at {68.30} (retros, umbrellas, backlog). Deletability is therefore sequenced, not blocked.

Option S1 — status quo (bidirectional vendoring + drift workflow). Keep both copy directions and the reminder workflow after the cutover. Rejected: after Phase 3 the KH-side primitive copies have zero production consumers; we would maintain dead copies, a drift workflow to watch them, and the cognitive overhead Liam is explicitly questioning. The arrangement’s sole rationale (no runtime dependency) no longer holds.

Option S2 — task-view as single source (staged). Schemas, primitives, gates, and scoped serialisation live ONLY in task-view once the server is the sole writer. Staged in two steps to match the cutover’s own sequencing:

  • S2-i (at Phase 3): primitives single-source. lib/ledger/ vendored copies (atomic-write, detect-schema, patch-apply, record-mutate) are deleted along with the direct write path; the KH-authored scoped-serialise.ts and ledger-budgets.ts relocate upstream with the gates (their “KH-only / never-vendor” annotations deliberately revised); the primitive-direction arm of task-view-vendor-drift.yml retires.
  • S2-ii (at/with {68.30}): schemas single-source. When the ledgers leave the public repo for their private home, the schema sources, the maintenance scripts, and the roundtrip tests migrate with them (to task-view and/or the private home per OQ-3); the schema-direction drift arm retires and the workflow is deleted whole.

Option S3 — KH-wrapper (task-view stays generic). task-view keeps only generic patch primitives + server; KH-specific gates (budgets, denylist guard) live in a KH-owned wrapper in front of the server. Rejected: it moves gate enforcement off the chokepoint (violating invariant 9 unless the wrapper becomes the only permitted client — recreating a dual-home problem), keeps two moving parts where one suffices, and protects a genericness we have no second consumer for. We own the fork; speculative generality is not worth the standing cost.

Option S2, staged (S2-i at Phase 3, S2-ii at {68.30}). Rationale: the vendoring existed solely to avoid a runtime dependency that the cutover now embraces by design; we own both repos, so “single source upstream” carries no third-party coordination cost; and the only thing genuinely anchoring schemas in KH is the ledgers themselves, whose departure is already committed ({68.30}). The staging means no consumer is ever orphaned: every KH schema consumer either retires with the direct path (Phase 3) or migrates with the documents ({68.30}).

(d) OQ-6 folded in — CI-twin disposition

Section titled “(d) OQ-6 folded in — CI-twin disposition”

The ledger client-name CI lint (ledger-client-name-guard.yml + scripts/check-ledger-client-name.ts, id68-branch-only) was a public-repo PR lint over committed ledger JSONs. Post-cutover and post-{68.30} its object disappears from the public repo entirely. Disposition (RECOMMENDED): the CI twin is not ported to public KH CI. The substrate’s write-time guard (invariants 28–36) becomes the enforcement point — structurally stronger than a PR lint, since it prevents the bytes from ever being written rather than catching them at review. Whether the ledgers’ private home re-hosts a lint twin in its own CI rides the OQ-3 home decision (Liam + Orchestrator). Interim (Phases 1–2, ledgers still public): the write-time guard is live from the first flag-ON write, and the existing canonical identity-guard.yml continues to cover the repo; the id68-branch guard files remain unmerged per the Inv 42 boundary and are formally superseded at AC-H2.

Simplicity-review invariants (numbered with the rest; ratification pending)

Section titled “Simplicity-review invariants (numbered with the rest; ratification pending)”
  1. After Phase 3, exactly one maintained implementation of each ledger-mutation primitive, each write gate, and the scoped/escape serialisation exists across the two repos — in task-view. KH retains no vendored copy of atomic-write, detect-schema, patch-apply, or record-mutate, and no parallel gate implementation.
  2. ledger-budgets remains plain data (never Zod .max()) wherever it lives, and the relocation of scoped-serialise.ts / ledger-budgets.ts upstream is accompanied by a deliberate revision of their “KH-only / never-vendor” annotations and of the drift workflow’s watched-path list — never a silent divergence.
  3. The task-view-vendor-drift.yml workflow shrinks monotonically with the cutover: the primitive-direction arm retires at Phase 3; the schema-direction arm retires when the schemas leave the public repo; the workflow file is deleted when both arms are gone. At no point does it watch a path that no longer exists.
  4. The client-name CI twin is not ported to public KH CI; the substrate write-time guard is the recorded enforcement point. Any private-home lint twin is decided and recorded with OQ-3, not defaulted into existence.
  5. The schema sources remain canonical in KH until {68.30}, then migrate with the ledgers; each residual KH consumer enumerated in §(b) is explicitly migrated or retired at that point — none is left importing a deleted module, and none silently keeps a stale copy.

  • AC-P0 (Phase 0 exit — upstream ready, zero KH writes yet): a tagged task-view release exists containing the ported gates (record-set, budget, client-name guard), conforming serialisation, and the capability-parity affordances — at minimum: subtask CRUD with auto-id allocation (invariants 37–38), append-journal semantics (invariants 39, 43), and the promote capability-theme third leg (invariant 40); any gap deferred to Phase-1 façade handling must be named explicitly in the Phase-0 exit record; the differential byte-identity proof passes — the same mutation applied to copies of the live ledgers via the current direct path and via the substrate yields byte-identical files (escapes, key order, trailing newline, minimal diff shape; invariant 21); the relocated guard test sets (invariant 36) and gate test ports are green upstream. No KH production ledger has received a substrate write.
  • AC-P1 (Phase 1 exit — façade landed, flag OFF): KH_LEDGER_SERVER exists and defaults OFF; with the flag OFF the full existing CLI test expectations still pass unchanged; with the flag ON in a scratch checkout, a differential parity suite (envelopes, exit codes, error vocabulary, warnings, on-disk bytes, mirror output) passes across the subcommand surface; git diff over .claude/skills/ and .claude/agents/ shows zero consumer edits required (invariant 6); ledger-mirror-parity is green; the pinned tag is bumped consistently across ci.yml (TASK_VIEW_TAG), the regen-mirrors cache key, and the vendor-drift workflow; the stale lib/ledger/README.md pin (OQ-9) is corrected.
  • AC-P2 (Phase 2 exit — default ON, soaked): the flag defaults ON; a soak of ≥1 full SDLC session completes with: scoped one-line-diff shape observed on real writes, ledger-mirror-parity green throughout, zero gate-outcome regressions, at least one conflict-retry exercised and resolved correctly (live, or synthetic via a concurrency test if no live conflict occurs — invariants 42–46), and a rollback drill (flip OFF, verify direct path restored, flip back) recorded.
  • AC-P3 (Phase 3 exit — retired): the direct write path is removed or frozen read-only; the 18 CLI test files are migrated or retired with their coverage accounted for; the consumer-facing docs (task-list-discipline.md §3, lib/ledger/README.md, docs/runbooks/ci.md, skill/agent prose naming the enforcement point) are updated; the umbrellas criteria (AC-U) are met; the simplicity-review invariants 58–61 that fall due at Phase 3 are satisfied.

Hold releases (RESEARCH §4.3, verbatim evidence sets)

Section titled “Hold releases (RESEARCH §4.3, verbatim evidence sets)”
  • AC-H1 — releases the {68.30} Gate-2 ledger-subset hold: 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, with the gates live server-side; (ii) the ledger-cli mutation surface is retired or frozen read-only; (iii) the consuming skill/agent surfaces (RESEARCH §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 the five ledger JSONs + four mirror directories does not break the live writer or CI. Dependency note: item (iv) requires the OQ-3 private-home decision (Liam + Orchestrator); AC-H1 cannot complete before that decision is recorded.
  • AC-H2 — releases the {68.27} ledger-guard holdback: the client-name guard’s functional equivalent is live in the substrate — JSON denylist parsing with per-token case sensitivity, net-new delta semantics, redacted error surfaces, the fail-loud-CI vs inactive-local split, and the override escape hatch (invariants 28–35) — with the 245c35ac/52ef3d5b test set relocated and green in the substrate suite (invariant 36), recorded in the ID-90 journal; the id68-branch guard files are thereby formally superseded and are never merged to canonical (Inv 42 satisfied), per the CI-twin disposition in §Simplicity review (d).
  • AC-U: the one-time normalisation commit lands isolated and content-neutral (invariant 51); umbrellas.json is registered in the substrate’s document registry; a post-normalisation update-umbrella write goes through the gated substrate path, produces a scoped minimal diff, and is guard-protected; the umbrellas roundtrip/helper tests pass against the aligned format.
  • AC-I: this spec and every ID-90 artefact (TECH, PLAN, journal blocks, commit messages, test fixtures) contain zero matches against the configured client denylist; guard test fixtures are synthetic only. The Checker greps against the canonical denylist; one match = FAIL.

#QuestionProduct constraint already fixed by this spec
OQ-2Server lifecycle and topology: long-lived per-session daemon vs spawn-on-demand; one instance per ledger vs a multi-ledger bind extensionInvariants 54–56: operator never manages instances; no-hang/fail-loud; loopback-only; promote atomic across documents regardless of topology
OQ-4append-journal server-side: first-class append operation vs façade read-concatenate-PATCH under the concurrency tokenInvariants 39 + 43: append-only effect, no stale-base resurrection, no lost interleaved appends — must hold under either design
OQ-7Do the denylist’s exclusion_patterns[] apply to ledger byte-delta counting, or are they identity-guard-only carve-outs? (escalate to Liam if semantics stay ambiguous)Invariants 30–32 fix token matching, redaction, and net-new semantics either way
T-1Transitional dual-shape denylist parser (JSON-first with comma-separated fallback) so local .env.local values and the CI secret can migrate independently — adopt or require JSON-only from day one?Invariants 29 + 35: JSON is canonical; set-but-invalid must fail loud
T-2Façade conflict-retry policy parameters (retry count, backoff)Invariants 43–45: bounded, ≥1, observable, exhaustion = mtime-mismatch
T-3Per-request override transport: how --force and the allow-client-name override travel from façade to server (per-request fields vs headers)Invariants 26 + 33: per-invocation scope, never server-global
T-4Under ratified S2: the consumption mechanism for any KH schema consumer that must outlive Phase 3 but precede {68.30} (tag-pinned vendor read vs git dependency), and the exact deletion commit sequencing for lib/ledger/Invariants 58–62
T-5Read-path routing for show/get/schema under flag ON (façade-local read vs GET endpoints)Invariant 15: byte-identical output makes the choice behaviourally invisible
(OQ-1)Gate-hosting split — substantively resolved by the §Simplicity review recommendation (gates upstream in task-view) pending Liam’s ratification at PRODUCT review; TECH confirms the concrete hosting detail under whichever option is ratifiedInvariant 9 (chokepoint) holds under any ratified option
  • Executing {68.30} (the git rm + private re-home) and selecting the private home (OQ-3) — ID-68’s record and a Liam + Orchestrator decision respectively; ID-90 supplies the release evidence (AC-H1) only.
  • A product-retros.json write path — no CLI surface exists today to preserve; the retros ledger is untouched by the cutover and travels with the {68.30} set.
  • Rewriting consumer skills/agents to call the server over HTTP directly; any future de-façade is a separate decision once the substrate is proven as sole writer.
  • Mirror format/content changes, task-view viewer/editor UI work, and publishing task-view as a public package.
  • Mass re-formatting of git history or of documents other than the one-time umbrellas normalisation (invariant 51).