Skip to content

PRODUCT — Ledger mutation CLI (ID-35.2)

Task: ID-35. This Subtask: {35.2} PRODUCT — Behavior invariants + per-subcommand acceptance criteria. Predecessor: RESEARCH.md (10-primitive inventory verified against task-view v0.2.0; VENDOR decision; mirror coupling).

Defines the observable behaviour of bun scripts/ledger-cli.ts. Each invariant is numbered and testable; {35.3} TECH maps a Proposed change to each.


In scope: a headless CLI (bun scripts/ledger-cli.ts <subcommand> [args]) with the 10 subcommands from RESEARCH §1, mutating the three KH workflow ledgers via vendored task-view v0.2.0 primitives; a structured JSON output envelope; a dry-run preview; a mirror-staleness reminder.

Out of scope: mirror generation (owned by regen-mirrors.sh, RESEARCH §4); git commit (owned by the Orchestrator / commit-commands); a long-running server or HTTP loopback; id auto-allocation beyond the deferred --next-id nicety; the ID-34 retroactive sweep.


bun scripts/ledger-cli.ts <subcommand> [args] [--flags]
show <ledger> <id> read one record (ledger: task|roadmap|backlog)
flip-task <taskId> <status> set Task.status
flip-subtask <taskId> <subId> <status> set Subtask.status
append-journal<taskId> <subId> <text> append an <info added on …> block to Subtask.details
add-subtask <taskId> <subtaskJson> append a Subtask to a Task
update-backlog<itemId> <fieldPath> <value> set a field on a backlog item
open-task <taskJson> insert a new Task into task-list
create-backlog<itemJson> insert a new backlog item
delete-backlog<itemId> remove a backlog item
promote <backlogId> <taskJson> move backlog item → new Task (atomic)
Global flags:
--dry-run print the post-mutation document to stdout; write nothing
--pretty human-readable output instead of JSON envelope
--regen-mirrors run scripts/regen-mirrors.sh after a successful write
--ledger-dir <path> override the ledger directory (default docs/reference)

  1. Structured JSON envelope by default. Success → stdout {ok:true, subcommand, …result} with exit 0. Error → stderr {ok:false, error:<machine-code>, detail} with exit 1. The envelope is parseable (the Orchestrator may consume it programmatically). --pretty swaps to human-readable lines but the exit codes are identical. Acceptance: every subcommand prints a valid JSON envelope to the correct stream with the correct exit code.

  2. No silent corrupt write. A mutation is re-parsed against the vendored Zod schema before any byte is written. A schema-parse failure aborts with exit 1 and the original file is byte-unchanged. Acceptance: feeding add-subtask a Subtask that violates the schema leaves the ledger unchanged + exits 1 with error:"schema-error" and the Zod issues in detail.

  3. Atomic single-file writes. Every write goes through write-to-temp + fsync + POSIX rename (the vendored atomicWriteFile); a crash mid-write never leaves a truncated ledger. Acceptance: the write path calls atomicWriteFile; no Bun.write/writeFile directly onto a canonical path.

  4. Unknown ledger / unknown subcommand / missing args fail loudly. A document_name that is not one of the three known values, an unrecognised subcommand, or missing positional args exit 1 with a usage-bearing error — never a partial mutation. Acceptance: detectSchema → kind:"unknown" maps to exit 1; an unknown subcommand prints usage + exits 1.

  5. --dry-run writes nothing. It runs the full mutation in-memory (including the Zod re-parse) and prints the resulting document (or a diff summary) to stdout; the canonical file mtime is unchanged. Acceptance: after --dry-run, the target file’s mtime + bytes are identical.

  1. flip-task / flip-subtask set status via a field patch + re-parse. Status is validated against the schema enum (TaskListStatus / SubtaskStatus); an invalid status exits 1. Acceptance: flip-subtask 35 1 done sets tasks[id=35].subtasks[id=1].status="done"; flip-task 35 not-a-status exits 1 with schema-error.

  2. append-journal appends an <info added on …> block, preserving prior content. It reads the current details, appends a timestamped block per the field-discipline journal shape (ID-34), and writes the whole field back. Append-only — never truncates. Acceptance: prior details content is intact and the new block is present with a valid ISO timestamp wrapper.

  3. add-subtask appends to subtasks[] with the sibling-dep superRefine enforced. A new Subtask whose dependencies[] reference a non-sibling is rejected (the schema’s superRefine runs on re-parse). Acceptance: a valid Subtask appends + re-parses; a cross-Task dep exits 1 with the superRefine message.

  4. open-task / create-backlog insert a whole record; duplicate id is rejected. Uses insertRecord; a record id already present exits 1 with error:"duplicate-id". Acceptance: inserting a fresh id succeeds; re-inserting it exits 1 duplicate-id; the document re-parses (unique-id / sibling-dep superRefines run).

  5. delete-backlog removes by id; absent id is a clean not-found. Uses removeRecord; a missing id exits 1 with error:"record-not-found" and writes nothing. Acceptance: deleting an existing item removes exactly it; deleting an absent id exits 1 not-found, file unchanged.

  6. promote is atomic across two ledgers. Uses the vendored promoteTransaction semantics (validate-everything-first → stage-both → commit-last, ADD side first). On any pre-commit failure BOTH files are left in their pre-transaction state. The backlog item is removed AND the Task inserted, or neither. Acceptance: a successful promote removes the backlog id + inserts the Task; a schema-invalid taskJson leaves BOTH ledgers unchanged + exits 1.

  7. show is read-only. Prints the named record (or error:"record-not-found"); never writes. Acceptance: show task 35 prints the ID-35 record; the file mtime is unchanged.

  1. Mutating commands surface field-discipline warnings. After a successful mutation the CLI runs parseTaskListWithWarnings (task-list) / the roadmap+backlog equivalents and prints any warnings[] to stderr (non-fatal) — so ID-34 drift surfaces to the operator at write time. (Per ID-34 TECH §3, this is the “ID-34 produces inputs the CLI consumes” contract.) Acceptance: mutating a Task whose description is over budget prints the discipline warning to stderr but still exits 0.

  2. Mutating commands print a mirror-staleness reminder. After a successful write the CLI reminds: mirrors are now stale; run bash scripts/regen-mirrors.sh before committing (CI gates on parity). --regen-mirrors runs it automatically. Acceptance: a successful write prints the reminder; --regen-mirrors invokes the script.

  3. The CLI does not commit and does not generate mirrors. It mutates canonical JSON only. Acceptance: no git invocation in the write path; no mirror .md written by the CLI itself (only by the opt-in regen-mirrors.sh).

  1. Vendored primitives are guarded against silent drift. The lib/ledger/ vendored copies are added to .github/workflows/task-view-vendor-drift.yml’s watch set alongside the existing schema files, so a task-view release that changes primitive semantics surfaces a re-vendor reminder. (Parent ratification of OQ-35-5, S267.) Acceptance: the workflow’s path filter / watch list includes lib/ledger/.

§4 Self-bootstrap acceptance (first dogfood)

Section titled “§4 Self-bootstrap acceptance (first dogfood)”

Per the cmux brief + ID-35 description: the first real use of the CLI is to open ID-35 itself via Promote — but ID-35 already exists as a Task (not a backlog item), so the literal Promote dogfood is re-framed as: the CLI’s own integration test exercises the full round-trip (open-task → flip-subtask → append-journal → show) against a temp-copied ledger fixture, proving the primitives wire correctly end-to-end. The ledger status updates for ID-35’s own subtasks are performed via the CLI where practical (dogfood), demonstrating the tool on its own Task record.


InvAssertionVerified by
1,4JSON envelope + exit codes + loud failureper-subcommand unit tests
2no silent corrupt writeschema-invalid mutation leaves file unchanged
3,15atomic write; no commit/mirror in write pathcode inspection + integration test
5dry-run writes nothingmtime/bytes unchanged
6–12per-subcommand correctnessunit + integration tests on temp fixtures
13discipline warnings surfacedover-budget mutation prints warning, exits 0
14mirror reminderstdout/stderr content check
16vendor-drift guard extendedworkflow path-filter grep

≥12 numbered invariants (16 here), each with a Checker-verifiable acceptance criterion; honours all RESEARCH ratifications + the parent’s S267 vendor-drift add-on.