Skip to content

Ledger-CLI friction log — S299 enactment session

Ledger-CLI friction log — S299 enactment session

Section titled “Ledger-CLI friction log — S299 enactment session”

Friction points hit while driving bun scripts/ledger-cli.ts through the S299 mutation plan (~30 ledger ops). Captured per Liam’s request — for improving my (the agent’s) ledger UX, not Liam’s. Each is an UX/ergonomics gap, not a correctness bug; every op ultimately succeeded.

F1 — promote / open-task require a COMPLETE task record (no auto-fill, no auto-stamp)

Section titled “F1 — promote / open-task require a COMPLETE task record (no auto-fill, no auto-stamp)”

Passing the load-bearing fields (id/title/description/status/priority/dependencies/subtasks/ effort_estimate) was not enough. promote --file rejected until I also supplied updatedAt, owner:null, priority_note:null, status_note:null, cross_doc_links:[], session_refs:[], commit_refs:[].

  • add-subtasks does auto-fill its optional subtask fields → the asymmetry is surprising.
  • updatedAt is required in the body and is not auto-stamped on promote — I had to inject date -u myself. Every other ledger write would be expected to stamp this.
  • Ask: promote/open-task should auto-fill nullable→null, array→[], and auto-stamp updatedAt (parity with add-subtasks), so the caller supplies only meaningful fields.

F2 — schema-error output is a raw Zod dump keyed by ARRAY INDEX, not record id

Section titled “F2 — schema-error output is a raw Zod dump keyed by ARRAY INDEX, not record id”

The F1 rejection pointed at path:["tasks",58,"updatedAt"] — index 58 in the array, not ID-70. To know which record failed I would have to count array position. A friendlier message would name the record id and list just the missing field names.

Section titled “F3 — cross_doc_links (object[]) has no append affordance”

--append only works on notes. To add ONE {path,anchor,raw} entry to an item that already has links, I must read the existing array, merge, and overwrite the whole field (clobber risk). For two items (bl-74, bl-75) I deliberately skipped the structured link and relied on a notes-append instead. Ask: an append mode for object[] fields, or a dedicated add-cross-doc-link <id> <path> <anchor> <raw>.

F4 — no first-class “fold / register a backlog item to a Task” operation

Section titled “F4 — no first-class “fold / register a backlog item to a Task” operation”

The S299 plan’s core motif is “register this backlog item to a not-yet-specced Task, consume at spec time.” There is no verb for it, so I hand-rolled the same concept three different ways: (a) notes --append naming the target, (b) a cross_doc_links entry, (c) appending a fold-in list to the target Task’s description. And for a deleted item (bl-135) the register had nowhere structured to live at all — it only survives as prose in ID-46/ID-61 descriptions. Ask: a register <backlogId> <taskId> [note] (and/or fold) verb that writes a structured backref on both sides, so “carry this across to spec time” is one op.

F5 — two ways to set Task status; the plan’s verb form was not the canonical one

Section titled “F5 — two ways to set Task status; the plan’s verb form was not the canonical one”

The mutation plan wrote update-task 53 status done, but the dedicated verb is flip-task 53 done (and update-task <id> <field> <value> is the generic field editor). I used flip-task. Minor, but the redundancy invites the wrong guess. Ask: either document flip-task as canonical for status in update-task --help, or alias them explicitly.

F7 — no --file for field-value edits → long inline strings fail silently

Section titled “F7 — no --file for field-value edits → long inline strings fail silently”

update-task <id> description "<long text>" / update-backlog <id> notes "…" accept the value ONLY as a positional shell arg. A long, multi-clause description passed inline this session silently no-op’d (the shell mis-parsed the ~1KB double-quoted arg; the command produced no "ok" line and the field was not written — only discovered via a post-write jq re-read). The workaround was update-task 61 description "$(cat file)". The record-CREATING commands already accept --file <path> / --file - (stdin); the field-EDIT commands do not. Ask: extend --file/stdin input to update-task / update-backlog field-value edits, so large field bodies need not be inlined. (Also: a silent no-op on a malformed arg is itself a footgun — a non-zero exit / explicit error would have surfaced it immediately.)

F6 — batch ergonomics: per-call mirror-stale reminder is noisy

Section titled “F6 — batch ergonomics: per-call mirror-stale reminder is noisy”

With --no-regen-mirrors (correct for a 30-op batch), every single call prints a 3-line “mirror regen suppressed … run regen-mirrors.sh before committing” banner. Across ~30 ops that is ~90 lines of identical reminder. Ask: collapse the suppressed-mirror reminder to one line, or emit it once at process exit rather than per-op.


  • --dry-run on every record-creating op caught F1 before any real write — excellent.
  • scoped/minimal-diff being the default kept the JSON diffs clean.
  • --no-regen-mirrors + one regen-mirrors.sh at the end is the right batch pattern.
  • schema <ledger> and <command> --help printing the field/type/budget slice was enough to author every body without guessing (e.g. cross_doc_links shape, status enums).