Skip to content

fix: prevent silent backlog body loss in tasks wrapper - #208

Merged
dnth merged 4 commits into
mainfrom
fm/fm-tasks-axi-append-note
Oct 5, 2026
Merged

dnth merged 4 commits into
mainfrom
fm/fm-tasks-axi-append-note

Conversation

@dnth

@dnth dnth commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Intent

Prevent silent loss of a backlog item's body. On 2026-10-05, adding evidence to two items with bin/fm-tasks-axi.sh edit <id> --body-file <file> replaced both bodies with only the new text, because the preceding read (show --json) printed nothing; the originals had to be recovered from session logs.

Requirements:

  • Investigate what tasks-axi already supports (tasks-axi --help, edit --help, update --help; AGENTS.md section 10 mentions update --body-file and --archive-body) and make the safe path the easy one in bin/fm-tasks-axi.sh, the wrapper this repo owns.
  • Add an append operation that preserves the prior body (fm-tasks-axi.sh append-note <id> --body-file <f>, also --body <text>).
  • Make a whole-body replace through the wrapper (update/edit with --body/--body-file) refuse when the new body would drop a non-empty prior body, unless an explicit flag is passed; the chosen flag is --archive-body so the old text stays recoverable in tasks-axi's note archive. A replace whose new text still contains the prior body, or a replace of an empty body, proceeds unflagged (deliberate: those cannot lose text).
  • Do not modify the upstream tasks-axi tool itself; if a needed primitive is missing upstream, say so in the PR. Known upstream gaps: tasks-axi has no append verb, and show/list reject --json, so the wrapper reads the prior body from show --full and fails closed when that read cannot be parsed. node is used to decode the JSON-quoted body line because jq is optional.
  • Update the guidance that referenced the old replace-only flow (AGENTS.md backlog-notes line, docs/architecture.md /stow paragraph, .agents/skills/stow) to point at append-note or --archive-body, with the script header as the single owner of the contract.

Acceptance criteria:

  • AC1: regression test: append keeps the prior body intact and adds the new text.
  • AC2: regression test: a replace through the wrapper that would drop a non-empty prior body refuses without the explicit flag, and with the flag the prior body is archived (or otherwise recoverable).
  • AC3: changed tests green via bin/fm-test-run.sh --changed, FM_LINT_JOBS=1 bin/fm-lint.sh clean, and the PR's full GitHub CI suite green.

Firstmate-Validation-Generation: 2716322c0d01807c3f7439b538717213

What Changed

  • Add append-note <id> --body <text> or --body-file <path> to preserve the existing body, append text, and verify the stored result. The wrapper uses show --full because upstream has no append verb or JSON body-read support, and refuses writes when it cannot parse the body.
  • Guard update and edit body replacements, including task aliases: dropping existing text requires --archive-body; preserving replacements and empty bodies proceed unflagged.
  • Update backlog-note guidance to reference the wrapper header, and add regression coverage registered with the test runner.

Risk Assessment

High-impact surface (every backlog note write goes through this wrapper). The round-1 review bypass (flags before the ID, and task update/task edit) was fixed in the review step and is covered by regression tests.

Known limitation: append-note and the replace guard read the body and then write it as separate steps, so two concurrent writers to the same item could still lose a note; this is accepted because backlog mutations are already serialized by the backlog lease, and closing it fully would need an append or conditional-update primitive that upstream tasks-axi does not provide.

Testing

Focused real-CLI regressions and independent manual commands passed against isolated backlogs, with body and archive transcripts captured. One archive-format assertion was corrected and re-run successfully. Lint, broad regression, and CI remain with their assigned phases.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Append inline or relative-file notes and preserve the existing multiline body ✅ pass live live-cli-transcript.log; targeted-cli-tests.log
Replace a non-empty body without archiving and receive a refusal with the body unchanged ✅ pass live live-cli-transcript.log; targeted-cli-tests.log
Try replacement with flags before the ID and task update/edit aliases; preservation guards still apply ✅ pass live live-cli-transcript.log; targeted-cli-tests.log
Replace with --archive-body and recover the complete previous body from the archive ✅ pass live live-cli-transcript.log, including persisted note-archive.md
Replace while retaining prior text, or populate an empty body, without requiring an archive ✅ pass live live-cli-transcript.log; targeted-cli-tests.log
Submit missing IDs, empty notes, unreadable files, or removed append options and leave the backlog unchanged ✅ pass live live-cli-transcript.log; targeted-cli-tests.log
Evidence: Live CLI commands, persisted bodies, and recoverable archive
$ fm-tasks-axi.sh ['add', 'live-note', 'Evidence validation', '--kind', 'ship', '--body', 'Original decision: preserve "quoted" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab']
ok: added live-note (ship) -> Queued
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab"
help[2]:
  - Run `tasks-axi start live-note` to move it to in flight
  - Run `tasks-axi block live-note --by <other>` to record a dependency
Exit: 0
$ fm-tasks-axi.sh ['append-note', 'live-note', '--body', 'Inline follow-up']
ok: updated live-note (body)
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up\n"
help[1]:
  - Run `tasks-axi show live-note --full` to see the result
Exit: 0
$ fm-tasks-axi.sh ['show', 'live-note', '--full']
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up"
Exit: 0
$ fm-tasks-axi.sh ['append-note', 'live-note', '--body-file', 'note.md']
ok: updated live-note (body)
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up\n\nFile evidence\nAnother line\n"
help[1]:
  - Run `tasks-axi show live-note --full` to see the result
Exit: 0
$ fm-tasks-axi.sh ['show', 'live-note', '--full']
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up\n\nFile evidence\nAnother line"
Exit: 0
$ fm-tasks-axi.sh ['update', '--json', 'live-note', '--body', 'replacement']
fm-tasks-axi: refusing to replace the non-empty body of live-note: the new body does not contain the existing text. Use append-note to add text, or re-run with --archive-body to replace it while archiving the old body.
Exit: 2
$ fm-tasks-axi.sh ['show', 'live-note', '--full']
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up\n\nFile evidence\nAnother line"
Exit: 0
$ fm-tasks-axi.sh ['update', '--json', 'live-note', '--body-file', '~/.no-mistakes/worktrees/dd71c22cc6d7/01M45ZSSSP3DRQHQH241NPQ537/.test-append-validation/caller/note.md']
fm-tasks-axi: refusing to replace the non-empty body of live-note: the new body does not contain the existing text. Use append-note to add text, or re-run with --archive-body to replace it while archiving the old body.
Exit: 2
$ fm-tasks-axi.sh ['show', 'live-note', '--full']
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up\n\nFile evidence\nAnother line"
Exit: 0
$ fm-tasks-axi.sh ['edit', '--json', 'live-note', '--body', 'replacement']
fm-tasks-axi: refusing to replace the non-empty body of live-note: the new body does not contain the existing text. Use append-note to add text, or re-run with --archive-body to replace it while archiving the old body.
Exit: 2
$ fm-tasks-axi.sh ['show', 'live-note', '--full']
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up\n\nFile evidence\nAnother line"
Exit: 0
$ fm-tasks-axi.sh ['edit', '--json', 'live-note', '--body-file', '~/.no-mistakes/worktrees/dd71c22cc6d7/01M45ZSSSP3DRQHQH241NPQ537/.test-append-validation/caller/note.md']
fm-tasks-axi: refusing to replace the non-empty body of live-note: the new body does not contain the existing text. Use append-note to add text, or re-run with --archive-body to replace it while archiving the old body.
Exit: 2
$ fm-tasks-axi.sh ['show', 'live-note', '--full']
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up\n\nFile evidence\nAnother line"
Exit: 0
$ fm-tasks-axi.sh ['task', 'update', '--json', 'live-note', '--body', 'replacement']
fm-tasks-axi: refusing to replace the non-empty body of live-note: the new body does not contain the existing text. Use append-note to add text, or re-run with --archive-body to replace it while archiving the old body.
Exit: 2
$ fm-tasks-axi.sh ['show', 'live-note', '--full']
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up\n\nFile evidence\nAnother line"
Exit: 0
$ fm-tasks-axi.sh ['task', 'update', '--json', 'live-note', '--body-file', '~/.no-mistakes/worktrees/dd71c22cc6d7/01M45ZSSSP3DRQHQH241NPQ537/.test-append-validation/caller/note.md']
fm-tasks-axi: refusing to replace the non-empty body of live-note: the new body does not contain the existing text. Use append-note to add text, or re-run with --archive-body to replace it while archiving the old body.
Exit: 2
$ fm-tasks-axi.sh ['show', 'live-note', '--full']
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up\n\nFile evidence\nAnother line"
Exit: 0
$ fm-tasks-axi.sh ['task', 'edit', '--json', 'live-note', '--body', 'replacement']
fm-tasks-axi: refusing to replace the non-empty body of live-note: the new body does not contain the existing text. Use append-note to add text, or re-run with --archive-body to replace it while archiving the old body.
Exit: 2


... [954 bytes truncated] ...

id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up\n\nFile evidence\nAnother line"
Exit: 0
$ fm-tasks-axi.sh ['update', 'live-note', '--body', 'Original decision: preserve "quoted" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up\n\nFile evidence\nAnother line\nPreserving replacement']
ok: updated live-note (body)
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up\n\nFile evidence\nAnother line\nPreserving replacement"
help[1]:
  - Run `tasks-axi show live-note --full` to see the result
Exit: 0
$ fm-tasks-axi.sh ['show', 'live-note', '--full']
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "Original decision: preserve \"quoted\" evidence.\n\nUnicode: 船 ⚓\nSecond line\twith tab\n\nInline follow-up\n\nFile evidence\nAnother line\nPreserving replacement"
Exit: 0
$ fm-tasks-axi.sh ['edit', 'live-note', '--body-file', '~/.no-mistakes/worktrees/dd71c22cc6d7/01M45ZSSSP3DRQHQH241NPQ537/.test-append-validation/caller/note.md', '--archive-body']
ok: updated live-note (body, archive)
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "File evidence\nAnother line\n"
help[1]:
  - Run `tasks-axi show live-note --full` to see the result
Exit: 0
$ fm-tasks-axi.sh ['show', 'live-note', '--full']
task:
  id: live-note
  title: Evidence validation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "File evidence\nAnother line"
Exit: 0

Persisted note-archive.md:

## Archived 2026-10-05
- [ ] live-note - Evidence validation (kind: ship) (since 2026-10-05)
  Original decision: preserve "quoted" evidence.

  Unicode: 船 ⚓
  Second line	with tab

  Inline follow-up

  File evidence
  Another line
  Preserving replacement

$ fm-tasks-axi.sh ['add', 'empty-note', 'Empty body', '--kind', 'ship']
ok: added empty-note (ship) -> Queued
task:
  id: empty-note
  title: Empty body
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi start empty-note` to move it to in flight
  - Run `tasks-axi block empty-note --by <other>` to record a dependency
Exit: 0
$ fm-tasks-axi.sh ['update', 'empty-note', '--body', 'First body']
ok: updated empty-note (body)
task:
  id: empty-note
  title: Empty body
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: First body
help[1]:
  - Run `tasks-axi show empty-note --full` to see the result
Exit: 0
$ fm-tasks-axi.sh ['show', 'empty-note', '--full']
task:
  id: empty-note
  title: Empty body
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: First body
Exit: 0
$ fm-tasks-axi.sh ['add', 'empty-append', 'Empty append', '--kind', 'ship']
ok: added empty-append (ship) -> Queued
task:
  id: empty-append
  title: Empty append
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi start empty-append` to move it to in flight
  - Run `tasks-axi block empty-append --by <other>` to record a dependency
Exit: 0
$ fm-tasks-axi.sh ['append-note', 'empty-append', '--body', 'First note']
ok: updated empty-append (body)
task:
  id: empty-append
  title: Empty append
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: "First note\n"
help[1]:
  - Run `tasks-axi show empty-append --full` to see the result
Exit: 0
$ fm-tasks-axi.sh ['show', 'empty-append', '--full']
task:
  id: empty-append
  title: Empty append
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: ship
  repo: "-"
  priority: "-"
  created: 2026-10-05
  closed: "-"
  deps: none
  links: none
  body: First note
Exit: 0
$ fm-tasks-axi.sh ['append-note', 'missing-id', '--body', 'unsafe']
error: "Task \"missing-id\" not found in this backlog"
code: NOT_FOUND
help[1]: Run `tasks-axi list` to see existing tasks
Exit: 1
$ fm-tasks-axi.sh ['append-note', 'live-note', '--body', '']
fm-tasks-axi: append-note: the note text is empty
Exit: 2
$ fm-tasks-axi.sh ['append-note', 'live-note', '--body-file', 'missing.md']
fm-tasks-axi: append-note: cannot read --body-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M45ZSSSP3DRQHQH241NPQ537/missing.md
Exit: 2
$ fm-tasks-axi.sh ['append-note', 'live-note', '--body', 'new', '--json']
Usage: fm-tasks-axi.sh append-note <id> (--body <text> | --body-file <path>)

Append text to a task's existing body, joined by a blank line, instead of
replacing the body the way `update --body`/`--body-file` does. To replace a
considered body, run `update <id> --body-file <path> --archive-body` so the
old body is archived into <data>/note-archive.md.
fm-tasks-axi: append-note: unknown flag --json
Exit: 2
$ fm-tasks-axi.sh ['append-note', 'live-note', '--body=new']
Usage: fm-tasks-axi.sh append-note <id> (--body <text> | --body-file <path>)

Append text to a task's existing body, joined by a blank line, instead of
replacing the body the way `update --body`/`--body-file` does. To replace a
considered body, run `update <id> --body-file <path> --archive-body` so the
old body is archived into <data>/note-archive.md.
fm-tasks-axi: append-note: unknown flag --body=new
Exit: 2
$ fm-tasks-axi.sh ['append-note', 'live-note', '--body-file=note.md']
Usage: fm-tasks-axi.sh append-note <id> (--body <text> | --body-file <path>)

Append text to a task's existing body, joined by a blank line, instead of
replacing the body the way `update --body`/`--body-file` does. To replace a
considered body, run `update <id> --body-file <path> --archive-body` so the
old body is archived into <data>/note-archive.md.
fm-tasks-axi: append-note: unknown flag --body-file=~/.no-mistakes/worktrees/dd71c22cc6d7/01M45ZSSSP3DRQHQH241NPQ537/note.md
Exit: 2
$ fm-tasks-axi.sh ['append-note', '--help']
Usage: fm-tasks-axi.sh append-note <id> (--body <text> | --body-file <path>)

Append text to a task's existing body, joined by a blank line, instead of
replacing the body the way `update --body`/`--body-file` does. To replace a
considered body, run `update <id> --body-file <path> --archive-body` so the
old body is archived into <data>/note-archive.md.
Exit: 0

All observable body/archive and no-mutation assertions passed.
Evidence: Focused real-CLI regression results
FM_TEST_BEGIN 2026-10-05T12:30:27Z tests/fm-tasks-axi.test.sh family=pure-contract-unit expected_gate_skip=none
ok - append-note keeps the prior body and appends the new text
ok - append-note on an empty body stores just the new text
ok - append-note resolves a relative --body-file against the caller directory
ok - append-note on a missing id fails and writes nothing
ok - append-note rejects removed options and advertises space-separated inputs
ok - update --body-file dropping a non-empty body is refused
ok - update --body dropping a non-empty body is refused
ok - edit --body dropping a non-empty body is refused
ok - update --archive-body replaces the body and archives the old one
ok - a replace whose new text contains the prior body proceeds
ok - a replace of an empty body proceeds without --archive-body
ok - replace guards cover both verbs, task aliases, body inputs, and flags before ID
ok - fm-tasks-axi: all cases passed
FM_TEST_END 2026-10-05T12:31:12Z tests/fm-tasks-axi.test.sh exit=0 duration_ms=45522 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=45582
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=1 duration_ms=45522 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-tasks-axi.test.sh duration_ms=45522

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 4 issues (2 errors, 2 warnings)
  • 🚨 bin/fm-tasks-axi.sh:315 - With a non-empty prior body, update --json &lt;id&gt; --body replacement returns here without checking preservation. Upstream removes flags before resolving the ID, so it successfully replaces the body without archiving. The same bypass affects edit, both body input forms, and task update/task edit, which bypass dispatch at bin/fm-tasks-axi.sh:340–346. Normalize the supported command forms and resolve the positional ID before applying the shared guard; otherwise refuse unsupported forms before forwarding.
  • ⚠️ bin/fm-tasks-axi.sh:258 - Simplification: append-note introduces a new --json output mode that no stated requirement needs. Remove its parser branch, forwarding at bin/fm-tasks-axi.sh:295, and usage declarations at lines 5, 97, and 233; retain the requested append operation.
  • ⚠️ bin/fm-tasks-axi.sh:245 - Simplification: append-note additionally accepts --body=... and --body-file=... at bin/fm-tasks-axi.sh:245 and :254. The requested separate-value forms satisfy the intent without these additional matching paths. Remove these branches unless the broader syntax is desired; upstream replacement-command compatibility can remain unchanged.

🔧 Fix applied.
4 issues (2 errors, 2 warnings) still open:

  • 🚨 bin/fm-tasks-axi.sh:315 - With a non-empty prior body, update --json &lt;id&gt; --body replacement returns here without checking preservation. Upstream removes flags before resolving the ID, so it successfully replaces the body without archiving. The same bypass affects edit, both body input forms, and task update/task edit, which bypass dispatch at bin/fm-tasks-axi.sh:340–346. Normalize the supported command forms and resolve the positional ID before applying the shared guard; otherwise refuse unsupported forms before forwarding.
  • ⚠️ bin/fm-tasks-axi.sh:258 - Simplification: append-note introduces a new --json output mode that no stated requirement needs. Remove its parser branch, forwarding at bin/fm-tasks-axi.sh:295, and usage declarations at lines 5, 97, and 233; retain the requested append operation.
  • ⚠️ bin/fm-tasks-axi.sh:245 - Simplification: append-note additionally accepts --body=... and --body-file=... at bin/fm-tasks-axi.sh:245 and :254. The requested separate-value forms satisfy the intent without these additional matching paths. Remove these branches unless the broader syntax is desired; upstream replacement-command compatibility can remain unchanged.
  • 🚨 bin/fm-tasks-axi.sh:284 - Round 1 left a read/write race unresolved. Two append-note calls can read body X; B writes X+B and verifies successfully; A then writes X+A and verifies successfully, silently losing B without an archive. The same invariant must hold at bin/fm-tasks-axi.sh:272 (append read), :287 (verification), :330 (replacement-guard read), and :352 (forwarded replacement): a preserving replacement approved against X can overwrite intervening notes. Enforce preservation against the current body inside the shared mutation critical section. Coordinated locking or a conditional-update primitive extends the current machinery, so the remedy needs authorization; the existing upstream update lock covers only the write, not this preceding read.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Append inline or relative-file notes and preserve the existing multiline body ✅ pass live live-cli-transcript.log; targeted-cli-tests.log
Replace a non-empty body without archiving and receive a refusal with the body unchanged ✅ pass live live-cli-transcript.log; targeted-cli-tests.log
Try replacement with flags before the ID and task update/edit aliases; preservation guards still apply ✅ pass live live-cli-transcript.log; targeted-cli-tests.log
Replace with --archive-body and recover the complete previous body from the archive ✅ pass live live-cli-transcript.log, including persisted note-archive.md
Replace while retaining prior text, or populate an empty body, without requiring an archive ✅ pass live live-cli-transcript.log; targeted-cli-tests.log
Submit missing IDs, empty notes, unreadable files, or removed append options and leave the backlog unchanged ✅ pass live live-cli-transcript.log; targeted-cli-tests.log
  • tasks-axi --help, tasks-axi edit --help, and tasks-axi update --help
  • TMPDIR="$PWD/.test-append-validation/tmp" bin/fm-test-run.sh tests/fm-tasks-axi.test.sh
  • python3 .test-append-validation/drive.py: real wrapper commands with body, archive, and unchanged-backlog assertions
  • Removed disposable testing directories and verified git status --short was clean.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

dnth added 4 commits October 5, 2026 17:29
`fm-tasks-axi.sh update|edit --body|--body-file` replaced the whole body,
so a caller meaning to add evidence could silently drop the prior text.
Add a wrapper-owned `append-note` that keeps the prior body and verifies
the stored result, and refuse a replace that would drop a non-empty body
unless `--archive-body` keeps the old text recoverable.
@dnth
dnth merged commit 7bfe32a into main Oct 5, 2026
18 checks passed
@dnth
dnth deleted the fm/fm-tasks-axi-append-note branch October 5, 2026 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant