Skip to content

Ledger-CLI v2 — fix-wave Subtask breakdown ({35.13}+)

Ledger-CLI v2 — fix-wave Subtask breakdown ({35.13}+)

Section titled “Ledger-CLI v2 — fix-wave Subtask breakdown ({35.13}+)”

Status: Plan of record. Authored: {35.12} RESEARCH round (S271, Task Planner) via planning-and-task-breakdown. Derived from: RESEARCH.md (the gap+fix spec of record). For: the orchestrator to write the Subtask records into docs/reference/task-list.json under Task ID-35.

Parent Task: ID-35 (Ledger mutation CLI). Existing subtasks 1–12 (ids contiguous, max 12) → this wave starts at subId 13. 13 new subtasks (13–25); total 25, at the soft ceiling.

Amendment (S271, parent directive + checker pass). A parallel backlog-integrity audit surfaced a record-set-preservation drop guard (RESEARCH §2.6) — the most severe wrong-shape write (a silently dropped record). It is landed as a distinct foundation write-gate subtask {35.16} (sibling of the budget pre-check {35.17}), and every create/delete/update/backfill command depends on it. This was a separate-subtask (not a fold-into-budget) choice because there was room under the ceiling (13 new subtasks → 25 total, exactly the ceiling) and the guard is a structurally distinct invariant (collection membership) from budget enforcement (field length); conflating their acceptance criteria would weaken both. {35.16}‘s insertion renumbered the former {35.16}–{35.24} to {35.17}–{35.25} and rewired the dependency arrays accordingly. Also folded: the {35.22} backfill id-set assertion (RESEARCH §2.6 applied to the 149-item backlog), the {35.22} discoverability dep correction, and the {35.21} (now {35.22}) auto-id testStrategy property fix (max+1, not a literal id).


Architecture decisions (full rationale → RESEARCH.md)

Section titled “Architecture decisions (full rationale → RESEARCH.md)”
  • Prevent-at-source, CLI-layer enforcement. Budget gating is a CLI pre-write check, NOT a schema .max() (which would reject the live ledger + diverge the vendored schema; RESEARCH §2.3, §7).
  • deps number/string: functional-keep. TM mandate, verified empirically; the fix is discoverability, not migration (RESEARCH §3). Cross-Task unification is an Open Question, escalated separately.
  • Backlog title: optional string ≤80, schema + CLI this wave; mirror H1 render is task-view fork (ID-20), OUT OF SCOPE (RESEARCH §6).
  • Auto-id lives in the CLI (max+1), not the vendored insertRecord (RESEARCH §2.2).
  • Zero new deps — extend the hand-rolled parseArgs (RESEARCH §2.4).

Shared-hotspot map (parallel-vs-serialise)

Section titled “Shared-hotspot map (parallel-vs-serialise)”
FileSubtasks touching itParallelism
lib/validation/ledger-budgets.ts (+ *-schema.ts helpers)13foundation, parallel-safe vs CLI
lib/validation/backlog-schema.ts14parallel-safe vs CLI (own file)
scripts/ledger-cli.ts (SHARED HOTSPOT)15, 16, 17, 18, 19, 20, 21, 22MUST SERIALISE — one writer at a time
backlog data (product-backlog.json) via CLI23fan-out across 3–4 executors, disjoint id-ranges
__tests__/scripts/ledger-cli*, __tests__/lib/ledger/*24after commands land
docs (task-list-discipline.md, READMEs, this spec)25after commands land

Rule of thumb: anything editing scripts/ledger-cli.ts (15–22) serialises on that file. The recommended order is 13/14 (parallel foundation) → 15 → 16 (record-set guard) → 17 (budget) → 18 → {19, 20} → 21 → 22 → {23} → {24, 25}. The two write-gate guards {35.16}+{35.17} land before any new command so every create/delete/update path commits through both. The dependency arrays below encode the hard constraints; the orchestrator dispatches CLI subtasks one-at-a-time even where deps would technically allow overlap, because they share the file.


All ids are integers local to Task 35. dependencies are sibling-only integer arrays (other subtasks of Task 35). status: "pending". details is the load-bearing dispatch brief; rationale lives in RESEARCH.md (cited, not inlined). testStrategy is a one-line single assertion.

{35.13} — Unified 3-ledger budget registry

Section titled “{35.13} — Unified 3-ledger budget registry”
  • description: Introduce a single budget registry mapping (ledger, recordKind, field) to a char budget, covering task-list, roadmap and backlog fields, and wire all three parse-with-warnings helpers to read it.
  • details: Per RESEARCH §4.1. Create lib/validation/ledger-budgets.ts exporting a registry keyed by record kind (task / subtask / theme / item) → field → budget. Seed with the existing task-list numbers (taskDescription 1500, taskStatusNote 300, subtaskDescription 250, subtaskTestStrategy 300) and add roadmap (theme description/notes) + backlog (description; title budget added by {35.14}) entries. Do NOT budget subtask.details (append-only journal). Re-point parseTaskListWithWarnings (task-list-schema.ts L207 FIELD_BUDGETS) and the scripts/ledger-sweep-s269.ts consumer at the registry (re-export FIELD_BUDGETS from it or superset). Add parseBacklogWithWarnings / extend parseRoadmapWithWarnings to emit budget warnings from the registry. NO schema .max() — registry is data, not a Zod constraint (RESEARCH §2.3/§7).
  • testStrategy: bun run test — registry exports all three ledgers’ field budgets and the three parse-with-warnings helpers emit a budget warning for an over-budget field of each kind.
  • dependencies: []
  • description: Add an optional title field to BacklogItemSchema and register its 80-char budget, keeping the live 149-item ledger parseable.
  • details: Per RESEARCH §6.1. In lib/validation/backlog-schema.ts add title: z.string().min(1).optional() positioned first after id (heading convention). MUST be optional — all 149 live items lack it. BacklogItemSchema is NOT .strict() so this is non-breaking, and patch-apply picks it up via BACKLOG_ITEM_KNOWN_FIELDS (so update-backlog <id> title works with no walker change — RESEARCH §1.1). Add the title ≤80 budget to the registry from {35.13}. description stays required (min(1)). Note in a code comment that this trips the non-blocking task-view-vendor-drift.yml reminder (expected, RESEARCH §7).
  • testStrategy: bun run test — current 149-item product-backlog.json still parses, and an item with a title field both parses and exposes a registry budget of 80.
  • dependencies: [13]

{35.15} — CLI input layer: --file/stdin, named flags, auto-id, reject-unknown-flags

Section titled “{35.15} — CLI input layer: --file/stdin, named flags, auto-id, reject-unknown-flags”
  • description: Extend the CLI arg parser with JSON-from-file/stdin input, named value-flags, a max+1 auto-id helper, and unknown-flag rejection, as the shared foundation every new command builds on.
  • details: Per RESEARCH §2.2/§2.4/§5.3. In scripts/ledger-cli.ts: (1) extend hand-rolled parseArgs with a value-flag set (--file, --id, --title, --description, --status, --depends, --priority, …) that consumes the next argv token; reject unknown flags (exit 1, list known). (2) Add readRecordInput() resolving precedence positional-JSON → --file <path> (- = stdin) → named-flags-object. (3) Add nextId(detected, collectionKey) returning max(ids)+1 as the correct primitive type (string for tasks/themes/items, number for subtasks). ZERO new deps (Node ≥22; no commander). Do not change any command behaviour yet — pure plumbing other subtasks consume.
  • testStrategy: bun run testreadRecordInput yields an identical record object from positional JSON, --file, and equivalent named flags; an unknown flag exits non-zero.
  • dependencies: []

{35.16} — Record-set-preservation write gate (the drop guard)

Section titled “{35.16} — Record-set-preservation write gate (the drop guard)”
  • description: Add a CLI write-gate assertion that the post-write id-set and count equal the pre-write set under the intended delta, rejecting any silent record drop or duplicate before bytes land.
  • details: Per RESEARCH §2.6 (highest-severity prevent-at-source guard). In scripts/ledger-cli.ts add assertRecordSet(beforeIds, afterIds, expectedDelta) run at the write gate of every mutating path — commitMutation (scoped AND whole-file) + promote (twice, once per ledger). beforeIds = collection id-set at loadLedger time; afterIds = id-set parsed from the bytes about to be written (parse the scopedSerialise/escapeSerialise output string, NOT the in-memory detected.data, so a serialise-side drop is caught). expectedDelta: field-edit/journal = ∅; create = +1 (the one new id); delete = −1; promote = task +1 AND backlog −1. Mismatch → {ok:false, error:'record-set-violation', detail:'<ledger>: missing [..] / unexpected [..]'}, exit 1, write nothing. Composes with — does NOT replace — the {35.17} budget gate; both must pass before atomicWriteFile/commitStagedWrite. Add to USAGE error list.
  • testStrategy: bun run test — a write whose serialised output is missing one pre-existing id is rejected record-set-violation (exit 1, no bytes written), while a normal field-edit (∅ delta) and a normal create (+1) both pass.
  • dependencies: [15]

{35.17} — CLI write-time budget pre-check + --force

Section titled “{35.17} — CLI write-time budget pre-check + --force”
  • description: Gate every mutating command on a pre-write budget check of the single changed record, rejecting over-budget writes unless --force, with an inline record-scoped message.
  • details: Per RESEARCH §2.3 (north star). In scripts/ledger-cli.ts, after the in-memory mutation but BEFORE atomicWriteFile, check the changed record’s budgeted fields against the registry ({35.13}). Over-budget → {ok:false, error:'budget-exceeded', detail:'<field> is <N> chars (budget <B>) on <ledger> <id>'}, exit 1, write nothing. --force downgrades to the existing soft warning + proceeds. Message is scoped to the changed record — NEVER the whole-ledger parseTaskListWithWarnings dump. Wire into commitMutation + promote, alongside the {35.16} record-set gate (both run at the same write-gate layer; record-set is the membership invariant, budget is the field-length one). subtask.details is exempt.
  • testStrategy: bun run test — a 789-char subtask description is rejected (exit 1, no write) without --force and written with --force.
  • dependencies: [13, 15]

{35.18} — Mirror regen default-on + fail-loud

Section titled “{35.18} — Mirror regen default-on + fail-loud”
  • description: Make mirror regeneration the default after every write, add --no-regen-mirrors to opt out, and surface a loud warning if regen fails.
  • details: Per RESEARCH §2.5. In scripts/ledger-cli.ts flip the default: commitMutation / promote call maybeRegenMirrors(true) unless --no-regen-mirrors. Add the --no-regen-mirrors flag (parser from {35.15}); remove reliance on --regen-mirrors opt-in (keep it as a harmless alias for back-compat). regen-mirrors.sh non-zero exit → loud stderr warning (write already committed, so post-write alert, not rollback). Update MIRROR_REMINDER/USAGE text accordingly.
  • testStrategy: bun run test — a mutating command with no mirror flag invokes regen by default, and --no-regen-mirrors suppresses it.
  • dependencies: [15]

{35.19} — update-subtask command (gap 1, highest value)

Section titled “{35.19} — update-subtask command (gap 1, highest value)”
  • description: Add update-subtask <taskId.subId> <field> <value> to edit any subtask field via the keyset-guarded walker with scoped-write and both write-gate guards.
  • details: Per RESEARCH §2.1. In scripts/ledger-cli.ts add update-subtask accepting a dotted taskId.subId; build FieldPatch ['tasks', taskId, 'subtasks', String(subId), field]; run fieldPatchMutation + commitMutation with --scoped available exactly as flip-subtask. Value parse: field-type-aware coercion driven by the SubtaskSchema field type (so dependencies parses as number[], description stays a string even if it looks like JSON — RESEARCH §5.3); the keyset guard (SUBTASK_KNOWN_FIELDS) rejects unknown fields. Commits through the {35.16} record-set gate (∅ delta) + {35.17} budget pre-check (description/testStrategy). Add to USAGE.
  • testStrategy: bun run testupdate-subtask 35.1 status done flips the field with a scoped 1-line diff and re-parses; an unknown field exits non-zero.
  • dependencies: [15, 16, 17]

{35.20} — Task / roadmap field-editors + create-theme

Section titled “{35.20} — Task / roadmap field-editors + create-theme”
  • description: Add update-task, update-roadmap field-editors and a create-theme command so every record kind has non-status field editing and roadmap gains create+edit (it has none today).
  • details: Per RESEARCH §4. In scripts/ledger-cli.ts: (1) update-task <taskId> <field> <value> (FieldPath ['tasks', taskId, field]; e.g. status_note, description, priority — keyset TASK_KNOWN_FIELDS). (2) update-roadmap <themeId> <field> <value> (FieldPath ['themes', themeId, field], mirror of update-backlog; keyset ROADMAP_THEME_KNOWN_FIELDS). (3) create-theme <json> via insertRecord (mirror of create-backlog). All use field-type-aware value coercion and commit through the {35.16} record-set gate (∅ for edits, +1 for create-theme) + {35.17} budget pre-check. Add to USAGE. Roadmap has 11 themes (under 12 ceiling).
  • testStrategy: bun run testupdate-roadmap edits a theme field, update-task edits a task field, and create-theme inserts a theme; each re-parses and rejects an unknown field.
  • dependencies: [15, 16, 17]

{35.21} — Auto-id wiring + field-type-aware coercion on create/update-backlog

Section titled “{35.21} — Auto-id wiring + field-type-aware coercion on create/update-backlog”
  • description: Wire the max+1 auto-id helper into all record-creating commands and replace update-backlog’s silent JSON.parse heuristic with field-type-aware coercion.
  • details: Per RESEARCH §2.2/§5.3. In scripts/ledger-cli.ts, when a record-creating command (add-subtask, open-task, create-backlog, create-theme) omits id (named-flag path) or --id is absent, inject nextId(...) from {35.15} (subtask → number, others → string). --id <n> forces an explicit id (still duplicate-checked by insertRecord). Replace update-backlog’s JSON.parse(value)-then-bare-string fallback with type-aware coercion driven by the BacklogItemSchema field type (so update-backlog 100 description "123" stays string “123”). Apply the same coercion to the {35.19}/{35.20} update commands. Auto-id-created records still commit through the {35.16} record-set gate (+1 delta).
  • testStrategy: bun run testadd-subtask 35 --title X assigns max(existing subId)+1 and create-backlog --title Y assigns max(existing item id)+1; update-backlog 100 description "123" keeps “123” a string.
  • dependencies: [15, 16, 20]

{35.22} — Discoverability: schema, per-subcommand --help, get

Section titled “{35.22} — Discoverability: schema, per-subcommand --help, get”
  • description: Add a schema [ledger|kind] subcommand printing each field’s name+type+budget, a per-subcommand --help, and a get <ledger> <id> [field] single-field read.
  • details: Per RESEARCH §5.1/§5.2 (the “prevent guessing” fix). In scripts/ledger-cli.ts: (1) schema [ledger|recordKind] prints per record kind every field name + Zod type (sourced from Schema.shape) + budget (registry {35.13}) + required/optional + enum values (from lib/validation/work-status.ts), so subtask.dependencies: number[] (sibling-only) / task.dependencies: string[] / subtask.description: string ≤250 / backlog.title: string ≤80 are explicit. (2) Per-subcommand --help prints that command’s flags + its target record’s schema slice (replaces today’s bare-USAGE fall-through). (3) get <ledger> <id> [field] extends show with single-field reads. Add all to USAGE. (Per-subcommand --help must enumerate the new commands → depends on them existing.)
  • testStrategy: bun run testschema task lists task.dependencies as string[] and subtask.dependencies as number[]; get backlog 100 status prints just the status.
  • dependencies: [13, 14, 15, 19, 20, 21]

{35.23} — Backlog title backfill (FAN OUT across 3–4 executors)

Section titled “{35.23} — Backlog title backfill (FAN OUT across 3–4 executors)”
  • description: Backfill a concise ≤80-char title for all 149 backlog items via update-backlog, fanned out across 3–4 executors on disjoint id-ranges, with a pre-commit id-set assertion.
  • details: Per RESEARCH §6.4 + §2.6. ORCHESTRATOR FANS OUT across 3–4 parallel executors on disjoint id-ranges (ids sorted then quartered, ~37–38 items each). Each executor, for each item in its range, reads the item description and writes a concise heading-essence title (≤80 chars) via update-backlog <id> title "<title>" (budget-enforced by {35.17}, so >80 is rejected at source; record-set-guarded by {35.16}, ∅ delta per edit — dogfoods both gates). Do NOT over-engineer: no LLM batch, no title-generation heuristic — a human-/agent-written short heading per item. Before the orchestrator commits the backfill, assert the backlog id-set + count are IDENTICAL pre/post — only title fields added, ZERO records dropped or reordered-out. Canonical state: 149 records, max id 185, ids non-contiguous (gaps are legitimate promotions / never- allocated pre-migration ids, NOT losses). Orchestrator runs bash scripts/regen-mirrors.sh once after all ranges complete. Mirror H1 still renders description until the task-view fork ships (ID-20, OUT OF SCOPE — RESEARCH §6.3).
  • testStrategy: bun run test (+ jq) — after backfill the backlog id-set is identical pre/post (149 records, max id 185, no id added or dropped) and every item carries a non-empty title ≤80 chars.
  • dependencies: [14, 16, 17]

{35.24} — Test coverage for the v2 command surface

Section titled “{35.24} — Test coverage for the v2 command surface”
  • description: Extend the existing ledger-cli test suites to cover every new command, both write-gate guards, auto-id, and the three input modes.
  • details: Per docs/reference/test-philosophy.md (verify real behaviour, not implementation). Extend __tests__/scripts/ledger-cli.test.ts + ledger-cli-integration.test.ts (and __tests__/lib/ledger/scoped-serialise.test.ts if scoped paths change). Cover: update-subtask / update-task / update-roadmap / create-theme happy + unknown-field paths; record-set- violation reject (exit 1, no write) on a dropped/duplicated id across scoped AND whole-file paths; budget-exceeded reject + --force write; auto-id max+1 for subtask (number) and backlog/theme (string); --file/stdin/named-flag equivalence; reject-unknown-flag; mirror-regen default-on flag wiring; schema/get output. Use bun run test (NOT bun test).
  • testStrategy: bun run test — the ledger-cli suites pass with new cases covering all v2 commands, both write-gate guards, auto-id, and input modes.
  • dependencies: [16, 19, 20, 21, 22]

{35.25} — Docs: discipline + CLI usage + v2 spec close-out

Section titled “{35.25} — Docs: discipline + CLI usage + v2 spec close-out”
  • description: Update the field-discipline doc, ledger-cli usage/README, and the v2 spec to document the new command surface, both write-time guards, and the backlog title field.
  • details: Update docs/reference/task-list-discipline.md (note CLI now REJECTS over-budget AND record-set-violating writes at write time, not just warns; add backlog title ≤80 row), the scripts/ledger-cli.ts header USAGE block + lib/ledger/README.md (new commands, the record-set + budget write gates, input modes, auto-id, regen default-on), and note in this spec dir that the task-view mirror renders title as H1 only after a future ID-20 fork release. Reference RESEARCH.md as the spec of record. UK English; no schema .max() claims.
  • testStrategy: bun run testdoc-freshness.test.ts and any mirror/doc guard tests pass after the doc updates.
  • dependencies: [19, 20, 21, 22]

Sibling-only dependency check (forcing function — PASSED)

Section titled “Sibling-only dependency check (forcing function — PASSED)”

Every dependencies[] above references only other subtasks of Task 35 (ids 13–25, all ≤ the referencing id — no forward deps). No cross-Task subtask dependency surfaced. The two cross-Task concerns (id-type unification; task-view mirror fork) are handled as scope boundaries / Open Questions (RESEARCH §3, §6.3), NOT as subtask deps — so the constraint is honoured without bending.

25-subtask ceiling check (PASSED — AT the ceiling)

Section titled “25-subtask ceiling check (PASSED — AT the ceiling)”

Task 35 had 12 subtasks; this wave adds 13 (13–25, incl. the {35.16} record-set guard) → 25 total, exactly at the 25 soft ceiling. No split needed, but there is no further headroom under this Task. The optional generic set / list polish (RESEARCH §5.1/§5.3) would push past 25 → it MUST become a follow-on Task, not a 26th subtask here. Any further ID-35 expansion is also a split signal.

Open Questions (for the orchestrator to route — NOT decided here)

Section titled “Open Questions (for the orchestrator to route — NOT decided here)”
  1. id-type unification (cross-Task). RESEARCH §3 recommends functional-keep (TM mandate). If Liam wants unification anyway, it is a separate Task touching the task-view fork (ID-20), lib/validation/*, lib/ledger/* walkers, all mirrors, and external consumers — must NOT fold into this wave.
  2. task-view mirror title H1 render (cross-Task, ID-20). Rendering backlog title as the mirror H1 needs an ID-20 fork release; this wave ships only the schema field + CLI surfacing.
  3. Budget-registry home. RESEARCH §4.1 recommends a new lib/validation/ledger-budgets.ts over enlarging the vendored task-list-schema.ts FIELD_BUDGETS delta; either is correct — the {35.13} executor confirms.
  4. Generic set / list (optional polish). Named commands close the gaps; a fully generic set + a list are optional and would push past the 25 ceiling → defer to a follow-on Task.