Skip to content

fix(bin): preserve hold reasons and reject invalid completion inventories - #6331

Merged
kunchenguid merged 7 commits into
kunchenguid:mainfrom
mremond:fm/fm-5712-hold-reason-and-circular-complete
Oct 1, 2026
Merged

kunchenguid merged 7 commits into
kunchenguid:mainfrom
mremond:fm/fm-5712-hold-reason-and-circular-complete

Conversation

@mremond

@mremond mremond commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Launch upstream ready-for-pr issue #5712 as its own ship from current origin/main, through the usual upstream contribution path; the maintainer merges. Keep it scoped to its issue.

Substance of the issue and the maintainer's triage (contract-class restore): bin/fm-captain-hold.sh hold --reason refuses any reason containing parentheses with "reason must not contain parentheses (tasks-axi hold contract)" and names no remedy, although parentheses are ordinary prose in a decision reason. Separately, bin/fm-captain-hold.sh complete <origin> <origin> accepts the origin task as its own captain-call inventory whenever the origin row looks durable, so a refused hold immediately before it (for example in a chain with pipefail off) goes unnoticed and the completion gate looks satisfied with no hold recorded. The maintainer left the issue open for a PR along the issue author's refined fix: hold stores the origin it was recorded for on the held task; complete refuses an inventory entry equal to the origin, and refuses an entry whose stored origin differs from the origin being completed, with older records that carry no stored origin falling back to the current check and flagged in the output; reason text is encoded where tasks-axi stores it instead of banning characters, so a reason containing parentheses, semicolons, quotes and a newline is stored and read back unchanged. Tests: a failed hold followed by complete <o> <o> exits non-zero; an entry held for a different origin is refused; a reason containing ( ) ; " and a newline round-trips unchanged. An optional hold --complete that records hold and completion in one locked step is welcome but not required.

What Changed

  • Encode hold reasons reversibly so punctuation and newlines survive storage and display in task reads, fleet snapshots, startup digests, and return briefs.
  • Record --origin before applying a hold; make complete and verify reject self-references and mismatched origins using backend identities, while completion flags legacy entries without recorded origins.
  • Document repair of historical self-referencing inventories and add regression coverage for origin checks, failed holds, and reason round-trips.

Risk Assessment

✅ Low: The changes are bounded and follow the latest recorded decisions; the full static review identified no additional material defects.

Testing

Baseline defects reproduced; all targeted live Markdown CLI scenarios passed with tasks-axi 0.2.6. Corrected driver setup errors were re-tested. CLI transcripts and persisted output were captured; labs were removed. No full suite, linters, publication or CI phases were run.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
A failed hold cannot be concealed by completing an origin with itself, even when that origin is already held. ✅ pass live Live CLI transcripts: circular-inventory
A historical circular inventory remains refused until the documented repair replaces it with a separate held call. ✅ pass live Live CLI transcripts: circular-inventory
Completion accepts the recorded origin and rejects another; active and released re-holds replace the previous association. ✅ pass live Live CLI transcripts: origin-association-and-rehold
An actual origin-write storage failure leaves new tasks unheld, with and without a deferral date; retry succeeds after storage restrictions are removed. ✅ pass live Live CLI transcripts: origin-write-failure
Legacy durable calls remain accepted with an explicit no-origin warning, while answered calls retain recorded-origin enforcement. ✅ pass live Live CLI transcripts: legacy-origin-fallback and answered-inventory
Punctuation, Unicode and multiline reasons round-trip through show, view, list and fleet output without changing titles, legacy reasons or unrelated fields. ✅ pass live Live CLI transcripts: reason-roundtrip-and-public-readers; Fleet output with decoded reasons
Startup and return summaries display decoded reasons while preserving literal percent and encoding-marker text. ✅ pass live Live CLI transcripts: startup-and-return-readers
When the configured task adapter is unavailable, startup's manual fallback still displays the original reason. ✅ pass live Live CLI transcripts: startup-fallback-reader
Evidence: Live CLI transcripts

Source: Live CLI transcripts

Target fadcf45b24e4ea3d9103e6d118431222c8e2e6bf
Base 8f756bbc287c5bdfacc64a7cc09e8516c64fc919
Real tasks-axi 0.2.6, Markdown backend. All lab homes were inside the test worktree and were removed. No source edits.


===== circular-inventory =====
Target: fadcf45b24e4ea3d9103e6d118431222c8e2e6bf
Real tasks-axi 0.2.6; no fake CLI or backend.

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-lab-home.sh create ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-v_y2kcd0/circular
~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-v_y2kcd0/circular
[exit 0]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-tasks-axi.sh add origin 'Origin investigation' --kind scout
ok: added origin (scout) -> Queued
task:
  id: origin
  title: Origin investigation
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: scout
  repo: "-"
  priority: "-"
  created: 2026-10-01
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi start origin` to move it to in flight
  - Run `tasks-axi block origin --by <other>` to record a dependency
[exit 0]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-captain-hold.sh hold origin --reason ''
stderr:
fm-captain-hold: reason must not be empty
[exit 1]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-captain-hold.sh complete origin origin
stderr:
fm-captain-hold: origin origin cannot be its own captain-call inventory entry; hold a separate captain task for the call and list that task
[exit 1]

ASSERTION: failed hold: circular completion refused without recording an attestation

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-captain-hold.sh hold origin --reason 'Choose a route'
origin
[exit 0]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-captain-hold.sh complete origin origin
stderr:
fm-captain-hold: origin origin cannot be its own captain-call inventory entry; hold a separate captain task for the call and list that task
[exit 1]

ASSERTION: durably held origin: circular completion refused without recording an attestation

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-captain-hold.sh verify origin
stderr:
fm-captain-hold: origin origin cannot be its own captain-call inventory entry; historical decision_keys in ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-v_y2kcd0/circular/state/origin.meta still contains origin; hold a separate captain task with --origin origin, replace only origin in the final decision_keys= line with that task id while preserving all other entries, then re-run complete origin <task-id>
[exit 1]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-captain-hold.sh hold separate --title 'Separate captain call' --reason 'Choose route' --origin origin
separate
[exit 0]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-captain-hold.sh complete origin separate
stderr:
fm-captain-hold: origin origin cannot be its own captain-call inventory entry; historical decision_keys in ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-v_y2kcd0/circular/state/origin.meta still contains origin; hold a separate captain task with --origin origin, replace only origin in the final decision_keys= line with that task id while preserving all other entries, then re-run complete origin <task-id>
[exit 1]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-captain-hold.sh complete origin separate
complete: origin captain-call inventory reviewed (separate)
[exit 0]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-captain-hold.sh verify origin
verified: origin captain-call inventory
[exit 0]

ASSERTION: Historical self-inventory stayed refused until its entry was replaced with a separate held call, then completion and verify succeeded


===== legacy-origin-fallback =====
Target: fadcf45b24e4ea3d9103e6d118431222c8e2e6bf
Real tasks-axi 0.2.6; no fake CLI or backend.

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-lab-home.sh create ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-v_y2kcd0/legacy
~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-v_y2kcd0/legacy
[exit 0]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-tasks-axi.sh add origin Origin --kind scout
ok: added origin (scout) -> Queued
task:
  id: origin
  title: Origin
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: scout
  repo: "-"
  priority: "-"
  created: 2026-10-01
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi start origin` to move it to in flight
  - Run `tasks-axi block origin --by <other>` to record a dependency
[exit 0]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-tasks-axi.sh add legacy 'Old call' --kind captain
ok: added legacy (captain) -> Queued
task:
  id: legacy
  title: Old call
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: captain
  repo: "-"
  priority: "-"
  created: 2026-10-01
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi start legacy` to move it to in flight
  - Run `tasks-axi block legacy --by <other>` to record a dependency
[exit 0]

$ tasks-axi hold legacy --reason 'Legacy URL https://example.test/%28literal%29' --kind captain
ok: hold legacy -> held (captain)
task:
  id: legacy
  title: Old call
  state: queued
  blocked: no
  blocked_by: none
  held: yes
  hold_reason: "Legacy URL https://example.test/%28literal%29"
  hold_kind: captain
  hold_until: "-"
  kind: captain
  repo: "-"
  priority: "-"
  created: 2026-10-01
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi unhold legacy` to resume dispatch
  - Run `tasks-axi ready --include-held` to review paused work
[exit 0]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-captain-hold.sh complete origin legacy
complete: origin captain-call inventory reviewed (legacy) [no recorded origin on: legacy; not checked against origin]
[exit 0]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-captain-hold.sh verify origin
verified: origin captain-call inventory
[exit 0]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-tasks-axi.sh add unheld 'No recorded call' --kind captain
ok: added unheld (captain) -> Queued
task:
  id: unheld
  title: No recorded call
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: captain
  repo: "-"
  priority: "-"
  created: 2026-10-01
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi start unheld` to move it to in flight
  - Run `tasks-axi block unheld --by <other>` to record a dependency
[exit 0]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-captain-hold.sh complete origin unheld
stderr:
fm-captain-hold: captain-held task unheld is neither held for the captain nor closed with a recorded captain answer
[exit 1]

ASSERTION: A genuinely old held row is accepted with an explicit no-origin warning; an unheld row cannot satisfy the fallback


===== reason-roundtrip-and-public-readers =====
Target: fadcf45b24e4ea3d9103e6d118431222c8e2e6bf
Real tasks-axi 0.2.6; no fake CLI or backend.

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-lab-home.sh create ~/.no-mistakes/worktrees/a

... [68259 bytes truncated] ...

ting, a bounded tail of every state/*.status,
data/projects.md, data/secondmates.md, data/captain.md, data/captain-shared.md,
and data/learnings.md.
Do NOT re-read any of them after reading this digest, and do NOT bulk-read
data/backlog.md or state/*.status: re-reading everything defeats the entire
point of this command.

Go to a source directly only when:
  - this digest flagged it ABSENT (then rebuild or create it per AGENTS.md),
  - its contents looked unparseable or corrupt,
  - an individual full status log is needed for older wake-event history, or a
    status line was capped and its tail matters (each task's full log path is
    printed with its tail),
  - a full task body is needed (bin/fm-tasks-axi.sh show <id> --full, or data/backlog.md),
  - the backlog listing disclosed omitted queued items and this turn needs them,
  - the NETWORK CHECKS section reported its checks still IN PROGRESS and this
    turn needs their verdict (bin/fm-startup-network.sh report),
  - or a STARTUP TRUNCATED banner named the stage that would have printed it, in
    which case that stage's sources were never emitted and must be reconciled.

================================================================================
FLEET STATE
================================================================================

data/backlog.md
--------------------------------------------------------------------------------
compact backlog listing (manual backend; done rows omitted; every in-flight, held, and blocked title line kept; other queued bounded to 20; indented task bodies omitted)
## In flight
## Queued
- [ ] prose - Investigate literal %28 and fm-hold-v1:bm9ydGg= (repo: firstmate) (kind: captain) (since 2026-10-01) (hold: "Pick route (north); say \"yes\"\nNext line") (hold-kind: captain)
- [ ] marker - Marker-like input (repo: firstmate) (kind: captain) (since 2026-10-01) (hold: "fm-hold-v1:bm9ydGg=") (hold-kind: captain)
- [ ] legacy - Legacy title %28 (kind: captain) (since 2026-10-01) (hold: Visit https://example.test/%28literal%29 and %0A) (hold-kind: captain)
(shown 0 in-flight, 3 held or blocked queued, 0 of 0 other queued title line(s); 0 done row(s) omitted)
Full task bodies remain available on demand: bin/fm-tasks-axi.sh show <id> --full when compatible tasks-axi is available, or data/backlog.md.

Work under way (state/*.meta)
--------------------------------------------------------------------------------
(none)

Orphan status logs (state/*.status without matching .meta)
--------------------------------------------------------------------------------
(none)

AFK
--------------------------------------------------------------------------------
absent

================================================================================
NETWORK CHECKS
================================================================================
skipped (read-only session) - GitHub authentication, project clone refresh,
secondmate liveness and convergence, and pending handoff delivery were not run.
They need the fleet lock, and this session must not spawn, steer, or merge, so it
has no action they would gate. The session holding the lock runs them.

================================================================================
CONTEXT
================================================================================

data/projects.md
--------------------------------------------------------------------------------
ABSENT

data/secondmates.md
--------------------------------------------------------------------------------
ABSENT

data/captain.md
--------------------------------------------------------------------------------
ABSENT

data/captain-shared.md (shared, main-authoritative, read-only in secondmate homes)
--------------------------------------------------------------------------------
ABSENT

data/learnings.md
--------------------------------------------------------------------------------
ABSENT

================================================================================
NEXT STEP
================================================================================
This session did not acquire the fleet lock. Stay read-only: do not arm,
drain, spawn, steer, merge, or repair fleet state from here. Only a session
with verified fleet-lock ownership may perform mutable follow-up.

The digest above is complete for this session start. The READ-ONCE CONTRACT
section near the top of it governs what may still be read from disk.
[exit 0]

ASSERTION: Startup manual digest preserves prose, literal percent text and marker-like text exactly

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-afk-return.sh check
=== Return brief ===
Supervisor health:
  - GAP: the watcher beat was 999999s old at return (grace 300s)
Your instructions:
  (no away-posture record for this window; legacy away flag only)
Waiting on you:
  held in the backlog:
    tasks[3]{id,state,kind,repo,title,hold_kind,hold_reason,hold_until}:
      prose,queued,captain,firstmate,"Investigate literal %28 and fm-hold-v1:bm9ydGg=",captain,"Pick route (north); say \"yes\"\nNext line","-"
      marker,queued,captain,firstmate,Marker-like input,captain,"fm-hold-v1:bm9ydGg=","-"
      legacy,queued,captain,"-",Legacy title %28,captain,"Visit https://example.test/%28literal%29 and %0A","-"
Tried and failed, or could not be fixed:
  (nothing)
Landed, cleanup due:
  (nothing)
Handled while away:
  0 outcome(s) handled by the away session (0 routine, 0 escalated above)
  (no routine outcomes recorded in the store for this window)
Cost: 0 supervision outcome(s) recorded (0 routine, 0 captain); 0 task(s) live at return.
catch-up health: GAP: the watcher beat was 999999s old at return (grace 300s)
fm-afk-return: catch-up clear; ordinary captain work may proceed
[exit 0]

ASSERTION: Real return check renders the same reason and leaves legacy, title and marker-like text unchanged


===== baseline-reproduction =====
Target: fadcf45b24e4ea3d9103e6d118431222c8e2e6bf
Real tasks-axi 0.2.6; no fake CLI or backend.

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-lab-home.sh create ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-pxttrvna/baseline
~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-pxttrvna/baseline
[exit 0]

$ git archive 8f756bbc287c5bdfacc64a7cc09e8516c64fc919 bin .tasks.toml (extracted only into disposable workspace)
$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/bin/fm-tasks-axi.sh add origin Origin --kind scout
ok: added origin (scout) -> Queued
task:
  id: origin
  title: Origin
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: scout
  repo: "-"
  priority: "-"
  created: 2026-10-01
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi start origin` to move it to in flight
  - Run `tasks-axi block origin --by <other>` to record a dependency
[exit 0]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-pxttrvna/base-code/bin/fm-captain-hold.sh hold origin --reason 'Choose a route'
origin
[exit 0]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-pxttrvna/base-code/bin/fm-captain-hold.sh hold origin --reason 'Choose (north); "yes"'
stderr:
fm-captain-hold: reason must not contain parentheses (tasks-axi hold contract)
[exit 1]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-pxttrvna/base-code/bin/fm-captain-hold.sh hold origin --reason 'Choose (north); "yes"
Next'
stderr:
fm-captain-hold: reason must be one line
[exit 1]

$ ~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-pxttrvna/base-code/bin/fm-captain-hold.sh complete origin origin
complete: origin captain-call inventory reviewed (origin)
[exit 0]

ASSERTION: The base rejects ordinary prose then accepts circular completion on the durable origin; target scenarios reject that completion and accept the prose
Evidence: Fleet output with decoded reasons

Source: Fleet output with decoded reasons

{
  "schema": "fm-fleet-snapshot.v1",
  "generated": "2026-10-01T17:13:39Z",
  "fm_home": "~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-v_y2kcd0/reasons",
  "roots": {
    "fm_root": "~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB",
    "state": "~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-v_y2kcd0/reasons/state",
    "data": "~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-v_y2kcd0/reasons/data",
    "config": "~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-v_y2kcd0/reasons/config",
    "projects": "~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-v_y2kcd0/reasons/projects"
  },
  "backlog": {
    "path": "~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-v_y2kcd0/reasons/data/backlog.md",
    "records": [
      {
        "body_excerpt": "fm-hold-v1:bm9ydGg= hold_reason: \"%28\"",
        "done": null,
        "hold_bucket": "live",
        "hold_kind": "captain",
        "blocked_by": null,
        "requires_child_metadata": false,
        "current_role": "queued",
        "hold_age_days": 0,
        "pr_url": null,
        "order": 1,
        "links": [
          "https://example.test/%28literal%29"
        ],
        "state": "queued",
        "checked": false,
        "blocked_reason": null,
        "since": "2026-10-01",
        "raw": "- [ ] legacy - Investigate literal %28, \"fm-hold-v1:bm9ydGg=\" (kind: captain) (since 2026-10-01) (hold: Visit https://example.test/%28literal%29 and %0A; fm-hold-v1:bm9ydGg=) (hold-kind: captain)",
        "hold_until": null,
        "priority": null,
        "hold_set": null,
        "reported": null,
        "body_lines": [
          "fm-hold-v1:bm9ydGg=",
          "hold_reason: \"%28\""
        ],
        "captain_actionable": true,
        "id": "legacy",
        "unresolved_blocker_ids": [],
        "title": "Investigate literal %28, \"fm-hold-v1:bm9ydGg=\"",
        "report_path": null,
        "blocked_by_ids": [],
        "hold_reason": "Visit https://example.test/%28literal%29 and %0A; fm-hold-v1:bm9ydGg=",
        "structured": true,
        "local_note": null,
        "kind": "captain",
        "completion": {
          "date": null,
          "verb": null
        },
        "merged": null,
        "repo": null
      },
      {
        "since": "2026-10-01",
        "raw": "- [ ] prose - Investigate literal %28, \"fm-hold-v1:bm9ydGg=\" (repo: firstmate) (kind: captain) (since 2026-10-01) (hold: fm-hold-v1:ICBQaWNrIHJvdXRlIChub3J0aCk7IHNheSAieWVzIiBvciAnbm8nIC0gMTAwJSBzdXJlICUyOHglMjksIGNhZsOpCVxzbGFzaA0KU2Vjb25kIGxpbmUKCg==) (hold-kind: captain)",
        "hold_until": null,
        "priority": null,
        "checked": false,
        "state": "queued",
        "links": [],
        "blocked_reason": null,
        "pr_url": null,
        "hold_age_days": 0,
        "order": 2,
        "hold_bucket": "live",
        "hold_kind": "captain",
        "done": null,
        "body_excerpt": "Captain hold set: 2026-10-01T17:13:33Z",
        "requires_child_metadata": false,
        "blocked_by": null,
        "current_role": "queued",
        "completion": {
          "date": null,
          "verb": null
        },
        "merged": null,
        "repo": "firstmate",
        "local_note": null,
        "structured": true,
        "kind": "captain",
        "captain_actionable": true,
        "id": "prose",
        "unresolved_blocker_ids": [],
        "blocked_by_ids": [],
        "hold_reason": "  Pick route (north); say \"yes\" or 'no' - 100% sure %28x%29, café\t\\slash\r\nSecond line\n\n",
        "title": "Investigate literal %28, \"fm-hold-v1:bm9ydGg=\"",
        "report_path": null,
        "hold_set": "2026-10-01T17:13:33Z",
        "body_lines": [
          "Captain hold set: 2026-10-01T17:13:33Z"
        ],
        "reported": null
      },
      {
        "kind": "captain",
        "structured": true,
        "local_note": null,
        "repo": "firstmate",
        "merged": null,
        "completion": {
          "verb": null,
          "date": null
        },
        "reported": null,
        "body_lines": [
          "Captain hold set: 2026-10-01T17:13:36Z"
        ],
        "hold_set": "2026-10-01T17:13:36Z",
        "title": "Investigate literal %28, \"fm-hold-v1:bm9ydGg=\"",
        "report_path": null,
        "blocked_by_ids": [],
        "hold_reason": "fm-hold-v1:bm9ydGg=",
        "unresolved_blocker_ids": [],
        "id": "marker",
        "captain_actionable": false,
        "blocked_reason": null,
        "links": [],
        "state": "queued",
        "checked": false,
        "hold_until": "2099-01-01",
        "priority": null,
        "raw": "- [ ] marker - Investigate literal %28, \"fm-hold-v1:bm9ydGg=\" (repo: firstmate) (kind: captain) (since 2026-10-01) (hold: fm-hold-v1:Zm0taG9sZC12MTpibTl5ZEdnPQ==) (hold-kind: captain) (hold-until: 2099-01-01)",
        "since": "2026-10-01",
        "blocked_by": null,
        "requires_child_metadata": false,
        "current_role": "queued",
        "done": null,
        "body_excerpt": "Captain hold set: 2026-10-01T17:13:36Z",
        "hold_kind": "captain",
        "hold_bucket": "dated",
        "order": 3,
        "hold_age_days": 0,
        "pr_url": null
      }
    ],
    "present": true
  },
  "tasks": [],
  "main_inventory": {
    "valid": true,
    "reason": null,
    "orphan_in_flight": [],
    "unstructured_current_count": 0
  },
  "contributions": {
    "known": 0,
    "checked": 0,
    "counts": {
      "captain": 0,
      "fleet": 0,
      "maintainer": 0,
      "nobody": 0
    },
    "unmeasured": 0,
    "complete": true,
    "proven_clear": true,
    "stale_verdicts": 0,
    "missing_verdicts": 0,
    "unreadable_records": 0,
    "valid_until": 900,
    "captain": [],
    "captain_omitted": 0
  },
  "scout_reports": [],
  "secondmate_current": {
    "registry": {
      "present": false,
      "available": true,
      "complete": true,
      "reason": null,
      "provenance": "registered-table",
      "path": "~/.no-mistakes/worktrees/acf4a767348a/01M3W33QQYV7FCM041WC2819ZB/.fm5712-live-v_y2kcd0/reasons/data/secondmates.md",
      "freshness": {
        "status": "fresh",
        "observed_at": "2026-10-01T17:13:39Z"
      },
      "records": [],
      "input_truncated": false,
      "records_truncated": false,
      "reasons": [],
      "lines_in_window": 0,
      "records_in_window": 0
    },
    "records": [],
    "total_registered": 0,
    "total": 0,
    "shown": 0,
    "truncated": 0
  },
  "secondmate_landed": {
    "records": [],
    "truncated": [],
    "unreadable": [],
    "partial": []
  },
  "secondmate_guidance": {
    "note": "For kind=secondmate, bearings selects validated structured state from that registered home; parent events and bounded terminal evidence are fallback-only supplements and never current-state authority."
  }
}
Evidence: Validation results and cleanup record

Source: Validation results and cleanup record

{
  "base": "8f756bbc287c5bdfacc64a7cc09e8516c64fc919",
  "target": "fadcf45b24e4ea3d9103e6d118431222c8e2e6bf",
  "target_tree": "ad066a12eca435b4f221d9be801d076ebdc6a34d",
  "tasks_axi_version": "0.2.6",
  "backend": "markdown",
  "source_changed": false,
  "disposable_labs_removed": true,
  "results": [
    {
      "name": "circular-inventory",
      "result": "pass",
      "live": true,
      "evidence": "~/.no-mistakes/evidence/01M3W33QQYV7FCM041WC2819ZB/circular-inventory.log",
      "reason": ""
    },
    {
      "name": "legacy-origin-fallback",
      "result": "pass",
      "live": true,
      "evidence": "~/.no-mistakes/evidence/01M3W33QQYV7FCM041WC2819ZB/legacy-origin-fallback.log",
      "reason": ""
    },
    {
      "name": "reason-roundtrip-and-public-readers",
      "result": "pass",
      "live": true,
      "evidence": "~/.no-mistakes/evidence/01M3W33QQYV7FCM041WC2819ZB/reason-roundtrip-and-public-readers.log",
      "reason": ""
    },
    {
      "name": "origin-write-failure",
      "result": "pass",
      "live": true,
      "evidence": "~/.no-mistakes/evidence/01M3W33QQYV7FCM041WC2819ZB/origin-write-failure.log",
      "reason": ""
    },
    {
      "name": "origin-association-and-rehold",
      "result": "pass",
      "live": true,
      "evidence": "~/.no-mistakes/evidence/01M3W33QQYV7FCM041WC2819ZB/origin-association-and-rehold.log",
      "reason": ""
    },
    {
      "name": "answered-inventory",
      "result": "pass",
      "live": true,
      "evidence": "~/.no-mistakes/evidence/01M3W33QQYV7FCM041WC2819ZB/answered-inventory.log",
      "reason": ""
    },
    {
      "name": "startup-fallback-reader",
      "result": "pass",
      "live": true,
      "evidence": "~/.no-mistakes/evidence/01M3W33QQYV7FCM041WC2819ZB/startup-fallback-reader.log",
      "reason": ""
    },
    {
      "name": "startup-and-return-readers",
      "result": "pass",
      "live": true,
      "evidence": "~/.no-mistakes/evidence/01M3W33QQYV7FCM041WC2819ZB/startup-and-return-readers.log",
      "reason": ""
    },
    {
      "name": "baseline-reproduction",
      "result": "pass",
      "live": true,
      "evidence": "~/.no-mistakes/evidence/01M3W33QQYV7FCM041WC2819ZB/baseline-reproduction.log",
      "reason": ""
    }
  ],
  "scope": "Live public CLI commands only; no full suite, linters, static analysis, pipeline control, publication, or CI. Startup reason rendering was driven via its real read-only digest; no harness primary was launched.",
  "driver_setup_corrections": "Corrected answer CLI flags, a startup banner assertion, and the unavailable-adapter setup before re-driving affected cases."
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 5 issues found → auto-fixed (4) ✅
  • 🚨 bin/fm-captain-hold.sh:956 - A failed re-hold can certify the wrong origin. Hold task h for A, release it with a recorded answer, then re-hold h for B. This writes B before the backend hold succeeds; if that operation fails, complete B h accepts A's old answer with B's new association. Publish the association only with a successfully established hold. Related sites: bin/fm-captain-hold.sh:831 (origin write), :959 and :962 (fallible hold operations), :862 (durability/origin check), :1727 and :1794 (complete/verify consumers).
  • 🚨 bin/fm-captain-hold.sh:894 - The required reason text must be “stored and read back unchanged,” but the diff passes --reason "$stored_reason" to tasks-axi while the routine public reader, bin/fm-tasks-axi.sh:140, still directly executes tasks-axi. Holding with reason '(north)\nNext' therefore exposes '%28north%29%0ANext' through show/list. Restore decoding at the shared public read boundary. Related changed sites: bin/fm-captain-hold.sh:959 and :962 (both writers), docs/captain-hold-lifecycle.md:67 (decoding promise), tests/fm-captain-hold-lifecycle.test.sh:4252 (round-trip assertion covers only fleet JSON).
  • ⚠️ bin/fm-hold-reason-lib.sh:47 - The blanket decoder changes text that this encoder never wrote. A task titled 'Investigate literal %28' becomes 'Investigate literal (' in startup output; an older hold containing a URL with %28 likewise changes in fleet JSON. No intent requirement calls for interpreting unrelated fields or legacy plain text as encoded reasons. Remove that blanket acceptance and restrict decoding to identifiable encoded reason fields. Related sites: bin/fm-session-start.sh:555 (entire listing), bin/fm-afk-return.sh:597 (entire rows), bin/fm-fleet-snapshot.sh:432 (all historical reasons), bin/fm-hold-reason-lib.sh:35 (parallel decoder).
  • ⚠️ bin/fm-captain-hold.sh:849 - The advertised recovery does not work for existing self-inventories. Before this change, complete o o persisted decision_keys=o. After upgrading, creating separate task c and following this message with complete o c still unions o into the inventory and refuses forever; --none also retains it. Distinguish this historical-record case and provide workable repair instructions while preserving self-inventory rejection. Related sites: bin/fm-captain-hold.sh:859 (resolved identity check), :1727 (verification of the union), :1794 (verify also blocks cleanup).
  • ⚠️ bin/fm-hold-reason-lib.sh:33 - Simplification: fm_hold_reason_decode has no callers. Production readers independently implement decoding in the stream filter and fleet jq expression, so this adds an unused parallel definition without satisfying an additional intent requirement. Remove the unused function from the final implementation.

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

  • 🚨 bin/fm-captain-hold.sh:956 - A failed re-hold can certify the wrong origin. Hold task h for A, release it with a recorded answer, then re-hold h for B. This writes B before the backend hold succeeds; if that operation fails, complete B h accepts A's old answer with B's new association. Publish the association only with a successfully established hold. Related sites: bin/fm-captain-hold.sh:831 (origin write), :959 and :962 (fallible hold operations), :862 (durability/origin check), :1727 and :1794 (complete/verify consumers).
  • 🚨 bin/fm-captain-hold.sh:894 - The required reason text must be “stored and read back unchanged,” but the diff passes --reason "$stored_reason" to tasks-axi while the routine public reader, bin/fm-tasks-axi.sh:140, still directly executes tasks-axi. Holding with reason '(north)\nNext' therefore exposes '%28north%29%0ANext' through show/list. Restore decoding at the shared public read boundary. Related changed sites: bin/fm-captain-hold.sh:959 and :962 (both writers), docs/captain-hold-lifecycle.md:67 (decoding promise), tests/fm-captain-hold-lifecycle.test.sh:4252 (round-trip assertion covers only fleet JSON).
  • ⚠️ bin/fm-captain-hold.sh:849 - The advertised recovery does not work for existing self-inventories. Before this change, complete o o persisted decision_keys=o. After upgrading, creating separate task c and following this message with complete o c still unions o into the inventory and refuses forever; --none also retains it. Distinguish this historical-record case and provide workable repair instructions while preserving self-inventory rejection. Related sites: bin/fm-captain-hold.sh:859 (resolved identity check), :1727 (verification of the union), :1794 (verify also blocks cleanup).
  • 🚨 bin/fm-captain-hold.sh:859 - Self-inventory remains reachable through supported Beads aliases. With captain-held origin fm-o and no recorded origin, complete fm-o o succeeds: resolve_entry returns the requested spelling o even when tasks-axi resolves it to fm-o. Both comparisons miss the identical row, and verify also accepts it. Compare backend-returned identities at verify_entry_durable. Related sites: bin/fm-captain-hold.sh:848 (initial comparison), :857 (requested identity), :865 (stored-origin comparison can also falsely reject equivalent spellings), :956 (origin storage), :1727 (complete), and :1794 (verify).
  • ⚠️ tests/fm-captain-hold-lifecycle.test.sh:4361 - Simplification: the Round 1 fixer introduced FM_TEST_ONLY, a new command-dispatch and early-exit mode. Neither issue fm-captain-hold: parentheses in --reason are refused without a remedy, and complete accepts the origin as its own inventory #5712 nor the approved R2/R3/R5 fixes require another test-selection option; the normal suite already invokes the regressions. Remove this added dispatch block to keep the fix round within the requested scope.

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

  • 🚨 bin/fm-captain-hold.sh:956 - A failed re-hold can certify the wrong origin. Hold task h for A, release it with a recorded answer, then re-hold h for B. This writes B before the backend hold succeeds; if that operation fails, complete B h accepts A's old answer with B's new association. Publish the association only with a successfully established hold. Related sites: bin/fm-captain-hold.sh:831 (origin write), :959 and :962 (fallible hold operations), :862 (durability/origin check), :1727 and :1794 (complete/verify consumers).
  • ⚠️ bin/fm-captain-hold.sh:849 - The advertised recovery does not work for existing self-inventories. Before this change, complete o o persisted decision_keys=o. After upgrading, creating separate task c and following this message with complete o c still unions o into the inventory and refuses forever; --none also retains it. Distinguish this historical-record case and provide workable repair instructions while preserving self-inventory rejection. Related sites: bin/fm-captain-hold.sh:859 (resolved identity check), :1727 (verification of the union), :1794 (verify also blocks cleanup).
  • 🚨 bin/fm-captain-hold.sh:859 - Self-inventory remains reachable through supported Beads aliases. With captain-held origin fm-o and no recorded origin, complete fm-o o succeeds: resolve_entry returns the requested spelling o even when tasks-axi resolves it to fm-o. Both comparisons miss the identical row, and verify also accepts it. Compare backend-returned identities at verify_entry_durable. Related sites: bin/fm-captain-hold.sh:848 (initial comparison), :857 (requested identity), :865 (stored-origin comparison can also falsely reject equivalent spellings), :956 (origin storage), :1727 (complete), and :1794 (verify).

🔧 Fix applied.
4 issues (3 errors, 1 warning) still open:

  • 🚨 bin/fm-captain-hold.sh:956 - A failed re-hold can certify the wrong origin. Hold task h for A, release it with a recorded answer, then re-hold h for B. This writes B before the backend hold succeeds; if that operation fails, complete B h accepts A's old answer with B's new association. Publish the association only with a successfully established hold. Related sites: bin/fm-captain-hold.sh:831 (origin write), :959 and :962 (fallible hold operations), :862 (durability/origin check), :1727 and :1794 (complete/verify consumers).
  • ⚠️ bin/fm-captain-hold.sh:849 - The advertised recovery does not work for existing self-inventories. Before this change, complete o o persisted decision_keys=o. After upgrading, creating separate task c and following this message with complete o c still unions o into the inventory and refuses forever; --none also retains it. Distinguish this historical-record case and provide workable repair instructions while preserving self-inventory rejection. Related sites: bin/fm-captain-hold.sh:859 (resolved identity check), :1727 (verification of the union), :1794 (verify also blocks cleanup).
  • 🚨 bin/fm-captain-hold.sh:859 - Self-inventory remains reachable through supported Beads aliases. With captain-held origin fm-o and no recorded origin, complete fm-o o succeeds: resolve_entry returns the requested spelling o even when tasks-axi resolves it to fm-o. Both comparisons miss the identical row, and verify also accepts it. Compare backend-returned identities at verify_entry_durable. Related sites: bin/fm-captain-hold.sh:848 (initial comparison), :857 (requested identity), :865 (stored-origin comparison can also falsely reject equivalent spellings), :956 (origin storage), :1727 (complete), and :1794 (verify).
  • 🚨 bin/fm-captain-hold.sh:997 - Round 3 moved R1's partial-write defect. For a new hold h --origin B, the backend hold can succeed and the subsequent origin lookup or body update fail. The remaining captain-held row has no recorded origin, so complete A h accepts it through the legacy fallback despite A being unrelated. Re-holds can similarly retain a stale association. Related sites: bin/fm-captain-hold.sh:983 and bin/fm-captain-hold.sh:986 (hold writes), bin/fm-captain-hold.sh:825 and bin/fm-captain-hold.sh:843 (subsequent failures), bin/fm-captain-hold.sh:882 and bin/fm-captain-hold.sh:888 (origin checks), bin/fm-captain-hold.sh:1754 and bin/fm-captain-hold.sh:1821 (complete/verify consumers). Make incomplete origin-bearing holds distinguishable and reject them at verify_entry_durable until finalized. The remedy needs approval because reliably distinguishing interrupted writes requires durable transition state or an atomic storage change.

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

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
A failed hold cannot be concealed by completing an origin with itself, even when that origin is already held. ✅ pass live Live CLI transcripts: circular-inventory
A historical circular inventory remains refused until the documented repair replaces it with a separate held call. ✅ pass live Live CLI transcripts: circular-inventory
Completion accepts the recorded origin and rejects another; active and released re-holds replace the previous association. ✅ pass live Live CLI transcripts: origin-association-and-rehold
An actual origin-write storage failure leaves new tasks unheld, with and without a deferral date; retry succeeds after storage restrictions are removed. ✅ pass live Live CLI transcripts: origin-write-failure
Legacy durable calls remain accepted with an explicit no-origin warning, while answered calls retain recorded-origin enforcement. ✅ pass live Live CLI transcripts: legacy-origin-fallback and answered-inventory
Punctuation, Unicode and multiline reasons round-trip through show, view, list and fleet output without changing titles, legacy reasons or unrelated fields. ✅ pass live Live CLI transcripts: reason-roundtrip-and-public-readers; Fleet output with decoded reasons
Startup and return summaries display decoded reasons while preserving literal percent and encoding-marker text. ✅ pass live Live CLI transcripts: startup-and-return-readers
When the configured task adapter is unavailable, startup's manual fallback still displays the original reason. ✅ pass live Live CLI transcripts: startup-fallback-reader
  • python3 ~/.no-mistakes/evidence/01M3W33QQYV7FCM041WC2819ZB/live-validation.py, with targeted reruns after correcting driver setup.
  • git archive 8f756bbc287c5bdfacc64a7cc09e8516c64fc919 bin .tasks.toml: executed the baseline commands and reproduced both original defects.
  • Real fm-captain-hold.sh hold, answer, complete and verify, including process-local RLIMIT_FSIZE failure injection.
  • Real fm-tasks-axi.sh show/view/list, fm-fleet-snapshot.sh --json, fm-session-start.sh and fm-afk-return.sh check against disposable Markdown backlogs.
  • git status --porcelain and workspace-local lab cleanup verification.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Closes #5712

…mplete

hold now stores a reason with parentheses, line breaks, or percent signs
through a reversible percent encoding that every reader decodes, instead of
refusing it. hold --origin records the origin on the held task, and complete
refuses the origin as its own inventory entry and an entry held for a
different origin; holds with no recorded origin are accepted and flagged.
@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[High risk] Adds encoding library for captain-hold reasons across multiple scripts.

The PR does not appear safe to merge until the outstanding historical-reason read defect is addressed.

Reviews (2) · Last reviewed commit: "fix(ci): Fixed both findings: failed bac..."

Comment thread bin/fm-captain-hold.sh
Comment thread bin/fm-hold-reason-lib.sh Outdated
Comment on lines +29 to +32
sub decode_reason {
my ($value) = @_;
return $value unless defined($value) && $value =~ s/^fm-hold-v1://;
return decode("UTF-8", decode_base64($value), FB_CROAK);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Historical reasons can break reads

An older, literal hold reason beginning with fm-hold-v1: is treated as encoded data even if this encoder never wrote it. For example, fm-hold-v1:bm9ydGg= displays as north; if the suffix decodes to invalid UTF-8, public task reads fail and the fleet snapshot can fail to load.

…us origin, and invalid base64/UTF-8 reasons remain verbatim. Added regressions and documented valid-literal ambiguity. Both failures were reproduced before fixes. Verification: 54 lifecycle tests and 9 wrapper tests passed; 7 Beads-specific cases skipped because tasks-axi is markdown-only. Focused lint and diff checks passed. No pipeline or publication actions performed
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Verdict: Tip vs main 8f756bb reviewed. Body links #5712 but has no Closes #5712 / no GitHub closing reference — does NOT close an issue (add a closing keyword if that is the intent). Tip encodes hold reasons, records Captain hold origin, refuses self-inventory/wrong-origin on complete, restores previous origin on failed backend re-hold — restore matching ready-for-pr #5712.
VISION: aligns on all seven (authority explicit via recorded origin; scripts own hold/complete mechanics).
Greptile: Review FAILURE with two P1s; tip commit 3245997 (after review) appears to address both (origin restore + safe decode). Waiting CI/Greptile re-pass.
Attestation: MATCH. NM: SUCCESS. CI: in progress 36904390333. MERGEABLE/UNSTABLE. No auto-merge yet. Firstmate-flag no. Workflow approvals none this pass. Security tip none.

@mremond

mremond commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Reply to the review comment "Historical reasons can break reads" on bin/fm-hold-reason-lib.sh:

A stored reason that starts with fm-hold-v1: but whose payload is not valid base64, not canonical base64, or not valid UTF-8 is now displayed verbatim and never fails a read.
test_hold_reason_round_trips_awkward_characters covers four such payloads (fm-hold-v1:/w==, fm-hold-v1:bm9ydGg=$, fm-hold-v1:bm9ydGg, fm-hold-v1:Zh==) through the public show, view and list wrappers and the fleet snapshot, and it passes at the current head.

The remaining case, an older literal reason that happens to be a valid encoding such as fm-hold-v1:bm9ydGg=, still decodes.
That is an accepted and documented residual: no earlier writer used this prefix, so such a literal would have to have been typed by hand, and the code comment in fm-hold-reason-lib.sh records the limitation.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Verdict: Tip vs main 349e189f310fe477ed88a592592187c1585def55. Body links ready-for-pr #5712 (no Closes keyword — closing the issue after merge). Tip encodes hold reasons (fm-hold-v1: + base64), records captain-hold origin before backend hold and restores previous origin on failed re-hold, refuses origin-as-own-inventory on complete — restore of the hold/complete contract #5712 specified as broken.

VISION.md (each rule): all align — authority via recorded origin; scripts own hold/complete mechanics; no new default-on captain surface.

Greptile: P1 failed-re-hold thread resolved on tip. Historical-reason thread outdated; tip returns invalid base64/UTF-8 payloads verbatim (covered by tests); documented residual for hand-typed valid fm-hold-v1:… literals accepted. Greptile check still FAILURE — accepted after tip+author rebuttal.

Attestation: MATCH. CI/NM: SUCCESS CLEAN. mergeable: MERGEABLE. Merging squash now.

Firstmate flag: no. Security tip: none.

@kunchenguid
kunchenguid merged commit 6af8331 into kunchenguid:main Oct 1, 2026
20 of 21 checks passed
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: this is merged. Thank you @mremond — really appreciate you taking the time on this.

d-ploutarchos added a commit to OK-LG/firstmate that referenced this pull request Oct 2, 2026
* test: preserve Pi calm transcript captures with Pi 1.0 (kunchenguid#6338)

* test(calm): pin Pi's regular TUI mode where pane assertions read scrollback

Pi 1.0.0 defaults its TUI to a fullscreen alternate-screen mode whose
scrollable transcript is application-owned, so rows that leave the viewport
never enter terminal scrollback and tmux capture-pane -S can no longer see
them. The Pi Calm e2e launches now pass --tui-mode regular wherever the flag
exists so the transcript assertions keep reading real scrollback on both the
Pi 1.0.0 line and earlier Pi lines, which have no such flag and render
regular-only anyway.

* no-mistakes(document): Correct Pi TUI documentation and scrollback rationale

* fix(bin): preserve hold reasons and reject invalid completion inventories (kunchenguid#6331)

* fix(bin): encode captain-hold reasons and reject self-inventory in complete

hold now stores a reason with parentheses, line breaks, or percent signs
through a reversible percent encoding that every reader decodes, instead of
refusing it. hold --origin records the origin on the held task, and complete
refuses the origin as its own inventory entry and an entry held for a
different origin; holds with no recorded origin are accepted and flagged.

* fix(review): Decode marked hold reasons consistently across readers

* fix(review): Remove unnecessary lifecycle test dispatch

* fix(review): Correct hold origin identity and inventory recovery

* fix(review): Record origins before placing backend holds

* fix(document): Clarify captain-hold validation and reason reader documentation

* fix(ci): Fixed both findings: failed backend holds restore the previous origin, and invalid base64/UTF-8 reasons remain verbatim. Added regressions and documented valid-literal ambiguity. Both failures were reproduced before fixes. Verification: 54 lifecycle tests and 9 wrapper tests passed; 7 Beads-specific cases skipped because tasks-axi is markdown-only. Focused lint and diff checks passed. No pipeline or publication actions performed

* fix: reclaim orphaned watcher arms on the next park (kunchenguid#6335)

* fix(bin): take over the watcher cycle a main-only pass-through leaves

An attended main-only pass-through leaves a successor watcher cycle
running through main's handling turn. The session's next park attached
to that cycle instead of owning it, so the successor's arm, orphaned by
its host's exit, kept owning the watcher while the new park's arm polled
it twice a second until the next close or the park boundary, hours later
in a quiet second mate. A second-mate restart hit this every time, since
its persist request is a main-only close.

The host now records the successor it leaves for main, and the next
host's first cycle runs bin/fm-watch-arm.sh --take-over on it: when that
arm still owns the healthy watcher, the new arm stops it, reports a
reason the cycle delivered first, and otherwise owns a fresh cycle. The
stop's own downtime publication is undone over an acknowledged episode
when no wake was appended in between, so the handover wakes nobody.

* no-mistakes(review): Keep left-arm record until the orphaned arm is gone

* no-mistakes(review): Relinquish successor arm only after durably recording it

* no-mistakes(review): Relinquish successor only after its record reads back

* no-mistakes(document): Clarify successor takeover guarantees and authoritative documentation

* no-mistakes(ci): Fixed ci-1: acknowledgement restore now requires the exact taken-over arm/watcher ledger row with signal=TERM, awaited within a short bound. Otherwise takeover proceeds without erasing downtime. Added a self-exit regression confirmed failing before the fix and passing afterward; ordinary takeover tests and the full watcher-arm suite pass. Updated Generation reuse documentation. Syntax, diff checks, and ShellCheck pass with existing SC1091/SC2034 warnings excluded. ci-2 remains unchanged

* no-mistakes(document): Clarify watcher take-over recovery and restart limits

* fix(bin): restore downtime on supervision-host hand-back when the successor already closed (kunchenguid#6355)

* fix(bin): restore supervision host hand-back continuity

* no-mistakes(review): Scope host hand-back failure fallback to lost pending:handling

* no-mistakes(review): Remove stray scratch test copy tests/.tmp-rest.test.sh

* no-mistakes(test): Initialise successor globals so early hand-back survives set -u

* no-mistakes(document): Document host hand-back downtime failure and Claude lost-handback notice

* no-mistakes(ci): I fixed the Greptile finding. The rule that must hold: when the supervision host hands back an actionable wake, its rewake is refused, and no watcher is healthy, the hand-back still has to reach main as a delivered notice. That must be true whether the recovery marker is `pending:handling` or `announced:handling`. Only one place applies this check: the lost hand-back fallback in `bin/fm-claude-stop-autoarm.sh`. **Fix:** that check now accepts both `pending:handling:*` and `announced:handling:*` tokens (a one-line change). Nothing else in the fallback changed: - Refusals on any other marker, such as an already acknowledged one, still exit 0 silently and open no failure episode. - The notice is still sent once per episode, and repeats are recorded as `failed-suppressed`. **Tests:** - `tests/fm-claude-stop-autoarm.test.sh`: the lost hand-back test now runs as a shared helper with two variants, one writing a `pending:handling` marker and a new one writing `announced:handling` (`test_host_lost_announced_handback_notifies_once_per_episode`). - `tests/fm-supervision-host.test.sh`: the end-to-end test where downtime restoration fails is now a shared helper too, with a new `announced` variant (`test_claude_stop_hook_notifies_when_closed_announced_successor_downtime_restore_fails`). It moves the handling episode to `announced` before the host hands back. Without the fix the hook would exit 0 here; the test requires exit 2, `outcome=failed` and a delivered failure notice. **Verification (all under nice -n 10):** - The full `tests/fm-claude-stop-autoarm.test.sh` suite passed (rc=0), including both lost hand-back variants and the benign-refusal test. - In `tests/fm-supervision-host.test.sh` I ran only the four hand-back test functions, all passing (rc=0). The suite can't run single functions, so I used a temporary copy with a trimmed test list and deleted it afterwards; `git status` shows only the 3 intended files changed. - shellcheck is clean on all three changed files. I did not run `bin/fm-lint.sh`. - I did not run the new tests against the unfixed code; the claim that they fail without the fix comes from reading the old check

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Mickaël Rémond <mremond@process-one.net>
Co-authored-by: Tiago <tiagop@hey.com>
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.

fm-captain-hold: parentheses in --reason are refused without a remedy, and complete accepts the origin as its own inventory

2 participants