Skip to content

fix(bin): refuse tasks-axi add/create --start so In flight always has a dispatch record - #5524

Merged
kunchenguid merged 4 commits into
kunchenguid:mainfrom
karotkriss:fm/fm-up-4753-tasks-axi-add-start
Sep 24, 2026
Merged

kunchenguid merged 4 commits into
kunchenguid:mainfrom
karotkriss:fm/fm-up-4753-tasks-axi-add-start

Conversation

@karotkriss

Copy link
Copy Markdown
Contributor

Intent

Fixes #4753

Running the backlog wrapper with add ... --start puts a row In flight although no dispatch record, status file, or inbox exists for it, and nothing later notices, so the live-task count includes work nobody is doing.
Make the wrapper refuse add --start (dispatch is the only path that moves a row to In flight) so a hand-started row cannot exist without dispatch artifacts.
This closes upstream issue #4753.

What Changed

  • bin/fm-tasks-axi.sh now rejects --start on add and its create alias, exiting with code 2 and pointing the caller to add the row Queued and let bin/fm-spawn.sh dispatch it; start <id> and plain add/create still pass through.
  • Documented the guard in docs/configuration.md, explaining that a hand-started row would lack the task record, status file, and inbox that dispatch creates and would otherwise count as live work nobody is running.
  • Added test_wrapper_refuses_add_start covering both spellings' refusal (exit 2, backlog unchanged, message names the dispatch path) and the still-allowed add then start <id> flow.

Risk Assessment

✅ Low: A small, well-bounded wrapper guard that refuses add/create --start with an order-independent check on $1, correct passthrough for start and other commands, accurate scoped docs, and a behavioral regression test that verifies refusal and passthrough.

Testing

Ran the focused suite tests/fm-tasks-axi.test.sh (all green, including the new add/create --start case) and drove a manual end-to-end transcript through the wrapper in an isolated split home. add --start and create --start are both refused with exit 2 and the message naming bin/fm-spawn.sh, and the backlog stays unchanged (no In flight row created). Plain add still writes a Queued row and start &lt;id&gt; still transitions it to In flight, matching the intent's scope. An adversarial flag-first add --start hs-3 is also refused. No UI surface exists for this CLI change; evidence is the CLI transcript.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
add --start is refused with exit 2 and leaves no In flight row ✅ pass live add-start-refusal-transcript.txt Scenario 1 + backlog dump (exit=2, message names bin/fm-spawn.sh, backlog unchanged)
create --start alias is refused identically ✅ pass live add-start-refusal-transcript.txt Scenario 2 + backlog dump (exit=2, no row written)
plain add (no --start) still writes a Queued row ✅ pass live add-start-refusal-transcript.txt Scenario 3 (ok: added hs-2 -> Queued, exit=0)
start <id> remains a documented passthrough to In flight ✅ pass live add-start-refusal-transcript.txt Scenario 4 (ok: start hs-2 -> In flight, exit=0)
Adversarial: --start placed before the id still refused ✅ pass live add-start-refusal-transcript.txt Scenario 5 (exit=2, refused)
Evidence: add --start / create --start refusal CLI transcript

Source: add --start / create --start refusal CLI transcript

\### Scenario 1: add --start is refused (exit 2, names dispatch path)
fm-tasks-axi: add --start would place a row In flight with no dispatch record; add it Queued and let bin/fm-spawn.sh start it
exit=2

\### Scenario 2: create --start alias is also refused
fm-tasks-axi: add --start would place a row In flight with no dispatch record; add it Queued and let bin/fm-spawn.sh start it
exit=2

\### Backlog after refusals (must be unchanged: no In flight row):
## In flight

## Queued

## Done

\### Scenario 3: plain add (no --start) still writes a Queued row
ok: added hs-2 -> Queued
task:
  id: hs-2
  title: queued work
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: task
  repo: "-"
  priority: "-"
  created: 2026-09-24
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi start hs-2` to move it to in flight
  - Run `tasks-axi block hs-2 --by <other>` to record a dependency
exit=0

\### Scenario 4: start <id> passthrough still moves the row to In flight
ok: start hs-2 -> In flight
help[1]:
  - Run `tasks-axi done hs-2 --pr <url>` when it ships
exit=0

\### Final backlog (hs-2 In flight, refused rows never created):
## In flight
- [ ] hs-2 - queued work (since 2026-09-24)

## Queued
## Done

\### Scenario 5 (adversarial): --start before the subcommand id, still refused
fm-tasks-axi: add --start would place a row In flight with no dispatch record; add it Queued and let bin/fm-spawn.sh start it
exit=2

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (3) ✅
  • 🚨 bin/fm-tasks-axi.sh:102 - The add --start guard only matches the literal command add ([ &#34;${1:-}&#34; = add ]), but tasks-axi add has a documented alias create. I verified tasks-axi create &lt;id&gt; &#34;&lt;title&gt;&#34; --start places the row in_flight identically to add --start, so fm-tasks-axi.sh create hs-1 &#34;x&#34; --start passes the guard untouched and reintroduces the exact In-flight-row-with-no-dispatch-record defect tasks-axi add --start can mark a row In flight with no dispatch record, and nothing ever corrects it #4753 closes. create is an advertised alias on this human/agent-facing wrapper, so it is a reachable intended-usage path, not hypothetical. Earliest shared fix: match both spellings, e.g. case &#34;${1:-}&#34; in add|create). The new test (tests/fm-tasks-axi.test.sh:207) exercises only add, so it does not cover this sibling path; extend it to assert create ... --start is also refused. The remedy corrects what the change already does (fully closing the only path), so it stays auto-fix.

🔧 Fix applied.
2 issues (1 error, 1 warning) still open:

  • 🚨 bin/fm-tasks-axi.sh:102 - The add --start guard only matches the literal command add ([ &#34;${1:-}&#34; = add ]), but tasks-axi add has a documented alias create. I verified tasks-axi create &lt;id&gt; &#34;&lt;title&gt;&#34; --start places the row in_flight identically to add --start, so fm-tasks-axi.sh create hs-1 &#34;x&#34; --start passes the guard untouched and reintroduces the exact In-flight-row-with-no-dispatch-record defect tasks-axi add --start can mark a row In flight with no dispatch record, and nothing ever corrects it #4753 closes. create is an advertised alias on this human/agent-facing wrapper, so it is a reachable intended-usage path, not hypothetical. Earliest shared fix: match both spellings, e.g. case &#34;${1:-}&#34; in add|create). The new test (tests/fm-tasks-axi.test.sh:207) exercises only add, so it does not cover this sibling path; extend it to assert create ... --start is also refused. The remedy corrects what the change already does (fully closing the only path), so it stays auto-fix.
  • ⚠️ bin/fm-tasks-axi.sh:101 - The change's stated goal is that "dispatch is the only path that moves a row to In flight" so "a hand-started row cannot exist without dispatch artifacts". The new guard closes add --start/create --start, but tasks-axi start &lt;id&gt; is a sibling command whose sole purpose is "Move a task to In flight (idempotent)" (verified via tasks-axi start --help). The wrapper passes start &lt;id&gt; straight through (line 116 * passthrough), so fm-tasks-axi.sh start hs-2 moves a Queued row to In flight with no dispatch record, status file, or inbox - the exact tasks-axi add --start can mark a row In flight with no dispatch record, and nothing ever corrects it #4753 condition, since those artifacts are created by bin/fm-spawn.sh's orchestration, not by tasks-axi start (dispatch calls fm_backlog_start -> tasks-axi start directly, bypassing this wrapper). The new test even codifies this hole as intended behavior: tests/fm-tasks-axi.test.sh:226 asserts start hs-2 passes through. So the invariant in the intent's rationale is not actually enforced. Whether the wrapper should also refuse start &lt;id&gt; (matching the same refusal for case add|create, plus a start command guard) is a product-scope decision beyond the literal 'refuse add --start' requirement, so this needs your call - the remedy expands the change's behavior, not just corrects it. The literal required constraint (refuse add --start) is satisfied.

🔧 Fix applied.
1 warning still open:

  • ⚠️ docs/configuration.md:123 - Fix-round commit f7c1cad claims to 'drop only-path overclaim', but the reworded text retains an equivalent universal claim. docs/configuration.md:123 states 'So a hand-started row cannot exist without dispatch artifacts', and the header comment at bin/fm-tasks-axi.sh:39-40 says refusing add/create --start means 'this wrapper cannot place a row In flight without the dispatch artifacts bin/fm-spawn.sh creates'. Both are false: tasks-axi start &lt;id&gt; places a Queued row In flight with no dispatch record, status file, or inbox, and this same wrapper passes start &lt;id&gt; straight through (bin/fm-tasks-axi.sh:117 * passthrough; codified by tests/fm-tasks-axi.test.sh:226 asserting start hs-2 succeeds). So a hand-started row CAN exist without dispatch artifacts via start &lt;id&gt;. This is the exact 'dispatch is the only path' overclaim the round-2 user instruction said to remove ('must say that add --start and create --start no longer create an In flight row without dispatch artifacts, not that dispatch is the only path'). Sibling sites to correct together: docs/configuration.md:123 (universal 'a hand-started row cannot exist without dispatch artifacts') and bin/fm-tasks-axi.sh:39-40 (universal 'this wrapper cannot place a row In flight without the dispatch artifacts'). Remedy: scope both to add --start / create --start specifically (they no longer place a row In flight without dispatch artifacts) and drop the universal wording, since start &lt;id&gt; remains a documented direct transition the wrapper passes through. Non-functional wording correction of an inaccuracy the change itself introduced, so auto-fix; it narrows, not expands, behavior claims.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
add --start is refused with exit 2 and leaves no In flight row ✅ pass live add-start-refusal-transcript.txt Scenario 1 + backlog dump (exit=2, message names bin/fm-spawn.sh, backlog unchanged)
create --start alias is refused identically ✅ pass live add-start-refusal-transcript.txt Scenario 2 + backlog dump (exit=2, no row written)
plain add (no --start) still writes a Queued row ✅ pass live add-start-refusal-transcript.txt Scenario 3 (ok: added hs-2 -> Queued, exit=0)
start <id> remains a documented passthrough to In flight ✅ pass live add-start-refusal-transcript.txt Scenario 4 (ok: start hs-2 -> In flight, exit=0)
Adversarial: --start placed before the id still refused ✅ pass live add-start-refusal-transcript.txt Scenario 5 (exit=2, refused)
  • bash tests/fm-tasks-axi.test.sh (test_wrapper_refuses_add_start drives add --start, create --start, plain add, and start <id> through the real wrapper)
  • Manual CLI transcript through bin/fm-tasks-axi.sh against real tasks-axi in an isolated split home: add --start, create --start, plain add, start &lt;id&gt;, and adversarial flag-first add --start hs-3
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…atch record

Fixes kunchenguid#4753

Dispatch (bin/fm-spawn.sh) is the only path that moves a backlog row to
In flight, because it creates the task record, status file, and inbox
that go with the row. A row hand-placed there through the wrapper's
`add --start` had none of those, and nothing later noticed, so the
live-task count included work nobody was doing. The wrapper now refuses
`add --start` (exit 2) and names the dispatch path; plain `add` and
`start <id>` pass through unchanged, and the lifecycle transitions
address tasks-axi directly so dispatch is unaffected.

The issue's other half, a reconcile sweep in bin/fm-inactive-reconcile.sh
that notices an In flight row with no task record, is left as is; this
change closes the only path that creates such a row.
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

VISION.md read in full from live main 0d983d2a2dac (post-#5526/#2868). Inspected tip a7d2640bb209 vs main: bin/fm-tasks-axi.sh, docs/configuration.md, tests/fm-tasks-axi.test.sh. Issue #4753 still open; main wrapper still passes add/create --start through to tasks-axi, so a hand-started row can land In flight with no meta/status/inbox. Competing PR for #4753: none.

Per-rule

  • One captain, one interface / peace of mind: aligns (live-task count must not include nobody's work).
  • Authority is explicit and never inferred: aligns (wrapper refuses a dishonest hand-start spelling).
  • A restart is a non-event / obligations closed by records: aligns (In flight without a dispatch record is a durable lie).
  • Scripts own the mechanics: aligns (order-independent --start guard on add|create).
  • Delegation with a spine: aligns (dispatch via bin/fm-spawn.sh remains the artifact-creating path).
  • Scope: aligns (home-addressing wrapper + docs; no workshop growth).
  • Note from tip: start <id> remains a documented passthrough (pipeline review warning scoped; docs drop the universal only-path claim). That residual hole is out of this PR's literal refuse-add/create---start scope and does not turn the bounded refusal into new-default.

contract-class: restore — restores the documented dispatch/record coupling for the wrapper's add/create --start entry that was broken on main (phantom In flight). Not a new default-on surface (FM-LEARN-4627). Contributor Fixes #4753 / restore claims noted only (FM-LEARN-CLAIMS). Issue triage already classed restore.

HEAD a7d2640bb209fb4994f36aa662680628f6455236. Attestation MATCH. CI/NM all SUCCESS. MERGEABLE/CLEAN (behind main from sibling merges; no conflict). Security: none. Workflow-approved: yes. Auto-merge eligible → squash-merging.

@kunchenguid
kunchenguid merged commit 977a81e into kunchenguid:main Sep 24, 2026
19 checks passed
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Thank you @karotkriss — merged (squash). The home-addressing wrapper now refuses add/create --start so those spellings cannot place a backlog row In flight without dispatch artifacts (#4753).

mituso89 pushed a commit to mituso89/firstmate that referenced this pull request Sep 26, 2026
… a dispatch record (kunchenguid#5524)

* fix(bin): refuse tasks-axi add --start so In flight always has a dispatch record

Fixes kunchenguid#4753

Dispatch (bin/fm-spawn.sh) is the only path that moves a backlog row to
In flight, because it creates the task record, status file, and inbox
that go with the row. A row hand-placed there through the wrapper's
`add --start` had none of those, and nothing later noticed, so the
live-task count included work nobody was doing. The wrapper now refuses
`add --start` (exit 2) and names the dispatch path; plain `add` and
`start <id>` pass through unchanged, and the lifecycle transitions
address tasks-axi directly so dispatch is unaffected.

The issue's other half, a reconcile sweep in bin/fm-inactive-reconcile.sh
that notices an In flight row with no task record, is left as is; this
change closes the only path that creates such a row.

* no-mistakes(review): refuse create --start alias, not just add --start

* no-mistakes(review): reword add --start guard docs to drop only-path overclaim

* no-mistakes(review): scope add/create --start guard docs, drop universal claim
mehulbhagwani pushed a commit to mehulbhagwani/firstmate that referenced this pull request Sep 26, 2026
… a dispatch record (kunchenguid#5524)

* fix(bin): refuse tasks-axi add --start so In flight always has a dispatch record

Fixes kunchenguid#4753

Dispatch (bin/fm-spawn.sh) is the only path that moves a backlog row to
In flight, because it creates the task record, status file, and inbox
that go with the row. A row hand-placed there through the wrapper's
`add --start` had none of those, and nothing later noticed, so the
live-task count included work nobody was doing. The wrapper now refuses
`add --start` (exit 2) and names the dispatch path; plain `add` and
`start <id>` pass through unchanged, and the lifecycle transitions
address tasks-axi directly so dispatch is unaffected.

The issue's other half, a reconcile sweep in bin/fm-inactive-reconcile.sh
that notices an In flight row with no task record, is left as is; this
change closes the only path that creates such a row.

* no-mistakes(review): refuse create --start alias, not just add --start

* no-mistakes(review): reword add --start guard docs to drop only-path overclaim

* no-mistakes(review): scope add/create --start guard docs, drop universal claim
mehulbhagwani pushed a commit to mehulbhagwani/firstmate that referenced this pull request Sep 26, 2026
… a dispatch record (kunchenguid#5524)

* fix(bin): refuse tasks-axi add --start so In flight always has a dispatch record

Fixes kunchenguid#4753

Dispatch (bin/fm-spawn.sh) is the only path that moves a backlog row to
In flight, because it creates the task record, status file, and inbox
that go with the row. A row hand-placed there through the wrapper's
`add --start` had none of those, and nothing later noticed, so the
live-task count included work nobody was doing. The wrapper now refuses
`add --start` (exit 2) and names the dispatch path; plain `add` and
`start <id>` pass through unchanged, and the lifecycle transitions
address tasks-axi directly so dispatch is unaffected.

The issue's other half, a reconcile sweep in bin/fm-inactive-reconcile.sh
that notices an In flight row with no task record, is left as is; this
change closes the only path that creates such a row.

* no-mistakes(review): refuse create --start alias, not just add --start

* no-mistakes(review): reword add --start guard docs to drop only-path overclaim

* no-mistakes(review): scope add/create --start guard docs, drop universal claim
mehulbhagwani pushed a commit to mehulbhagwani/firstmate that referenced this pull request Sep 27, 2026
… a dispatch record (kunchenguid#5524)

* fix(bin): refuse tasks-axi add --start so In flight always has a dispatch record

Fixes kunchenguid#4753

Dispatch (bin/fm-spawn.sh) is the only path that moves a backlog row to
In flight, because it creates the task record, status file, and inbox
that go with the row. A row hand-placed there through the wrapper's
`add --start` had none of those, and nothing later noticed, so the
live-task count included work nobody was doing. The wrapper now refuses
`add --start` (exit 2) and names the dispatch path; plain `add` and
`start <id>` pass through unchanged, and the lifecycle transitions
address tasks-axi directly so dispatch is unaffected.

The issue's other half, a reconcile sweep in bin/fm-inactive-reconcile.sh
that notices an In flight row with no task record, is left as is; this
change closes the only path that creates such a row.

* no-mistakes(review): refuse create --start alias, not just add --start

* no-mistakes(review): reword add --start guard docs to drop only-path overclaim

* no-mistakes(review): scope add/create --start guard docs, drop universal claim
mehulbhagwani pushed a commit to mehulbhagwani/firstmate that referenced this pull request Sep 27, 2026
… a dispatch record (kunchenguid#5524)

* fix(bin): refuse tasks-axi add --start so In flight always has a dispatch record

Fixes kunchenguid#4753

Dispatch (bin/fm-spawn.sh) is the only path that moves a backlog row to
In flight, because it creates the task record, status file, and inbox
that go with the row. A row hand-placed there through the wrapper's
`add --start` had none of those, and nothing later noticed, so the
live-task count included work nobody was doing. The wrapper now refuses
`add --start` (exit 2) and names the dispatch path; plain `add` and
`start <id>` pass through unchanged, and the lifecycle transitions
address tasks-axi directly so dispatch is unaffected.

The issue's other half, a reconcile sweep in bin/fm-inactive-reconcile.sh
that notices an In flight row with no task record, is left as is; this
change closes the only path that creates such a row.

* no-mistakes(review): refuse create --start alias, not just add --start

* no-mistakes(review): reword add --start guard docs to drop only-path overclaim

* no-mistakes(review): scope add/create --start guard docs, drop universal claim
mehulbhagwani pushed a commit to mehulbhagwani/firstmate that referenced this pull request Sep 27, 2026
… a dispatch record (kunchenguid#5524)

* fix(bin): refuse tasks-axi add --start so In flight always has a dispatch record

Fixes kunchenguid#4753

Dispatch (bin/fm-spawn.sh) is the only path that moves a backlog row to
In flight, because it creates the task record, status file, and inbox
that go with the row. A row hand-placed there through the wrapper's
`add --start` had none of those, and nothing later noticed, so the
live-task count included work nobody was doing. The wrapper now refuses
`add --start` (exit 2) and names the dispatch path; plain `add` and
`start <id>` pass through unchanged, and the lifecycle transitions
address tasks-axi directly so dispatch is unaffected.

The issue's other half, a reconcile sweep in bin/fm-inactive-reconcile.sh
that notices an In flight row with no task record, is left as is; this
change closes the only path that creates such a row.

* no-mistakes(review): refuse create --start alias, not just add --start

* no-mistakes(review): reword add --start guard docs to drop only-path overclaim

* no-mistakes(review): scope add/create --start guard docs, drop universal claim
RooseveltAdvisors pushed a commit to RooseveltAdvisors/firstmate that referenced this pull request Sep 29, 2026
… a dispatch record (kunchenguid#5524)

* fix(bin): refuse tasks-axi add --start so In flight always has a dispatch record

Fixes kunchenguid#4753

Dispatch (bin/fm-spawn.sh) is the only path that moves a backlog row to
In flight, because it creates the task record, status file, and inbox
that go with the row. A row hand-placed there through the wrapper's
`add --start` had none of those, and nothing later noticed, so the
live-task count included work nobody was doing. The wrapper now refuses
`add --start` (exit 2) and names the dispatch path; plain `add` and
`start <id>` pass through unchanged, and the lifecycle transitions
address tasks-axi directly so dispatch is unaffected.

The issue's other half, a reconcile sweep in bin/fm-inactive-reconcile.sh
that notices an In flight row with no task record, is left as is; this
change closes the only path that creates such a row.

* no-mistakes(review): refuse create --start alias, not just add --start

* no-mistakes(review): reword add --start guard docs to drop only-path overclaim

* no-mistakes(review): scope add/create --start guard docs, drop universal claim
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.

tasks-axi add --start can mark a row In flight with no dispatch record, and nothing ever corrects it

2 participants