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-subtasksdoes auto-fill its optional subtask fields → the asymmetry is surprising.updatedAtis required in the body and is not auto-stamped on promote — I had to injectdate -umyself. 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 withadd-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.
F3 — cross_doc_links (object[]) has no append affordance
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.
What worked well (keep)
Section titled “What worked well (keep)”--dry-runon 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+ oneregen-mirrors.shat the end is the right batch pattern.schema <ledger>and<command> --helpprinting the field/type/budget slice was enough to author every body without guessing (e.g.cross_doc_linksshape, status enums).