Skip to content

fix(bin): report a worker parked on the exit picker as blocked-on-prompt - #6682

Open
tiago-peixoto wants to merge 8 commits into
kunchenguid:mainfrom
tiago-peixoto:fm/upstream-4754-remainder
Open

tiago-peixoto wants to merge 8 commits into
kunchenguid:mainfrom
tiago-peixoto:fm/upstream-4754-remainder

Conversation

@tiago-peixoto

@tiago-peixoto tiago-peixoto commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Refs #4754.

Contribute the supervision part of #4754, which the maintainer reopened and labelled ready-for-pr (contract class: restore) after #6666 fixed the control side. What remains on main, in the maintainer's words: a worker parked on a dialog still reads as healthy quiet; the watcher and away-mode liveness do not report blocked-on-prompt. The issue's expected behaviour: a classified modal or exit dialog should be visible to supervision as blocked-on-prompt, not as healthy idle.

This contribution covers the dialog that pull request 6666 already recognises, the Claude background-task exit picker. Recognising other dialogs (MCP approval, model-downgrade confirmation, other exit confirmations) is not part of it.

What Changed

  • bin/fm-watch.sh now runs the existing fm_composer_blocking_dialog matcher on the stale-path pane capture it already takes for a worker. When the pane shows the Claude background-task exit picker, the plain stale: wake reason and the final wedge escalation reason end with blocked-on-prompt: <dialog name> inside the one parenthetical. Triage and schedule are unchanged, and the deferral, dead-record and declared-wait recheck reasons keep their wording.
  • bin/fm-supervise-daemon.sh (the away-mode daemon) reads that name off the stale wake. It appends the name to a status-log escalation, and it saves the name in state/.subsuper-dialog-<key> for a self-handled stale so the later stale persisted ... (possible wedge, blocked-on-prompt: <name>) line still carries it. The file is removed with the stale marker, with pause tracking, and by the watcher on the first poll where the pane no longer shows the picker.
  • tests/fm-watch-triage.test.sh and tests/fm-daemon.test.sh gain cases for this reporting, using fakes and not a live pane. docs/architecture.md, docs/verification/runtime-backends.md, the fm-watch.sh header and the afk skill describe the new reason suffix.

Risk Assessment

✅ Low: The two fix rounds net out to comment and doc wording only (git diff d25bed3..HEAD shows no code or test change), the wedge deferral functions are untouched against the base, and the remaining code adds a name suffix on existing stale wakes whose daemon parsing, key derivation and file cleanup I traced without finding a wrong result.

Testing

I built a disposable lab home with bin/fm-lab-home.sh, started real Claude in a private tmux pane, started a background sleep, and typed /exit to open the real exit picker. I then ran the real watcher inside that tmux server and recorded each wake line, ran the real away daemon with a 20 second wedge threshold and recorded its escalation buffer and log, and drove three adversarial cases (picker closed with Esc, picker words quoted by Claude above a normal composer, pane registered as a second mate). Six scenarios passed live. The seventh scenario, the away daemon's dialog handling for status-driven paths, is untested live: I did not drive it in the lab, and only tests/fm-daemon.test.sh covers it. The wedge escalation scenario used a two-line stub for the crew-state verdict, because the real verdict for a lab pane is unknown and the wedge timer needs a working verdict; the watcher, tmux, pane and picker were real. I ran tests/fm-daemon.test.sh once, and it passed, including the four dialog tests. I did not run tests/fm-watch-triage.test.sh, which takes about 20 minutes; remote CI owns it. No screenshot applies: the surface is wake text, captured as text files.

  • Live validation: ✅ go - 6 of 7 scenarios driven live against the product
Scenario Result Live Evidence
A worker pane parks on the real Claude exit picker; the watcher's stale wake names it as blocked-on-prompt ✅ pass live 02-watcher-plain-stale-names-picker.txt: watcher printed and queued stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker); pane recorded in 01-real-claude-exit-picker-pane.txt
A worker judged working stays on the picker past the wedge threshold; the wedge escalation keeps its count and ends with the picker name, after demand-deep-inspection at escalation 3 ✅ pass live 03-watcher-wedge-escalation-names-picker.txt: (idle 503s, possible wedge, escalation 1, blocked-on-prompt: ...) and (... escalation 3, demand-deep-inspection: ..., blocked-on-prompt: ...). Crew-st…
In away mode a worker stays on the picker past the threshold; the daemon's persistence escalation names the picker ✅ pass live 04-away-daemon-persistence-names-picker.txt: stale persisted 21s (possible wedge, blocked-on-prompt: Claude background-task exit picker): lab:fm-picker in state/.subsuper-escalations
Adversarial: in away mode the picker is closed with Esc before the threshold; the saved name is dropped and the later persistence line does not claim a dialog ✅ pass live 05-away-closed-picker-drops-name.txt: state/.subsuper-dialog-picker gone within 1s of Esc; next lines are plain stale: lab:fm-picker and stale persisted 21s (possible wedge): lab:fm-picker
Adversarial: Claude quotes the picker's words above a normal composer; the watcher sends an ordinary stale with no blocked-on-prompt ✅ pass live 06-quoted-picker-text-stays-plain-stale.txt: watcher printed exactly stale: lab:fm-picker
A non-paused second mate sits on the real picker; the watcher keeps polling and sends no wake (recorded decision) ✅ pass live 07-secondmate-on-picker-stays-quiet.txt: watcher alive after 25s, empty stdout, beacon advanced, zero queue rows
Away daemon dialog handling for status-driven paths: an unseen status escalation gains the name, and a seen terminal status clears like a plain stale ⏸️ untested no The prior payload did not establish a live result for this scenario. It recorded only that bash tests/fm-daemon.test.sh passed, including a dialog name joins the escalation of an unseen status and…
Evidence: Real Claude 2.1.292 exit picker in the lab pane

Source: Real Claude 2.1.292 exit picker in the lab pane

 ▐▛███▛█   Claude Code v2.1.292
▝▜██████▀  Opus 5.5 · Claude Max
 ▝▝   ▝▝   /tmp/fm-lab.43yCHk/proj


❯ Run the shell command "sleep 1800" with run_in_background true, then reply with only the word started.

● Bash(sleep 1800)
  ⎿  Running in the background (↓ to manage)
  ⎿  Allowed by auto mode classifier

● started

✻ Cogitated for 2s · done 7:20 PM · 1 shell still running

❯ /exit

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  Background work is running
  The following will stop when you exit:

  shell · sleep 1800

  ❯ 1. Exit and stop tasks
    2. Move to background and exit
    3. Stay

  Enter to confirm · Esc to cancel
Evidence: Watcher plain stale wake names the picker

Source: Watcher plain stale wake names the picker

stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker)

# watcher arm 2 stdout (real fm-watch.sh, real tmux pane, real Claude 2.1.292 exit picker)
stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker)

# state/.wake-queue
1791393698	3	stale	lab:fm-picker	stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker)
Evidence: Wedge escalations 1 and 3 name the picker

Source: Wedge escalations 1 and 3 name the picker

stale: lab:fm-picker (idle 503s, possible wedge, escalation 1, blocked-on-prompt: Claude background-task exit picker) stale: lab:fm-picker (idle 501s, possible wedge, escalation 3, demand-deep-inspection: same pane has wedge-escalated 3 times in a row - do not re-absorb on the run-step/pane state alone, blocked-on-prompt: Claude background-task exit picker)

# Real watcher, real pane on the real picker; crew verdict stubbed to 'state: working · source: run-step' so the wedge timer path runs
# arm 3: first wedge escalation
stale: lab:fm-picker (idle 503s, possible wedge, escalation 1, blocked-on-prompt: Claude background-task exit picker)
# arm 4: third escalation (count preset to 2)
stale: lab:fm-picker (idle 501s, possible wedge, escalation 3, demand-deep-inspection: same pane has wedge-escalated 3 times in a row - do not re-absorb on the run-step/pane state alone, blocked-on-prompt: Claude background-task exit picker)
Evidence: Away daemon persistence line names the picker

Source: Away daemon persistence line names the picker

stale persisted 21s (possible wedge, blocked-on-prompt: Claude background-task exit picker): lab:fm-picker

# Real fm-supervise-daemon.sh in away mode (state/.afk), FM_STALE_ESCALATE_SECS=20, real pane on the real picker
# state/.subsuper-escalations
stale persisted 21s (possible wedge, blocked-on-prompt: Claude background-task exit picker): lab:fm-picker
# state/.supervise-daemon.log
[2026-10-07T19:23:00+0200] daemon starting (pid 2917092); target=lab:super; target_source=FM_SUPERVISOR_TARGET; backend=tmux; backend_source=FM_SUPERVISOR_BACKEND; afk=on; inject_skip='heartbeat'; stale_escalate=20s; batch=3600s
[2026-10-07T19:23:02+0200] wake: stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker)
[2026-10-07T19:23:04+0200] self-handle: stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker) -> transient stale (lab:fm-picker): working: parked between turns
Evidence: Away mode: closed picker drops the name

Source: Away mode: closed picker drops the name

stale persisted 21s (possible wedge): lab:fm-picker

# Same daemon run. The picker was re-reported at 19:23:38, then Esc closed it about 1s later.
# The saved name file state/.subsuper-dialog-picker was gone within 1s; the next persistence line has no name.
# state/.subsuper-escalations
stale persisted 21s (possible wedge, blocked-on-prompt: Claude background-task exit picker): lab:fm-picker
stale persisted 21s (possible wedge): lab:fm-picker
# state/.supervise-daemon.log
[2026-10-07T19:23:00+0200] daemon starting (pid 2917092); target=lab:super; target_source=FM_SUPERVISOR_TARGET; backend=tmux; backend_source=FM_SUPERVISOR_BACKEND; afk=on; inject_skip='heartbeat'; stale_escalate=20s; batch=3600s
[2026-10-07T19:23:02+0200] wake: stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker)
[2026-10-07T19:23:04+0200] self-handle: stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker) -> transient stale (lab:fm-picker): working: parked between turns
[2026-10-07T19:23:38+0200] wake: stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker)
[2026-10-07T19:23:41+0200] self-handle: stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker) -> transient stale (lab:fm-picker): working: parked between turns
[2026-10-07T19:23:49+0200] wake: stale: lab:fm-picker
[2026-10-07T19:23:51+0200] self-handle: stale: lab:fm-picker -> transient stale (lab:fm-picker): working: parked between turns
[2026-10-07T19:23:58+0200] wake: stale: lab:fm-picker
[2026-10-07T19:24:01+0200] self-handle: stale: lab:fm-picker -> transient stale (lab:fm-picker): working: parked between turns
Evidence: Quoted picker text stays a plain stale

Source: Quoted picker text stays a plain stale

stale: lab:fm-picker

# Pane: Claude quoted the picker's words above a normal composer (no live picker)
❯ Reply with exactly these three lines and nothing else, each on its own line, no code block: "Background work is
  running" then "❯ 1. Exit and stop tasks" then "Enter to confirm · Esc to cancel"
● Background work is running
  ❯ 1. Exit and stop tasks
  Enter to confirm · Esc to cancel
✻ Churned for 1s · done 7:24 PM · 1 shell still running
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯ 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ⏵⏵ auto mode on · 1 shell · ← for agents · ↓ to manage

# watcher stdout
stale: lab:fm-picker
Evidence: Second mate on the picker stays quiet

Source: Second mate on the picker stays quiet

# meta kind=secondmate, status 'working:', real pane on the real picker; watcher polled every 2s for 25s
watcher still running: yes
watcher stdout bytes: 0
beacon mtime before=1791393903 after=1791393941
wake queue rows: 0

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

⚠️ **Rebase** - 1 warning
  • ⚠️ bin/fm-watch.sh - merge conflict rebasing onto origin/main
🔧 **Review** - 2 issues found → auto-fixed (2) ✅
  • ⚠️ bin/fm-watch.sh:1558 - wedge_timer_check adds the picker name only on the final escalation line. The three deferrals that return before it send their own stale reasons without the name: wedge_defer_wait (reason built at bin/fm-watch.sh:1426), wedge_defer_writing (bin/fm-watch.sh:1204) and wedge_dead_record (bin/fm-watch.sh:1496). Concrete trace outside away mode: a worker's crew state reads working (run-step), the pane shows the exit picker, and the background shell that caused the picker writes files in the task worktree. The first stale is absorbed into the wedge timer with no wake. At FM_STALE_ESCALATE_SECS, crew_worktree_written_since is true, so the watcher sends stale: &lt;win&gt; (idle Ns, writing its worktree for Ns, rechecked on a long cadence not a wedge; confirm the writes are real progress). That line never says blocked-on-prompt, and it repeats on the long cadence while the worker stays parked. The intent says a classified dialog "should be visible to supervision as blocked-on-prompt, not as healthy idle". docs/architecture.md:22 also states that a stale wake for such a pane "also ends its reason with blocked-on-prompt", which is not true for these reasons. Passing the name to those reasons widens which wake lines change, and the last commit deliberately narrowed that set, so the author must choose: (a) pass $dialog through stale_reason_naming_dialog on the wedge_defer_wait and wedge_defer_writing reasons, or (b) keep them as they are and narrow the sentence in docs/architecture.md:22 to the plain stale and wedge-escalation reasons.
  • ℹ️ bin/fm-watch.sh:42 - The new header sentence about the picker name sits between "...another routine supervision resume." and the existing fragment "Unless afk is active." On main that fragment qualified the wedge escalation count and demand-deep-inspection sentence. It now reads as qualifying the picker sentence, which is wrong: the away branch at bin/fm-watch.sh:3144 does add the name. Move the picker sentence after "Unless afk is active." so the fragment stays attached to the sentence it qualified.

🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:

  • ⚠️ bin/fm-watch.sh:1558 - wedge_timer_check adds the picker name only on the final escalation line. The three deferrals that return before it send their own stale reasons without the name: wedge_defer_wait (reason built at bin/fm-watch.sh:1426), wedge_defer_writing (bin/fm-watch.sh:1204) and wedge_dead_record (bin/fm-watch.sh:1496). Concrete trace outside away mode: a worker's crew state reads working (run-step), the pane shows the exit picker, and the background shell that caused the picker writes files in the task worktree. The first stale is absorbed into the wedge timer with no wake. At FM_STALE_ESCALATE_SECS, crew_worktree_written_since is true, so the watcher sends stale: &lt;win&gt; (idle Ns, writing its worktree for Ns, rechecked on a long cadence not a wedge; confirm the writes are real progress). That line never says blocked-on-prompt, and it repeats on the long cadence while the worker stays parked. The intent says a classified dialog "should be visible to supervision as blocked-on-prompt, not as healthy idle". docs/architecture.md:22 also states that a stale wake for such a pane "also ends its reason with blocked-on-prompt", which is not true for these reasons. Passing the name to those reasons widens which wake lines change, and the last commit deliberately narrowed that set, so the author must choose: (a) pass $dialog through stale_reason_naming_dialog on the wedge_defer_wait and wedge_defer_writing reasons, or (b) keep them as they are and narrow the sentence in docs/architecture.md:22 to the plain stale and wedge-escalation reasons.
  • ℹ️ bin/fm-watch.sh:42 - The new header sentence about the picker name sits between "...another routine supervision resume." and the existing fragment "Unless afk is active." On main that fragment qualified the wedge escalation count and demand-deep-inspection sentence. It now reads as qualifying the picker sentence, which is wrong: the away branch at bin/fm-watch.sh:3144 does add the name. Move the picker sentence after "Unless afk is active." so the fragment stays attached to the sentence it qualified.
  • ⚠️ bin/fm-watch.sh:1647 - Sibling left behind by the round 1 fix. Round 1 made the three wedge_timer_check deferral reasons carry the picker name, but handle_paused_stale still builds its recheck reason without it and takes no dialog argument. Trace outside away mode: a worker's last status line is paused: waiting on the upstream release cut, the pane is not busy and shows the Claude background-task exit picker. The poll loop computes dialog at bin/fm-watch.sh:3112, then the declared-wait branch calls handle_paused_stale (callers at bin/fm-watch.sh:3236, 3246, 3251, and bin/fm-watch.sh:1708 from busy_turn_bound_check). resurface_absorbed sends stale: &lt;win&gt; (paused Ns, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) with no blocked-on-prompt:. That line repeats once per FM_PAUSE_RESURFACE_SECS while the worker stays parked, so the supervisor reads a healthy declared wait. The same worker gets the name when its crew state reads working and it reaches wedge_defer_wait instead (that is what the new test test_exit_picker_wait_deferral_names_the_dialog sets up), so the result depends on which absorber takes the pane. All four reasons in handle_paused_stale are affected: captain-held (bin/fm-watch.sh:1622), declared time beyond cadence (1630), declared time passed (1634), declared pause (1640). The away-mode counterpart has the same gap: the daemon's pause action calls stale_marker_remove, which deletes the saved name (bin/fm-supervise-daemon.sh:1688), and its pause re-surface line never names the dialog. docs/architecture.md:22 says a stale wake for such a pane "also ends its reason with blocked-on-prompt", and the comment on stale_reason_naming_dialog (bin/fm-watch.sh:1749) already allows for it by noting the pause matcher keys on "declared pause,". Naming the dialog here widens which wake lines change, and the round 1 instruction limited the fix to three named reasons, so the author must choose: (a) pass $dialog into handle_paused_stale and run stale: $win ($reason) through stale_reason_naming_dialog, with one test, or (b) keep the declared-wait rechecks as they are and state that exception in docs/architecture.md:22 and the bin/fm-watch.sh header.

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

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 6 of 7 scenarios driven live against the product
Scenario Result Live Evidence
A worker pane parks on the real Claude exit picker; the watcher's stale wake names it as blocked-on-prompt ✅ pass live 02-watcher-plain-stale-names-picker.txt: watcher printed and queued stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker); pane recorded in 01-real-claude-exit-picker-pane.txt
A worker judged working stays on the picker past the wedge threshold; the wedge escalation keeps its count and ends with the picker name, after demand-deep-inspection at escalation 3 ✅ pass live 03-watcher-wedge-escalation-names-picker.txt: (idle 503s, possible wedge, escalation 1, blocked-on-prompt: ...) and (... escalation 3, demand-deep-inspection: ..., blocked-on-prompt: ...). Crew-st…
In away mode a worker stays on the picker past the threshold; the daemon's persistence escalation names the picker ✅ pass live 04-away-daemon-persistence-names-picker.txt: stale persisted 21s (possible wedge, blocked-on-prompt: Claude background-task exit picker): lab:fm-picker in state/.subsuper-escalations
Adversarial: in away mode the picker is closed with Esc before the threshold; the saved name is dropped and the later persistence line does not claim a dialog ✅ pass live 05-away-closed-picker-drops-name.txt: state/.subsuper-dialog-picker gone within 1s of Esc; next lines are plain stale: lab:fm-picker and stale persisted 21s (possible wedge): lab:fm-picker
Adversarial: Claude quotes the picker's words above a normal composer; the watcher sends an ordinary stale with no blocked-on-prompt ✅ pass live 06-quoted-picker-text-stays-plain-stale.txt: watcher printed exactly stale: lab:fm-picker
A non-paused second mate sits on the real picker; the watcher keeps polling and sends no wake (recorded decision) ✅ pass live 07-secondmate-on-picker-stays-quiet.txt: watcher alive after 25s, empty stdout, beacon advanced, zero queue rows
Away daemon dialog handling for status-driven paths: an unseen status escalation gains the name, and a seen terminal status clears like a plain stale ⏸️ untested no The prior payload did not establish a live result for this scenario. It recorded only that bash tests/fm-daemon.test.sh passed, including a dialog name joins the escalation of an unseen status and…
  • bin/fm-lab-home.sh create $LAB then tmux -L fm-lab new-session ... claude on TMUX_TMPDIR=$LAB/tmux; background sleep 1800, then /exit to open the real picker
  • FM_HOME=$LAB FM_POLL=2 bin/fm-watch.sh run in a lab tmux window against meta window=lab:fm-picker kind=ship backend=tmux - plain stale wake
  • Same watcher with FM_CREW_STATE_BIN stubbed to a working run-step verdict and .stale-since backdated 500s - wedge escalation 1 and escalation 3
  • touch $LAB/state/.afk then FM_STALE_ESCALATE_SECS=20 FM_SUPERVISOR_TARGET=lab:super bin/fm-supervise-daemon.sh - away persistence line
  • Same daemon run: re-report the picker, press Esc in the pane, read state/.subsuper-dialog-picker and state/.subsuper-escalations
  • Asked lab Claude to print the picker's three lines above its composer, then ran the watcher
  • Reopened the picker, set kind=secondmate, ran the watcher for 25s
  • bash tests/fm-daemon.test.sh (passed; unit test only, not a live drive)
  • Teardown: answered the picker, tmux -L fm-lab kill-server, rm -rf $LAB, git status --short empty
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

A pane parked on that picker still read as healthy quiet after Enter was refused. The existing stale and wedge reports now carry the dialog name, including through away-mode escalation.

Refs kunchenguid#4754
@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Adds dialog-name tracking to stale-wake escalation logic.

The PR appears safe to merge within its stated worker-only scope.

Reviews (3) · Last reviewed commit: "fix: keep the exit-picker name off paths..."

Comment thread bin/fm-watch.sh Outdated
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: I read tip 6c6b8993 against main 5838f105. It reuses #6666's fm_composer_blocking_dialog on the stale path's 40-line capture and adds blocked-on-prompt: Claude background-task exit picker to stale wake reasons. It also makes three changes to what runs by default:

  • A non-paused second mate, which main exempts from the stale path entirely, now gets a pane capture on every poll and a stale wake while its pane shows the picker.
  • The away daemon keeps a finished worker's stale marker ageing while the picker is named, so a done: worker parked on the picker now escalates as a possible wedge.
  • A new state/.subsuper-dialog-<key> record carries the name into the daemon's escalations.

Attestation MATCH for 6c6b8993. no-mistakes and all CI lanes passed. The diff has no security concern. Greptile's one P2 (a narrow false-wedge window right after a finished worker closes the picker) was answered in-thread and Greptile accepted it as a documented limitation. That accepted P2 is the only reason the Greptile check shows red.

Contract-class: new-default. #4754 was stamped restore for the remaining scope, but this tip adds wake sources the unconfigured product does not run today (second-mate picker wakes, and wedge escalation for a finished worker). Those are new default-on surfaces, not a fix to an existing specified path. Annotating existing worker stale wakes on its own would be closer to restore.

VISION (per rule):

  1. One captain, one interface: aligns. A worker parked on a dialog stops looking like healthy quiet, and the name is in the wake itself.
  2. Authority is explicit and never inferred: aligns. It only reports; it never answers the dialog.
  3. Scripts own the mechanics, agents own the judgment: aligns. Recognition is the shared script matcher; the decision stays with the supervisor.
  4. A restart is a non-event: aligns. The dialog name is a state-file record.
  5. Delegation with a spine: aligns. Supervisors see why a worker is stuck.
  6. The fleet outlives any vendor: aligns, with a caveat. It reads Claude's rendered picker, which is the quarantined, version-pinned matcher fix: refuse a confirming Enter on the Claude background-task exit picker #6666 already added, not a new contract.
  7. Scope: aligns. It costs one extra pane capture per poll for each non-paused second mate.

This is with the maintainer for a decision, not waiting on you. Because it changes default supervision behavior, it won't auto-merge. Nothing more is needed from you unless the maintainer asks for changes. Thank you for building on #6666 instead of competing with it.

A second mate stays exempt from stale capture, and a finished worker in away mode no longer ages into a wedge. The name remains on the stale and wedge wakes a worker already raises.
@tiago-peixoto tiago-peixoto changed the title fix: report the Claude exit picker as blocked-on-prompt in stale supervision fix(bin): name the Claude exit picker on stale and wedge wakes Oct 6, 2026
@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Comments Outside Diff

These findings could not be posted inline.

  • P1 Second mate picker goes unseen bin/fm-watch.sh:3053 ▶

    When a non-paused second mate sits on the recognised exit picker, this continue skips its pane capture before the dialog matcher runs. The watcher sends no blocked-on-prompt wake, leaving the mate looking healthy while it is blocked. This conflicts with the PR’s stated second-mate coverage and its intent to make a classified exit dialog visible to supervision.

  • P1 Finished worker's picker stays hidden bin/fm-supervise-daemon.sh:1722 ▶

    In away mode, a worker with an already-reported done: status can open the Claude exit picker and send a named stale wake. The daemon handles that wake without escalating it, then clears both the dialog name and the marker that housekeeping would use to report it later. The worker remains blocked on the prompt without the away supervisor being told, contrary to the PR’s stated intent.

@tiago-peixoto

Copy link
Copy Markdown
Contributor Author

The two parts that ran by default, and that main does not run, are removed. The change now only names the dialog in the stale and wedge wakes that main already raises.

@tiago-peixoto

Copy link
Copy Markdown
Contributor Author

Escalating a finished worker parked on the picker was removed because it added behaviour that does not run on main today. The description lists that case as not covered. It is left for a follow-up.

@tiago-peixoto tiago-peixoto changed the title fix(bin): name the Claude exit picker on stale and wedge wakes fix(bin): report a worker parked on the exit picker as blocked-on-prompt Oct 7, 2026
@tiago-peixoto tiago-peixoto reopened this Oct 7, 2026
@kunchenguid

kunchenguid commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks, @tiago-peixoto. Narrowing this was the right call. I re-reviewed head 6a5284d3 against main ac0811c4, because the head changed materially since my 2026-10-06 review of 6c6b8993.

What changed since then: the two parts that ran by default and that main doesn't run are gone.

  • A non-paused second mate on the picker is no longer captured or woken. The second-mate continue in bin/fm-watch.sh is untouched.
  • A finished (done:) worker parked on the picker no longer escalates as a possible wedge. A terminal status that was already reported still clears the persistence marker, the same as on main.

What the tip does now:

  • bin/fm-watch.sh runs fix: refuse a confirming Enter on the Claude background-task exit picker #6666's fm_composer_blocking_dialog on the 40-line capture each poll already takes. When the Claude background-task exit picker is showing, the plain stale: reason and the final wedge reason (after any demand-deep-inspection clause) end with blocked-on-prompt: <name> inside the one parenthetical. I checked that no branch wakes where main doesn't. Every change swaps the reason string on a wake main already sends, and the throttled early return in surface_nonterminal_stale still returns before wake.
  • bin/fm-supervise-daemon.sh parses the name, adds it to a status escalation it already sends, and keeps it in a new state/.subsuper-dialog-<key> file so its later stale persisted ... (possible wedge, blocked-on-prompt: ...) line still names it. That file is removed with the stale marker, with pause tracking, when the window is gone, and by the watcher on the first poll that no longer shows the picker.
  • Greptile's two P1s on 46c95ff8 were about the second-mate and finished-worker coverage, and that coverage is now explicitly out of scope. The earlier P2 is gone along with the finished-worker path.

CI: all green on 6a5284d3, including Require no-mistakes. The cancelled Behavior portable serial 2 belongs to the superseded run, and serial 2 passed on the current run. The no-mistakes attestation matches this head. GitHub reports MERGEABLE/CLEAN. There are no workflow, network or secret changes.

Contract-class: new-default (narrower than before, but the same class). Every unconfigured home's watcher now reads Claude's rendered picker on each worker poll and changes the text of stale and wedge wakes. The away daemon also keeps a new state record. Main doesn't promise naming a dialog in supervision wakes. The stale wake itself works as specified, so this adds information to a default path rather than fixing a broken one. It's a small presentation addition and I think a good one, but it's default-on, so it won't auto-merge.

What Kun will weigh: whether to take a default-on blocked-on-prompt: suffix on existing stale and wedge wakes, backed by the version-pinned picker matcher, with second mates and finished workers left for a follow-up. #5947 (deferring wedge escalation while a no-mistakes run is running) touches the same wedge-escalation path in bin/fm-watch.sh, so whichever lands second will need a rebase.

VISION.md, rule by rule

  • One captain, one interface: aligns. A worker parked on a dialog no longer reads as plain quiet, and the reason is in the wake itself, with no new wake.
  • Authority is explicit and never inferred: aligns. It only reports and never answers the dialog. Because it's default-on, the decision goes to Kun.
  • Scripts own the mechanics, agents own the judgment: aligns. Recognition is the shared script matcher, and the decision stays with the supervisor.
  • A restart is a non-event: aligns. The name lives in a state file that is cleared deterministically.
  • Delegation with a spine: aligns. Supervisors see why a worker is stuck.
  • The fleet outlives any vendor: aligns, with the same caveat as before. It reads Claude's rendered picker through fix: refuse a confirming Enter on the Claude background-task exit picker #6666's quarantined, version-pinned matcher, not a new shared contract.
  • Scope: aligns. No new capture, because it reuses the poll's existing capture.

Next step: this is otherwise ready, and it's with Kun for a merge-or-leave decision, not waiting on you. Nothing more is needed from you unless he asks for changes.

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