Skip to content

fix(bin): keep a declared pause absorbed and make the listed decision key the one that closes it - #2749

Open
Tiraso12 wants to merge 9 commits into
kunchenguid:mainfrom
Tiraso12:fm/firstmate-land-unpublished-fixes
Open

fix(bin): keep a declared pause absorbed and make the listed decision key the one that closes it#2749
Tiraso12 wants to merge 9 commits into
kunchenguid:mainfrom
Tiraso12:fm/firstmate-land-unpublished-fixes

Conversation

@Tiraso12

@Tiraso12 Tiraso12 commented Aug 21, 2026

Copy link
Copy Markdown

Intent

Fix the existing open PR #2749 so it no longer conflicts with main, keeping both unique fixes (fix(watcher): keep a live crew's declared pause absorbed; fix(bin): make the listed decision key the one that closes it) plus the pipeline doc commit, rebased onto latest origin/main which already includes the merged #2748 hold-cadence work. Now with no-mistakes v1.53.0: drive the pipeline so the PR body gets the structured pipeline attestation comment (no-mistakes-pipeline-attestation:v1 HTML comment) that 1.46+ writes, alongside the existing 'Updates from git push no-mistakes' line, so the required 'PR must be raised via no-mistakes' check passes. Do not change product code unless the new pipeline requires a fix. Do not restore refuse-before-send behavior. Do not merge the PR.

What Changed

  • bin/fm-watch.sh: pause_state_class now treats a crew's own paused:/captain-held declaration as authoritative instead of re-reading agent liveness on every poll. A crew the authoritative read already names paused stays absorbed even while its agent is alive, the cheap .paused-<key> cadence holds without any liveness read until the recheck marker ages past STALE_ESCALATE_SECS, and window_kind is only read on the poll that actually re-decides a declared wait - so a live crew holding a dev server no longer emits a contentless stale wake on each repaint.
  • bin/fm-wake-drain.sh: every OPEN DECISIONS entry renders the key the fold actually decided (including the default bucket a keyless line opens) plus its own ready-to-run bin/fm-send.sh <task> --resolve-key <key> close command, so the listing can no longer advertise a [key=...] token from the note prose that fm-send would reject. Task ids that are unsafe to paste - outside the plain-slug charset or leading-dash - get a pointer instead of a command, and an entry's note and command are charged to the section byte budget together so nothing is listed without a way to close it.
  • bin/fm-send.sh: a --resolve-key naming a key that is not open no longer refuses before sending; it warns pre-send, delivers the answer, closes whichever other named keys are open, then exits nonzero with a "delivered, do not resend" diagnostic. Malformed usage (bad charset, duplicate key, --key, explicit backend target, empty message) is still refused outright, the exit-status contract in the header now documents the post-delivery nonzero paths, and bin/fm-brief.sh plus docs/architecture.md were updated to match. Tests for all three scripts were extended, including an end-to-end case that replays the drain's printed close command verbatim through the real fm-send.

Risk Assessment

⚠️ Medium: The two intended fixes are well-scoped, correctly rebased onto origin/main (which already carries #2748), and covered by tests that drive the real drain and real fm-send end to end; the single confirmed gap - reserved-namespace rows advertising a close command that cannot close - leaves the ledger in the safe (still-open) direction and is a reasonable follow-up rather than a merge blocker.

Testing

Ran the four suites covering the changed surfaces plus five neighbouring ones (all pass), then proved each fix is a real regression by reverting only the three product scripts to the base commit and watching the new tests go red. Product-level evidence comes from two before/after CLI transcripts driving the real drain, send and watcher scripts: the OPEN DECISIONS listing now prints the key the fold decided with its own close command, and running that command verbatim closes the decision, while a note-prose key delivers the answer and exits nonzero instead of refusing to send; the live declared-pause scenario drops from 8 contentless stale wakes out of 8 repaints to 0, with the bounded long-cadence re-surface still firing once. No tmux exists on this host, so the terminal backend and fm-crew-state are stubbed exactly as the repo's own suites stub them; the change has no rendered UI surface, so no screenshots apply. The pipeline-attestation comment required by the intent is produced by the push/PR phases and is outside this phase's ownership.

Evidence: Evidence index (what each artifact shows, how to reproduce)

Source: Evidence index (what each artifact shows, how to reproduce)

# Evidence: firstmate PR #2749 rebase (fm/firstmate-land-unpublished-fixes)

base `1231b6ae` -> target `e3c595a`. Two product fixes plus a docs commit.
Every artifact below drives the REAL `bin/fm-wake-drain.sh`, `bin/fm-send.sh`
and `bin/fm-watch.sh`; only the terminal backend (tmux) and `fm-crew-state.sh`
are stubbed, because no tmux binary exists on this host.

## fix(bin): make the listed decision key the one that closes it

`decision-key-e2e-transcript.txt` - the captain-facing CLI surface, before and
after, over the same fleet state (an inline-keyed decision, a keyless one, and
one whose *note text* contains `[key=red]`).

- BEFORE: the listing shows no key at all for two of the three decisions and a
  generic `--resolve-key <key>` hint; the only key on screen for `theme` is the
  `[key=red]` inside the note. Answering with it is refused and the captain's
  answer is dropped entirely (`text typed into the crew pane: ''`).
- AFTER: every entry renders the key the fold actually decided plus its own
  ready-to-run close command. Running the printed command verbatim delivers the
  answer, appends `resolved [key=totals-pool-separation]`, and the decision is
  gone from the next listing. The note-prose key still closes nothing, but the
  answer IS delivered, exit 1, with "Do not resend the answer" on stderr, and
  the decision stays listed.

Reproduce: `./fm-decision-key-e2e.sh <bin-dir> <label>`

## fix(watcher): keep a live crew's declared pause absorbed

`live-paused-churn-e2e-transcript.txt` - the 2026-08-12 report replayed: a LIVE
crew holds a running dev server and declares `paused:` while the captain
reviews. The dev server repaints, so each poll settles on a stale pane hash the
watcher has never triaged. One long-lived watcher, re-armed only when it
actually exits, exactly as firstmate does after handling a wake.

- BEFORE: 8 of 8 repaints woke firstmate with a contentless `stale: test:fm-held`,
  and 8 such rows landed in the durable wake queue.
- AFTER: 0 of 8. The wake queue stays empty and the watcher's own triage log
  records `absorbed stale (paused, awaiting external, ...)`.
- Absorbed, not silenced: with the declared wait backdated past the bounded
  window the same live crew re-surfaces exactly once, labeled
  `awaiting external - declared pause, rechecked on a long cadence not a wedge`.
- The trailing `check: rearm-resurface` on the BEFORE run is a harness artifact
  (that run's last watcher was killed by the driver, leaving a recovery marker),
  not product behavior.

Reproduce: `./fm-paused-churn-e2e.sh <bin-dir> <label>`

## Regression proof (fails before, passes after)

With the tests at the target commit and only `bin/fm-watch.sh`,
`bin/fm-wake-drain.sh`, `bin/fm-send.sh` reverted to the base commit:

- `before-fix-open-decisions.txt` - `not ok - open section is missing the decision's own answerer-closes command`
- `before-fix-send-resolve-key.txt` - `not ok - the diagnostic should say the answer already landed (missing: 'Do not resend the answer')`
- `before-fix-watch-triage.txt` - `not ok - a live declared pause woke firstmate on churny stale hash 1: stale: test:fm-held`

All three pass at the target commit.
Evidence: Decision-key E2E transcript (before/after, real drain + send)

Source: Decision-key E2E transcript (before/after, real drain + send)

BEFORE (base 1231b6ae) $ bin/fm-wake-drain.sh OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line): banner needs-decision: which banner color for the launch header theme needs-decision: pick a [key=red] or [key=blue] theme toy-store [key=totals-pool-separation] needs-decision: display-only or engine bug OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>' $ bin/fm-send.sh theme --resolve-key red "go with red" exit: 1 error: --resolve-key 'red': no open decision or blocker with that key ... nothing was sent. text typed into the crew pane: '' <-- the captain's answer was dropped AFTER (target e3c595a) $ bin/fm-wake-drain.sh OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line): banner [key=default] needs-decision: which banner color for the launch header close it: bin/fm-send.sh banner --resolve-key default '<answer>' theme [key=default] needs-decision: pick a [key=red] or [key=blue] theme close it: bin/fm-send.sh theme --resolve-key default '<answer>' toy-store [key=totals-pool-separation] needs-decision: display-only or engine bug close it: bin/fm-send.sh toy-store --resolve-key totals-pool-separation '<answer>' OPEN DECISIONS: answer each one with the close command printed under it; the key that closes a decision is the bracketed key before its verb, never a [key=...] inside the note text. $ bin/fm-send.sh toy-store --resolve-key totals-pool-separation "engine bug - fix the pool split" # the printed command, verbatim exit: 0 text typed into the crew pane: engine bug - fix the pool split toy-store status log now: resolved [key=totals-pool-separation]: answered: engine bug - fix the pool split $ bin/fm-send.sh theme --resolve-key red "go with red" exit: 1 error: the answer was delivered to sess:fm-theme, but no decision was closed for --resolve-key 'red' ... Do not resend the answer. text typed into the crew pane: 'go with red' <-- delivered, closes nothing

############################################################
# BEFORE the fix (base 1231b6ae)
############################################################

$ bin/fm-wake-drain.sh          # what the captain's firstmate actually reads
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
banner needs-decision: which banner color for the launch header
theme needs-decision: pick a [key=red] or [key=blue] theme
toy-store [key=totals-pool-separation] needs-decision: display-only or engine bug
OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'

--- the captain answers the totals decision -------------------
$ bin/fm-send.sh toy-store --resolve-key totals-pool-separation "engine bug - fix the pool split"
exit: 0
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.
text typed into the crew pane: engine bug - fix the pool split
toy-store status log now:
    working: wiring the settle-up totals
    needs-decision: [key=totals-pool-separation] display-only or engine bug
    resolved [key=totals-pool-separation]: answered: engine bug - fix the pool split

--- the captain answers the theme decision with the key the note text advertises ---
$ bin/fm-send.sh theme --resolve-key red "go with red"
exit: 1
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.
error: --resolve-key 'red': no open decision or blocker with that key in /var/folders/h0/h_hy5c9x3vddzpxxk3_wzjwc0000gn/T/tmp.LmjFCs6S2x/home/state/theme.status, and no captain-held task 'red' or 'theme-decision-red' still open (already closed or mistyped). Re-check the OPEN DECISIONS listing, then resend without that key or with the right one; nothing was sent.
text typed into the crew pane: ''

--- drain again ------------------------------------------------
$ bin/fm-wake-drain.sh
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
banner needs-decision: which banner color for the launch header
theme needs-decision: pick a [key=red] or [key=blue] theme
OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'


############################################################
# AFTER the fix (target e3c595a)
############################################################

$ bin/fm-wake-drain.sh          # what the captain's firstmate actually reads
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
banner [key=default] needs-decision: which banner color for the launch header
  close it: bin/fm-send.sh banner --resolve-key default '<answer>'
theme [key=default] needs-decision: pick a [key=red] or [key=blue] theme
  close it: bin/fm-send.sh theme --resolve-key default '<answer>'
toy-store [key=totals-pool-separation] needs-decision: display-only or engine bug
  close it: bin/fm-send.sh toy-store --resolve-key totals-pool-separation '<answer>'
OPEN DECISIONS: answer each one with the close command printed under it; the key that closes a decision is the bracketed key before its verb, never a [key=...] inside the note text.

--- the captain answers the totals decision -------------------
$ bin/fm-send.sh toy-store --resolve-key totals-pool-separation "engine bug - fix the pool split"
exit: 0
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.
text typed into the crew pane: engine bug - fix the pool split
toy-store status log now:
    working: wiring the settle-up totals
    needs-decision: [key=totals-pool-separation] display-only or engine bug
    resolved [key=totals-pool-separation]: answered: engine bug - fix the pool split

--- the captain answers the theme decision with the key the note text advertises ---
$ bin/fm-send.sh theme --resolve-key red "go with red"
exit: 1
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.
warning: --resolve-key 'red': no open decision or blocker with that key in /var/folders/h0/h_hy5c9x3vddzpxxk3_wzjwc0000gn/T/tmp.cQIp9KG0rC/home/state/theme.status, and no captain-held task 'red' or 'theme-decision-red' still open (already closed, mistyped, or transferred). This does not cancel the answer; nothing will be closed for that key, and the delivery outcome is reported when the send completes.
error: the answer was delivered to sess:fm-theme, but no decision was closed for --resolve-key 'red' (not open in /var/folders/h0/h_hy5c9x3vddzpxxk3_wzjwc0000gn/T/tmp.cQIp9KG0rC/home/state/theme.status). Do not resend the answer. Re-check the OPEN DECISIONS listing and close the real key with its printed command, or leave it open if it is already closed.
text typed into the crew pane: 'go with red'

--- drain again ------------------------------------------------
$ bin/fm-wake-drain.sh
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
banner [key=default] needs-decision: which banner color for the launch header
  close it: bin/fm-send.sh banner --resolve-key default '<answer>'
theme [key=default] needs-decision: pick a [key=red] or [key=blue] theme
  close it: bin/fm-send.sh theme --resolve-key default '<answer>'
OPEN DECISIONS: answer each one with the close command printed under it; the key that closes a decision is the bracketed key before its verb, never a [key=...] inside the note text.
Evidence: Live declared-pause churn E2E transcript (before/after, real watcher)

Source: Live declared-pause churn E2E transcript (before/after, real watcher)

crew 'held' status log: paused: holding the dev server while the captain reviews crew is LIVE (pane_current_command=grok), pane repaints once per second BEFORE (base 1231b6ae) - 8 pane repaints, one long-lived watcher repaint 1: WOKE FIRSTMATE -> stale: test:fm-held repaint 2: WOKE FIRSTMATE -> stale: test:fm-held ... (3-7 identical) ... repaint 8: WOKE FIRSTMATE -> stale: test:fm-held supervision turns burned on a crew that is fine: 8 of 8 total stale rows queued: 8 AFTER (target e3c595a) - same 8 repaints repaint 1: absorbed (watcher still blocking, captain not interrupted) ... (2-7 identical) ... repaint 8: absorbed (watcher still blocking, captain not interrupted) supervision turns burned on a crew that is fine: 0 of 8 (wake queue empty - no wake was ever queued) [2026-08-22T15:06:25-0400] absorbed stale (paused, awaiting external, age 49s): test:fm-held --- absorbed, not silenced: past the bounded window --- watcher printed: stale: test:fm-held (paused 501s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)

############################################################
# BEFORE the fix (base 1231b6ae)
############################################################
crew 'held' status log:  paused: holding the dev server while the captain reviews
crew is LIVE (pane_current_command=grok), pane repaints once per second
captain is reviewing; nothing is actually wrong

--- 8 pane repaints, one long-lived watcher (re-armed only if it exits) ---
repaint 1: WOKE FIRSTMATE -> stale: test:fm-held 
repaint 2: WOKE FIRSTMATE -> stale: test:fm-held 
repaint 3: WOKE FIRSTMATE -> stale: test:fm-held 
repaint 4: WOKE FIRSTMATE -> stale: test:fm-held 
repaint 5: WOKE FIRSTMATE -> stale: test:fm-held 
repaint 6: WOKE FIRSTMATE -> stale: test:fm-held 
repaint 7: WOKE FIRSTMATE -> stale: test:fm-held 
repaint 8: WOKE FIRSTMATE -> stale: test:fm-held 

supervision turns burned on a crew that is fine: 8 of 8

--- durable wake queue rows for this window --------------------
  stale	stale: test:fm-held
  stale	stale: test:fm-held
  stale	stale: test:fm-held
  stale	stale: test:fm-held
  stale	stale: test:fm-held
  stale	stale: test:fm-held
  stale	stale: test:fm-held
  stale	stale: test:fm-held
  total stale rows queued: 8

--- watcher's own triage log -----------------------------------
  (none)

--- and the pause is absorbed, not silenced: past the bounded window ---
  watcher printed: check: rearm-resurface 


############################################################
# AFTER the fix (target e3c595a)
############################################################
crew 'held' status log:  paused: holding the dev server while the captain reviews
crew is LIVE (pane_current_command=grok), pane repaints once per second
captain is reviewing; nothing is actually wrong

--- 8 pane repaints, one long-lived watcher (re-armed only if it exits) ---
repaint 1: absorbed (watcher still blocking, captain not interrupted)
repaint 2: absorbed (watcher still blocking, captain not interrupted)
repaint 3: absorbed (watcher still blocking, captain not interrupted)
repaint 4: absorbed (watcher still blocking, captain not interrupted)
repaint 5: absorbed (watcher still blocking, captain not interrupted)
repaint 6: absorbed (watcher still blocking, captain not interrupted)
repaint 7: absorbed (watcher still blocking, captain not interrupted)
repaint 8: absorbed (watcher still blocking, captain not interrupted)

supervision turns burned on a crew that is fine: 0 of 8

--- durable wake queue rows for this window --------------------
  (empty - no wake was ever queued)

--- watcher's own triage log -----------------------------------
  lines: 26
  [2026-08-22T15:06:19-0400] absorbed stale (paused, awaiting external, age 43s): test:fm-held
  [2026-08-22T15:06:21-0400] absorbed stale (paused, awaiting external, age 45s): test:fm-held
  [2026-08-22T15:06:24-0400] absorbed stale (paused, awaiting external, age 48s): test:fm-held
  [2026-08-22T15:06:25-0400] absorbed stale (paused, awaiting external, age 49s): test:fm-held

--- and the pause is absorbed, not silenced: past the bounded window ---
  watcher printed: stale: test:fm-held (paused 501s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) 
  queued: 1787425587	1	stale	test:fm-held	stale: test:fm-held (paused 501s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)
Evidence: Regression proof: drain test red against pre-fix code

Source: Regression proof: drain test red against pre-fix code

not ok - open section is missing the decision's own answerer-closes command

FM_TEST_BEGIN 2026-08-22T18:55:13Z tests/fm-wake-drain-open-decisions.test.sh family=unclassified expected_gate_skip=none
not ok - open section is missing the decision's own answerer-closes command
FM_TEST_END 2026-08-22T18:55:14Z tests/fm-wake-drain-open-decisions.test.sh exit=1 duration_ms=875 gate_skip=false
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=933
FM_TEST_SUMMARY_FAMILY family=unclassified count=1 duration_ms=875 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-wake-drain-open-decisions.test.sh duration_ms=875
Evidence: Regression proof: fm-send test red against pre-fix code

Source: Regression proof: fm-send test red against pre-fix code

not ok - the diagnostic should say the answer already landed (missing: 'Do not resend the answer')

FM_TEST_BEGIN 2026-08-22T18:55:20Z tests/fm-send-resolve-key.test.sh family=backend-dispatch expected_gate_skip=none
ok - fm-send --resolve-key: the answer send itself closes the open decision
ok - fm-send --resolve-key: the close never re-wakes its own home, later lines still do
ok - fm-send --resolve-key: a colon-first stated key is open under that key and answerable
ok - fm-send --resolve-key: an answer that starts a workstream leaves no orphaned decision
ok - fm-send: a send without --resolve-key never closes a decision, and working/done still cannot
not ok - the diagnostic should say the answer already landed (missing: 'Do not resend the answer')
--- output ---
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the requested message WILL still be sent.
●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
error: --resolve-key 'mistyped': no open decision or blocker with that key in /var/folders/h0/h_hy5c9x3vddzpxxk3_wzjwc0000gn/T//fm-send-resolve-key.C6rCjh/not-open-2166/state/t4.status, and no captain-held task 'mistyped' or 't4-decision-mistyped' still open (already closed or mistyped). Re-check the OPEN DECISIONS listing, then resend without that key or with the right one; nothing was sent.
FM_TEST_END 2026-08-22T18:55:27Z tests/fm-send-resolve-key.test.sh exit=1 duration_ms=6830 gate_skip=false
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=6886
FM_TEST_SUMMARY_FAMILY family=backend-dispatch count=1 duration_ms=6830 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-send-resolve-key.test.sh duration_ms=6830
Evidence: Regression proof: watcher test red against pre-fix code

Source: Regression proof: watcher test red against pre-fix code

not ok - a live declared pause woke firstmate on churny stale hash 1: stale: test:fm-held

FM_TEST_BEGIN 2026-08-22T18:55:32Z tests/fm-watch-triage.test.sh family=watcher-wake-lock expected_gate_skip=none
ok - signal_reason_is_actionable: benign absorbed, captain verbs and coalesced batches surfaced
ok - stale_is_terminal: terminal status surfaces, non-terminal and no-status are benign
ok - scan_captain_relevant_statuses lists only captain-relevant statuses
ok - classifier primitives: keyed decisions and activity phases, captain relevance, window-to-task, and overrides
ok - crew_is_provably_working: only working+run-step/pane is provable; idle/finished/parked/failed/unknown surface
ok - status_is_paused: only the leading paused verb matches, paused is not captain-relevant, and the two declared-wait verbs stay separable
ok - crew_absorb_class: working/paused/none from one read; crew_is_paused and crew_is_provably_working agree
ok - crew_worktree_written_since: real writes are evidence; no worktree, no anchor, quiet trees, .git churn and a mate's own home are not
ok - an empty FM_WORKTREE_WRITE_PRUNE widens the probe to the whole depth-bounded tree instead of disabling it
ok - an empty FM_WORKTREE_WRITE_PRUNE exported into the environment prunes nothing, widening the probe
ok - the worktree write probe is wall-clock bounded, and hitting the bound reads as no write evidence
ok - signal_crew_provably_working: benign only when every referenced crew is provably working
ok - a secondmate's status signal is never absorbed as provably working; crewmates are unaffected
ok - a no-verb signal whose crew is provably working is absorbed (no exit, no queue, suppressor advanced, beacon present)
ok - a bare turn-end whose crew is provably working (busy pane) is absorbed
ok - a bare turn-end whose crew is not provably working is surfaced (the swallowed-finish fix)
ok - a no-verb working: note whose crew is idle with no running pipeline is surfaced
ok - a secondmate's status note surfaces even while its own agent is busy
ok - a self-announced close never wakes its own home, and the next real note still does
ok - captain-relevant signal is surfaced (queue + exit) and marked surfaced
ok - a stale pane sitting on a terminal status is surfaced (queue + exit)
ok - a stale terminal-looking status is overridden and absorbed while a run is actively working, then wedge-escalated
ok - provably-working non-terminal stale is absorbed on first sight, then wedge-escalated past the threshold
ok - consecutive wedge escalations on the same pane accumulate and demand deep inspection at the threshold
ok - a pane becoming active again resets the consecutive wedge-escalation counter
ok - a busy worker below the turn-age bound remains working with no escalation
ok - a busy worker with a stable pane hash still escalates once its completed-turn age reaches the bound
ok - a busy worker whose pane hash changes every poll still escalates once its completed-turn age reaches the bound
ok - touching a busy worker's completed-turn marker resets the age and prevents an old-age escalation
ok - repeated busy turn-age escalations reuse the existing escalation counter and demand deep inspection at the threshold
ok - the production default busy-turn-age bound is 3600s (5min under does not wedge, 66min over does)
ok - a busy pane under a declared pause is rechecked on the long cadence, and lifting the pause restores the wedge escalation
ok - a not-provably-working non-terminal stale is surfaced immediately (never left to wait out the timer)
ok - a declared pause is absorbed on first sight, then re-surfaced as a recheck past the threshold, never wedge-escalated
ok - exited declared-pause and captain-held panes use bounded pause cadence while a live captain-held gate still surfaces once
not ok - a live declared pause woke firstmate on churny stale hash 1: stale: test:fm-held
FM_TEST_END 2026-08-22T18:57:05Z tests/fm-watch-triage.test.sh exit=1 duration_ms=92720 gate_skip=false
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=92780
FM_TEST_SUMMARY_FAMILY family=watcher-wake-lock count=1 duration_ms=92720 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-watch-triage.test.sh duration_ms=92720
Evidence: Reproduction script: decision-key E2E

Source: Reproduction script: decision-key E2E

#!/usr/bin/env bash
# End-to-end demo of "the listed decision key is the one that closes it".
# Drives the REAL bin/fm-wake-drain.sh and bin/fm-send.sh over a crafted fleet
# state, with a fake tmux endpoint standing in for a live crew pane.
# usage: fm-decision-key-e2e.sh <bin-dir> <label>
set -u
BIN=$1; LABEL=$2
WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT
HOME_DIR="$WORK/home"; STATE="$HOME_DIR/state"; FB="$WORK/fakebin"
mkdir -p "$STATE" "$FB"
export FM_GATE_REFUSE_BYPASS=1

cat > "$FB/tmux" <<'SH'
#!/usr/bin/env bash
set -u
case "${1:-}" in
  send-keys)
    shift; literal=0
    while [ $# -gt 0 ]; do
      case "$1" in -t) shift 2 ;; -l) literal=1; shift ;; *) break ;; esac
    done
    [ "$literal" = 1 ] && printf '%s' "${1:-}" >> "$FM_SEND_LOG"
    exit 0 ;;
  display-message)
    for a in "$@"; do case "$a" in *cursor_y*) printf '1\n'; exit 0 ;; esac; done
    printf 'fakepane\n'; exit 0 ;;
  capture-pane) printf '\u256d\u2500\u2500\u2500\u2500\u256e\n\u2502    \u2502\n\u2570\u2500\u2500\u2500\u2500\u256f\n'; exit 0 ;;
esac
exit 0
SH
chmod +x "$FB/tmux"
printf '#!/usr/bin/env bash\nexit 0\n' > "$FB/sleep"; chmod +x "$FB/sleep"
LOG="$WORK/typed.txt"; : > "$LOG"

# Three crews report to the captain. Each writes its decision the way a real
# worker does, per the brief in bin/fm-brief.sh.
for t in toy-store banner theme; do
  printf 'window=sess:fm-%s\nkind=ship\n' "$t" > "$STATE/$t.meta"
done
printf 'working: wiring the settle-up totals\nneeds-decision: [key=totals-pool-separation] display-only or engine bug\n' > "$STATE/toy-store.status"
printf 'needs-decision: which banner color for the launch header\n' > "$STATE/banner.status"
printf 'needs-decision: pick a [key=red] or [key=blue] theme\n' > "$STATE/theme.status"

drain() { FM_STATE_OVERRIDE="$STATE" "$BIN/fm-wake-drain.sh" 2>/dev/null; }

echo "############################################################"
echo "# $LABEL"
echo "############################################################"
echo
echo '$ bin/fm-wake-drain.sh          # what the captain'"'"'s firstmate actually reads'
drain
echo
echo "--- the captain answers the totals decision -------------------"
echo '$ bin/fm-send.sh toy-store --resolve-key totals-pool-separation "engine bug - fix the pool split"'
: > "$LOG"
env PATH="$FB:$PATH" FM_ROOT_OVERRIDE="$HOME_DIR" FM_HOME="$HOME_DIR" FM_SEND_LOG="$LOG" \
  FM_SEND_SETTLE=0 "$BIN/fm-send.sh" toy-store --resolve-key totals-pool-separation "engine bug - fix the pool split" \
  >/dev/null 2>"$WORK/err1"; rc=$?
echo "exit: $rc"
grep -v -e '^●' -e '^$' "$WORK/err1" || true
echo "text typed into the crew pane: $(cat "$LOG")"
echo "toy-store status log now:"
sed 's/^/    /' "$STATE/toy-store.status"
echo
echo "--- the captain answers the theme decision with the key the note text advertises ---"
echo '$ bin/fm-send.sh theme --resolve-key red "go with red"'
: > "$LOG"
env PATH="$FB:$PATH" FM_ROOT_OVERRIDE="$HOME_DIR" FM_HOME="$HOME_DIR" FM_SEND_LOG="$LOG" \
  FM_SEND_SETTLE=0 "$BIN/fm-send.sh" theme --resolve-key red "go with red" \
  >/dev/null 2>"$WORK/err2"; rc=$?
echo "exit: $rc"
grep -v -e '^●' -e '^$' "$WORK/err2" || true
echo "text typed into the crew pane: '$(cat "$LOG")'"
echo
echo "--- drain again ------------------------------------------------"
echo '$ bin/fm-wake-drain.sh'
drain
Evidence: Reproduction script: live declared-pause churn E2E

Source: Reproduction script: live declared-pause churn E2E

#!/usr/bin/env bash
# End-to-end demo of "keep a live crew's declared pause absorbed".
# Reproduces the 2026-08-12 report: a LIVE crew holds a running dev server and
# declares `paused:` while waiting on the captain. The dev server repaints, so
# every watcher poll settles on a stale pane hash it has never triaged.
# Drives the REAL bin/fm-watch.sh, re-arming it after each surfaced wake exactly
# as firstmate does after handling one.
# usage: fm-paused-churn-e2e.sh <bin-dir> <label>
set -u
BIN=$1; LABEL=$2
WORK=$(mktemp -d)
STATE="$WORK/state"; FB="$WORK/fakebin"
mkdir -p "$STATE" "$FB"
export FM_GATE_REFUSE_BYPASS=1
WINDOW="test:fm-held"; KEY=$(printf '%s' "$WINDOW" | tr ':/.' '___')
PANE="$WORK/pane.txt"

cat > "$FB/tmux" <<'SH'
#!/usr/bin/env bash
set -u
case "${1:-}" in
  list-windows) [ -n "${FM_FAKE_TMUX_WINDOW:-}" ] && printf '%s\n' "${FM_FAKE_TMUX_WINDOW#*:}"; exit 0 ;;
  capture-pane) [ -n "${FM_FAKE_TMUX_CAPTURE:-}" ] && cat "$FM_FAKE_TMUX_CAPTURE"; exit 0 ;;
  display-message)
    case "$*" in *pane_current_command*) printf '%s\n' "${FM_FAKE_TMUX_CURRENT_COMMAND:-}"; exit 0 ;; esac ;;
esac
exit 1
SH
chmod +x "$FB/tmux"
cat > "$FB/fm-crew-state.sh" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "${FM_FAKE_CREW_STATE:-state: unknown}"
SH
chmod +x "$FB/fm-crew-state.sh"

printf 'window=%s\nkind=ship\nharness=grok\nbackend=tmux\n' "$WINDOW" > "$STATE/held.meta"
printf 'paused: holding the dev server while the captain reviews\n' > "$STATE/held.status"
# The captain has already read that paused: line; only the pane churn is new.
if [ "$(uname)" = Darwin ]; then sig=$(stat -f '%z:%Fm' "$STATE/held.status"); else sig=$(stat -c '%s:%Y' "$STATE/held.status"); fi
printf '%s' "$sig" > "$STATE/.seen-held_status"
printf 'dev server listening on :3000, GET / 200 (0)' > "$PANE"

echo "############################################################"
echo "# $LABEL"
echo "############################################################"
echo "crew 'held' status log:  $(cat "$STATE/held.status")"
echo "crew is LIVE (pane_current_command=grok), pane repaints once per second"
echo "captain is reviewing; nothing is actually wrong"
echo
echo "--- 8 pane repaints, one long-lived watcher (re-armed only if it exits) ---"
surfaced=0
WATCH_PID=
arm() {
  PATH="$FB:$PATH" FM_FAKE_TMUX_WINDOW="$WINDOW" FM_FAKE_TMUX_CAPTURE="$PANE" \
    FM_FAKE_TMUX_CURRENT_COMMAND=grok \
    FM_FAKE_CREW_STATE='state: paused · source: status-log · holding the dev server' \
    FM_STATE_OVERRIDE="$STATE" FM_CREW_STATE_BIN="$FB/fm-crew-state.sh" \
    FM_PAUSE_RESURFACE_SECS=3600 FM_STALE_ESCALATE_SECS=240 FM_POLL=1 FM_SIGNAL_GRACE=1 \
    FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$BIN/fm-watch.sh" > "$WATCH_OUT" 2>/dev/null &
  WATCH_PID=$!
}
WATCH_OUT="$WORK/watch.out"; : > "$WATCH_OUT"
arm
for round in 1 2 3 4 5 6 7 8; do
  printf 'dev server listening on :3000, GET / 200 (%s)' "$round" > "$PANE"
  i=0
  while [ "$i" -lt 40 ]; do
    kill -0 "$WATCH_PID" 2>/dev/null || break
    sleep 0.1; i=$((i + 1))
  done
  if kill -0 "$WATCH_PID" 2>/dev/null; then
    printf 'repaint %s: absorbed (watcher still blocking, captain not interrupted)\n' "$round"
  else
    wait "$WATCH_PID" 2>/dev/null || true
    surfaced=$((surfaced + 1))
    printf 'repaint %s: WOKE FIRSTMATE -> %s\n' "$round" "$(tr '\n' ' ' < "$WATCH_OUT")"
    # Firstmate handles the wake: drain the durable record, acknowledge it, re-arm.
    d="$WORK/drain.$round.out"
    FM_STATE_OVERRIDE="$STATE" "$BIN/fm-wake-drain.sh" > "$d" 2>/dev/null || true
    ack=$(sed -n 's/.*--ack-through \([0-9][0-9]*\) --recovery-generation \([^ ]*\).*/\1 \2/p' "$d" | tail -1)
    if [ -n "$ack" ]; then
      # shellcheck disable=SC2086
      set -- $ack
      FM_STATE_OVERRIDE="$STATE" "$BIN/fm-wake-drain.sh" --ack-through "$1" --recovery-generation "$2" >/dev/null 2>&1 || true
    fi
    : > "$WATCH_OUT"
    arm
  fi
done
kill "$WATCH_PID" 2>/dev/null || true; wait "$WATCH_PID" 2>/dev/null || true
echo
echo "supervision turns burned on a crew that is fine: $surfaced of 8"
echo
echo "--- durable wake queue rows for this window --------------------"
if [ -s "$STATE/.wake-queue" ]; then
  awk -F '\t' -v w="$WINDOW" '$4 == w { print "  " $3 "\t" $5 }' "$STATE/.wake-queue"
  awk -F '\t' -v w="$WINDOW" '$3 == "stale" && $4 == w { n++ } END { printf "  total stale rows queued: %d\n", n + 0 }' "$STATE/.wake-queue"
else
  echo "  (empty - no wake was ever queued)"
fi
echo
echo "--- watcher's own triage log -----------------------------------"
if [ -s "$STATE/.watch-triage.log" ]; then
  grep -c . "$STATE/.watch-triage.log" | sed 's/^/  lines: /'
  tail -4 "$STATE/.watch-triage.log" | sed 's/^/  /'
else
  echo "  (none)"
fi

echo
echo "--- and the pause is absorbed, not silenced: past the bounded window ---"
# Backdate the declared wait so it is older than the re-surface cadence.
back=$(( $(date +%s) - 500 ))
if [ "$(uname)" = Darwin ]; then touch -mt "$(date -r "$back" '+%Y%m%d%H%M.%S')" "$STATE/held.status"
else touch -m -d "@$back" "$STATE/held.status"; fi
if [ "$(uname)" = Darwin ]; then sig=$(stat -f '%z:%Fm' "$STATE/held.status"); else sig=$(stat -c '%s:%Y' "$STATE/held.status"); fi
printf '%s' "$sig" > "$STATE/.seen-held_status"
rm -f "$STATE/.paused-resurfaced-$KEY" "$STATE/.watcher-down"
: > "$WATCH_OUT"
PATH="$FB:$PATH" FM_FAKE_TMUX_WINDOW="$WINDOW" FM_FAKE_TMUX_CAPTURE="$PANE" \
  FM_FAKE_TMUX_CURRENT_COMMAND=grok \
  FM_FAKE_CREW_STATE='state: paused · source: status-log · holding the dev server' \
  FM_STATE_OVERRIDE="$STATE" FM_CREW_STATE_BIN="$FB/fm-crew-state.sh" \
  FM_PAUSE_RESURFACE_SECS=240 FM_STALE_ESCALATE_SECS=240 FM_POLL=1 FM_SIGNAL_GRACE=1 \
  FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$BIN/fm-watch.sh" > "$WATCH_OUT" 2>/dev/null &
pid=$!
i=0; while [ "$i" -lt 60 ]; do kill -0 "$pid" 2>/dev/null || break; sleep 0.1; i=$((i + 1)); done
kill "$pid" 2>/dev/null || true; wait "$pid" 2>/dev/null || true
if [ -s "$WATCH_OUT" ]; then
  echo "  watcher printed: $(tr '\n' ' ' < "$WATCH_OUT")"
else
  echo "  watcher printed nothing (the stale wait never re-surfaced)"
fi
FM_STATE_OVERRIDE="$STATE" "$BIN/fm-wake-drain.sh" 2>/dev/null | grep -i 'awaiting external' | sed 's/^/  queued: /' || true

rm -rf "$WORK"

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 warning
  • ⚠️ bin/fm-wake-drain.sh:173 - The new per-entry close command is printed for reserved-namespace decision keys, where it provably closes nothing while reporting success. Concrete path: fm-pending-reply-lib escalates a marked request by appending blocked [key=pending-reply-abc123]: pending-reply-missed: task=ios ... to the parent status log. The drain (verified by running it) prints ios [key=pending-reply-abc123] blocked: ... plus close it: bin/fm-send.sh ios --resolve-key pending-reply-abc123 &#39;&lt;answer&gt;&#39;. fm-send accepts the key because it is open per status_open_decisions (bin/fm-send.sh:462), delivers, appends resolved [key=pending-reply-abc123]: answered: &lt;text&gt;, and exits 0. Re-folding that log shows _fm_decision_key_transition_allowed (bin/fm-classify-lib.sh:320) rejects the close - the note answered: ... does not speak the reserved namespace's vocabulary - so the key is still open. Every later drain reprints the same command, so the captain re-answers the same escalation each supervision turn while fm-send keeps reporting a clean close. This contradicts the invariant declared in the same hunk ('it can never advertise a key that would close nothing') and in docs/architecture.md:52. Suggested earliest shared boundary rather than a second symptom patch: _fm_decision_key_transition_allowed already owns the rule - have print_open_decisions_section consult it with the note fm-send would write and degrade to a pointer naming the owning library for a row it would reject, and have fm-send apply the same check so a reserved key it cannot honestly close is counted as unmatched instead of closed.
✅ **Test** - passed

✅ No issues found.

  • bin/fm-test-run.sh tests/fm-wake-drain-open-decisions.test.sh tests/fm-wake-drain-unread-status.test.sh tests/fm-send-resolve-key.test.sh - all pass at the target commit
  • bin/fm-test-run.sh tests/fm-watch-triage.test.sh - all pass, including the three new live-declared-pause tests
  • bin/fm-test-run.sh tests/fm-wake-drain-open-decisions-cursor.test.sh tests/fm-classify-decision-key.test.sh tests/fm-captain-hold-lifecycle.test.sh tests/fm-brief.test.sh tests/fm-documentation-audiences.test.sh - neighbouring suites over the same fold, brief text and docs, all pass
  • Regression proof: git checkout 1231b6ae -- bin/fm-watch.sh bin/fm-wake-drain.sh bin/fm-send.sh then re-ran the three suites - each fails on its new test (not ok - open section is missing the decision&#39;s own answerer-closes command, not ok - the diagnostic should say the answer already landed, not ok - a live declared pause woke firstmate on churny stale hash 1), then restored with git checkout HEAD -- bin/...
  • Manual E2E: fm-decision-key-e2e.sh drives the real bin/fm-wake-drain.sh + bin/fm-send.sh over a crafted fleet state (inline key, keyless, note-prose key) against base and target bin, replaying the printed close command verbatim
  • Manual E2E: fm-paused-churn-e2e.sh drives the real bin/fm-watch.sh through 8 dev-server repaints under a declared paused: with a live pane, re-arming after each surfaced wake and draining/acking as firstmate does, against base and target bin
  • Verified git status --porcelain clean after testing; transient /tmp fixtures removed
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Corrective — two named defects: (1) a live crew's declared paused: was re-decided per pane hash and queued contentless stale wakes; (2) OPEN DECISIONS advertised a key that fm-send --resolve-key refused, which cancelled the send and dropped the captain's answer. The unmatched-key path now delivers, closes nothing, and exits nonzero (no enable flag; that is the safer direction of the defect).

VISION: aligns (obligations close by records; a captain answer must not fall through the cracks; idle declared waits must not spend tokens).

Security: no.

Overlap (do not land together): pause_state_class in bin/fm-watch.sh is a semantic conflict with #2748. This PR drops the cheap-path live-agent re-check so a declared pause stays absorbed; #2748 keeps that live-agent re-check and instead promotes secondmate/dead-crew holds. Also overlaps #2750 on handle_paused_stale / tests/fm-watch-triage.test.sh. Same live-pause absorb: #2419. The send/drain half (bin/fm-send.sh, bin/fm-wake-drain.sh) is unique among the three.

This is waiting on the author, not the captain. Fork CI is approved (runs 32508738381 CI now in_progress; 32508738229 Require no-mistakes). no-mistakes is currently red: the body has the signature line but is missing the structured <!-- no-mistakes-pipeline-attestation:v1 ... --> comment (needs no-mistakes >= 1.46.0). Re-run git push no-mistakes so that attestation lands in the PR body.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Two stacked fixes: (1) absorb a live crew's declared pause on the long cadence; (2) render the fold's decision key and deliver a captain answer even when a named key is not open (nonzero, close nothing).

Class: (1) is corrective. (2) is a default send-path change — delivery no longer cancels on a missing key. That is waiting on CI, not on a captain decision.

VISION: pause absorb aligns (token efficiency, not always-on noise). Send-key honesty aligns (never drop a captain answer). Cannot yet land: no-mistakes/CI must finish, and this overlaps #2748 on fm-watch.sh / watch-triage tests. #2748 is the green captain-hold cadence fix and is landing first. After that, rebase if this goes dirty.

Waiting on CI/author rebase after the overlap lands — not waiting on the captain.

A crew that declares `paused:` is idling on a known external wait, so its
stale pane must be absorbed and rechecked only on the long pause cadence.
`pause_state_class` instead re-read agent liveness on every distinct stale
hash and let a live or unreadable agent override the authoritative paused
verdict, returning `none`. Because a held dev server repaints, every repaint
was a fresh first sighting, so the stale path queued another contentless
`stale: <window>` wake every minute or two while `.watch-triage.log`
simultaneously logged the same pane as absorbed.

The authoritative crew-state read now decides first, and liveness only
breaks the remaining tie: a `paused:` or `captain-held` status whose crew
`fm-crew-state.sh` can no longer confirm still surfaces once unless the
agent is confirmed dead, so a captain hold over a crew that never declared
the wait itself cannot silence a live decision gate. Once the bounded
cadence is granted, the cheap `.paused-<key>` path holds it without
re-deciding per stale hash until its recheck marker ages out.

Regression tests cover the three boundaries: a live declared pause absorbs
every churny stale hash with no queued wake, a live idle crew without a
declared pause still surfaces on the unchanged schedule, and a declared
pause past the bounded window still re-surfaces once as a labeled recheck.
The existing live-gate case moves to `captain-held`, where the
surface-once-when-unconfirmable rule actually applies.
The OPEN DECISIONS section printed no key at all whenever the fold landed on
the shared "default" bucket. A decision whose note text still carried a
"[key=...]" token therefore advertised that token as its key, and the section's
single generic hint invited answering with it. fm-send refused that key - and
because the refusal cancelled the send, the captain's answer was dropped rather
than delivered, leaving a real decision open until someone noticed and resent it
by hand.

Every entry now renders the key the fold decided, "default" included, and
carries its own ready-to-run close command underneath, so no key has to be
inferred from note prose; a "[key=...]" inside a note stays quoted verbatim as
the worker's own words. Note and command are charged to the byte cap together,
so an entry is dropped whole rather than listed with no way to close it, and a
task id outside the plain-slug charset gets a pointer instead of a string an
agent would paste into a shell.

fm-send now delivers the answer even when a named key is not open. That is a
deliberate change to the safety property, not a convenience relaxation. The
mis-state the flag exists to prevent is a decision that looks answered while its
record stays open, and cancelling the send never prevented it - it produced the
worse version, where the decision stayed open AND no worker ever received the
answer the captain believed was given. Delivering the text is no more dangerous
than the plain steer that is always allowed, because the flag's power is the
close, and that is exactly what an unmatched key still withholds. "Delivered,
not closed" is also the outcome the post-delivery append-failure path already
treats as the safe direction, since an open decision re-surfaces on every later
presentation. The diagnostic stays loud: it exits nonzero, names the key, and
says not to resend, while any other named key that is open still closes.
Malformed usage - a key outside the slug charset, a duplicate key, --key, an
explicit backend target, an empty message - is still refused before anything is
sent, because those are argument errors with no answer to deliver.

The generated briefs now show the keyed form explicitly, so what a worker is
told to write matches what the fold reads. The parse itself already honors the
inline marker after kunchenguid#2202; the regression tests cover the inline-marker form and
the keyless fallback, and prove the agreement end to end by replaying the drain's
own printed commands through the real fm-send.
@Tiraso12
Tiraso12 force-pushed the fm/firstmate-land-unpublished-fixes branch from a94147d to 19a81f6 Compare August 22, 2026 16:41
@Tiraso12 Tiraso12 changed the title fix(bin): stop dropping captain answers and churning on declared pauses fix(bin): keep a declared pause absorbed and print the key that closes a decision Aug 22, 2026
@greptile-apps

greptile-apps Bot commented Aug 22, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains from the previously reported follow-up issues.

Reviews (4): Last reviewed commit: "no-mistakes(document): clarify partial r..." | Re-trigger Greptile

Comment thread bin/fm-wake-drain.sh
Comment thread bin/fm-send.sh
…ro paths

The header's exit status contract said every nonzero other than 3 means the
send failed and nothing may be assumed delivered, but several paths exit
nonzero only AFTER the submit is confirmed: the pending-reply delivery commit,
a failed closing append, and the unmatched --resolve-key this branch added.
A caller reading the contract literally would treat an answer that already
landed as unsent and resend it, duplicating the instruction.

State the exception as a class rather than an enumeration that would drift:
a nonzero whose stderr says the text was delivered and not to resend reports
an unreconciled ledger, never an undelivered message.
…atched-key

OPEN DECISIONS treated `-foo` as a pasteable task id, so the printed command
was `fm-send.sh -foo ...` and fm-send parsed it as a flag. A leading dash is
now the same unpasteable case as an illegal charset, and the drain prints a
pointer instead.

The unmatched --resolve-key path still delivers then exits 1. The header now
names that post-delivery nonzero as a ledger mismatch, not an undelivered
send, so a caller does not resend an answer that already landed.
@Tiraso12 Tiraso12 changed the title fix(bin): keep a declared pause absorbed and print the key that closes a decision fix(bin): list the key that actually closes a decision and keep declared pauses absorbed Aug 22, 2026
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

VISION: aligns. OPEN DECISIONS now prints the fold's actual close key (including default) next to a paste-safe command, so a captain answer can close the record it named; a crew's own paused: stays absorbed on the long cadence instead of queuing contentless stale wakes. Obligations stay closed by records, not recollection.

Class: corrective. Two named defects: (1) listed decision key vs the key that actually closes; (2) declared pause re-decided per pane hash. Send path still delivers an unmatched --resolve-key (closes nothing for that key, exits nonzero, do not resend). That is the safe direction for the original drop-the-answer bug, not a new default capability. Pause absorb kept.

Security: none. No new network, no credential handling, no workflow-file change. Leading-dash ids are no longer pasted as runnable close commands.

Overlap: bin/fm-watch.sh / send-path with #2795 and #2631. Related to #2790 (answer/decision class) but different work: this lists the key that closes. Touches classify/drain/watch/send; not the captain-hold pair.

CI: HEAD e3c595a5d602b1e6575b109cfa2fe1fdbd253e67. CI run 32587660954 in_progress (approved this pass, not green). Require no-mistakes / body-compliance 32587660911 and 32587687976 both FAILURE. No structured no-mistakes-pipeline-attestation:v1 on this HEAD (a git-push signature is not a substitute). Greptile SUCCESS — not a gate.

Workflows approved: yes (CI 32587660954, NM 32587660911, NM 32587687976). Not land-eligible. Waiting on CI, not the captain.

@Tiraso12 Tiraso12 changed the title fix(bin): list the key that actually closes a decision and keep declared pauses absorbed fix(bin): keep a declared pause absorbed and make the listed decision key the one that closes it Aug 22, 2026
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.

2 participants