Skip to content

fix: handle opencode idle supervision#50

Closed
tommy230 wants to merge 4 commits into
kunchenguid:mainfrom
tommy230:fm-daemon-opencode-fix-r7
Closed

fix: handle opencode idle supervision#50
tommy230 wants to merge 4 commits into
kunchenguid:mainfrom
tommy230:fm-daemon-opencode-fix-r7

Conversation

@tommy230

Copy link
Copy Markdown

Intent

Captain, the developer was trying to finish validation for PR #49 after the earlier run failed only at the push step. They wanted fork routing enabled and verified with a green doctor check, then wanted the validation rerun so the already-passing review, tests, docs, and lint could proceed to a successful push through the fork instead of the upstream repository.

What Changed

  • Teach the supervision daemon's composer guard to treat opencode's bordered Ask anything... prompt as idle while still deferring on real typed input.
  • Exclude the supervisor pane from stale-wedge tracking so only fm-* crewmate windows can create or escalate stale markers.
  • Document the opencode supervision behavior and add shell regressions covering idle prompt detection, typed-input protection, and stale-marker handling.

Risk Assessment

✅ Low: Captain, the change is narrowly scoped to daemon stale-pane filtering and opencode idle-composer detection, with focused regression coverage and no material merge blockers found.

Testing

Captain, targeted opencode daemon regression and full shell behavior suite passed; evidence logs show idle opencode composer accepted as empty, typed composer guarded as pending, supervisor stale ignored, crewmate stale behavior preserved, and afk injection e2e scenarios still working. Worktree cleanup check found no transient test residue.

Evidence: Targeted opencode daemon evidence
ok - opencode: idle composer (border + 'Ask anything...' placeholder) is NOT pending
ok - opencode: idle composer without a dynamic suggestion is NOT pending
ok - opencode: border-only composer chrome is NOT pending
ok - opencode: typed text on the composer line IS pending (protection holds)
ok - opencode: typed text resembling the placeholder still IS pending
ok - opencode: typed text beginning with placeholder prefix IS pending
ok - legacy bare prompts ($, >) are still NOT pending (no regression)
ok - is_crewmate_window: fm-* only; supervisor pane / pane-id excluded
ok - classify_stale ignores the supervisor pane (self-handle, no marker)
ok - stale_marker_record: no marker for supervisor pane; crewmate still tracked
ok - classify_stale: crewmate (fm-*) stale still classified (terminal+transient)
ok - housekeeping drops an orphaned supervisor marker without escalating
all opencode daemon tests passed
Evidence: Full shell behavior suite evidence
== tests/fm-afk-inject-e2e.test.sh ==
ok - Scenario A: partial input defers injection; digest arrives clean after idle
ok - Scenario B: swallowed Enter produces exactly one clean digest
all e2e injection tests passed
== tests/fm-bootstrap.test.sh ==
ok - bootstrap accepts treehouse get --lease support
ok - bootstrap reports treehouse without get --lease support
== tests/fm-daemon-opencode.test.sh ==
ok - opencode: idle composer (border + 'Ask anything...' placeholder) is NOT pending
ok - opencode: idle composer without a dynamic suggestion is NOT pending
ok - opencode: border-only composer chrome is NOT pending
ok - opencode: typed text on the composer line IS pending (protection holds)
ok - opencode: typed text resembling the placeholder still IS pending
ok - opencode: typed text beginning with placeholder prefix IS pending
ok - legacy bare prompts ($, >) are still NOT pending (no regression)
ok - is_crewmate_window: fm-* only; supervisor pane / pane-id excluded
ok - classify_stale ignores the supervisor pane (self-handle, no marker)
ok - stale_marker_record: no marker for supervisor pane; crewmate still tracked
ok - classify_stale: crewmate (fm-*) stale still classified (terminal+transient)
ok - housekeeping drops an orphaned supervisor marker without escalating
all opencode daemon tests passed
== tests/fm-secondmate.test.sh ==
ok - FM_HOME parameterizes data and state paths
ok - fm-lock status is scoped per home
ok - secondmates registry records scopes and allows overlapping project clone lists
ok - home seeding records routing scope from filled charter briefs
ok - home seed validation rejects duplicate home routes
ok - home seed validation rejects duplicate id routes
ok - home seed validation rejects nested home routes
ok - home seeding durably leases treehouse-acquired dash homes under the secondmate id
ok - home seeding returns rejected acquired homes through treehouse
ok - home seed rollback warns when treehouse-acquired return fails
ok - home seeding leaves unsafe acquired active homes untouched
ok - home seeding rolls back failed clone attempts without residue
ok - home seeding refuses direct seed without filled charter text
ok - home seeding refuses unfilled placeholder charters
ok - home seeding refuses empty normalized charter fields
ok - home seeding refuses local-only projects
ok - home seeding refuses registry delimiter home paths
ok - home seeding refuses active home and repo root
ok - home seeding refuses homes marked for another id
ok - home seeding refuses homes registered to another id
ok - home seeding refuses same-id reassignment to a different home
ok - home seeding refuses registered home overlaps
ok - remote-backed subhome seeding requires a source origin
ok - remote-backed subhome seeding validates existing destination origins
ok - home seeding resolves relative source origins against the source project
ok - home seeding skips initialized existing no-mistakes clones
ok - home seeding refuses uninitialized existing no-mistakes clones
ok - home seeding refuses project destinations outside the subhome
ok - home seeding refuses operational directories outside the subhome
ok - home seeding refuses symlinked leaf files
ok - kind=secondmate spawn launches in the home and records routing meta
ok - secondmate spawn validates homes before launch
ok - secondmate spawn refuses operational directories outside the subhome
ok - fm-send resolves bare firstmate windows through this home
ok - restart recovery can respawn a secondmate from durable registry and charter
ok - secondmate teardown retires empty homes and releases routing
ok - secondmate teardown refuses to hide failed leased-home return
ok - secondmate teardown raw-removes plain-clone homes
ok - secondmate force teardown discards child work
ok - force teardown allows operational directory symlinks inside the subhome
ok - force teardown refuses operational directory symlinks outside the subhome
ok - secondmate teardown requires seeded home marker
ok - secondmate teardown refuses homes containing registered nested homes
ok - secondmate teardown refuses nested homes from the child registry
ok - force teardown validates subhome before child cleanup
ok - force teardown refuses child worktrees inside the active home
ok - force teardown refuses child worktrees inside the firstmate repo
ok - force teardown refuses unregistered child worktree paths
ok - secondmate teardown refuses ancestor homes
ok - secondmate teardown refuses descendant homes
ok - idle kind=secondmate pane is healthy and not stale
ok - secondmate charter brief is idle by default and does not self-initiate work
ok - fm-backlog-handoff moves in-scope items, is idempotent, and aborts safely
ok - fm-backlog-handoff creates absent sections and refuses unsafe homes
== tests/fm-spawn-batch.test.sh ==
ok - batch dispatch re-execs and reports every id=repo pair
ok - a single id=repo pair routes through batch dispatch
ok - single-task invocation (no '=') is untouched by batch detection
ok - batch dispatch rejects an argument that is not id=repo
ok - an arg whose id part contains '/' is not treated as a batch pair
ok - FM_HOME scopes projects/ paths for single-task spawn
ok - FM_PROJECTS_OVERRIDE scopes projects/ paths for single-task spawn
== tests/fm-teardown.test.sh ==
ok - local-only worktree with HEAD on a fork remote is torn down (fix holds)
ok - local-only worktree with truly unpushed work is refused (safety preserved)
ok - local-only worktree with work merged into local main is torn down (no regression)
ok - no-mistakes worktree with HEAD on origin is torn down (no regression)
ok - no-mistakes worktree with truly unpushed work is refused (no regression)
ok - local-only worktree with unpushed work is torn down under --force (escape hatch)
== tests/fm-wake-queue.test.sh ==
ok - supervise daemon state root is scoped by FM_HOME
ok - concurrent append plus drain preserves queue records
ok - signal written while no watcher runs is caught on next run
ok - stale wake is queued before suppressor state is advanced
ok - check output is queued before cadence suppression
ok - simultaneous watcher starts leave exactly one live process
ok - two atomic drains cannot consume the same records twice
ok - drain collapses obvious duplicate heartbeat and signal records
ok - killed watcher stale lock is reclaimed
ok - live watcher lock with stale heartbeat is actionable
ok - guard warns when queued wakes are pending
ok - guard orders watcher re-arm after queued wake drain
ok - routine signal self-handles
ok - captain-relevant status verbs escalate
ok - check + unknown escalate; heartbeat self-handles
ok - transient stale self-handles and records a persistence marker
ok - stale + terminal status escalates immediately
ok - persistent stale escalates after threshold and clears its marker
ok - resumed (busy) stale clears its marker without escalating
ok - multiple escalations flush as a single batched digest
ok - batch flush measures max-delay from the first append, not the last
ok - catch-all scan escalates a missed terminal once, not twice
ok - handle_wake routes routine->self and captain->escalate
ok - INJECT_SKIP forces self-handle, bypassing captain-relevant classification
ok - is_wake_reason distinguishes watcher wake reasons from singleton-status stdout
ok - terminal-stale escalate removes its marker so housekeeping does not re-escalate
ok - captain signal escalate marks seen so the catch-all scan does not re-fire
ok - _collapse_newlines replaces newlines with literal separator
ok - afk flag absent: daemon does not inject, buffer preserved
ok - afk flag present: daemon injects with sentinel marker prefix
ok - injected digest is single-line (no embedded newlines)
ok - busy-guard defers injection when supervisor pane is busy
ok - marker detection: marker -> stay afk, no marker -> exit afk
ok - /afk invocation is exempt from afk exit (no self-cancel)
ok - should_exit_afk returns false when afk is not active
ok - strip_injection_marker removes the sentinel marker cleanly
ok - pane_input_pending detects partial input on the cursor line
ok - pane_input_pending: blank cursor line is not pending
ok - pane_input_pending: bare prompts are not pending (idle)
ok - composer guard defers injection when pane has pending input
ok - swallowed Enter: type-once + Enter-retry, no concatenation
ok - normal inject: exactly one digest, one Enter, no duplicates
ok - classify_signal dedupes against the catch-all scan seen marker
ok - classify_stale dedupes against the signal path seen marker

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • tests/fm-daemon-opencode.test.sh | tee /var/folders/kg/vqcvwwlx3xs4wblm4wpvpkz00000gn/T/no-mistakes-evidence/01KVTHAFWWYCCNMXDRS475F212/fm-daemon-opencode-test.log
  • for test_script in tests/*.test.sh; do printf '== %s ==\n' "$test_script"; "$test_script" || exit $?; done | tee /var/folders/kg/vqcvwwlx3xs4wblm4wpvpkz00000gn/T/no-mistakes-evidence/01KVTHAFWWYCCNMXDRS475F212/all-shell-tests.log
  • git status --short
  • find . -maxdepth 3 \( -name '.pytest_cache' -o -name 'node_modules' -o -name 'coverage' -o -name 'dist' -o -name 'tmp' -o -name '*.log' \) -print
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

e-jung and others added 4 commits June 23, 2026 11:08
…pervisor pane from stale scan

Two bugs that together completely broke autonomous (permanent-afk) supervision
when firstmate runs on opencode (the captain's harness):

Bug 1 — pane_input_pending() false-positives on opencode's idle composer.
opencode renders a bordered input widget on the cursor line, so a clean idle
prompt is NOT blank: the cursor line is '<indent>┃  Ask anything... "<sugg>"'
(┃ = U+2503 box-drawing border; 'Ask anything...' is the fixed empty-composer
placeholder). The old COMPOSER_IDLE_RE never matched it, so every injection was
deferred forever ('inject deferred: pending input'). Broaden the regex to
recognize opencode's idle composer (box-drawing border + placeholder, or
border-only chrome). Typed text ('┃  hello') matches neither, so the pending-
input guard still holds. A typed line resembling the placeholder but without
the literal '...' still classifies as pending.

Bug 2 — stale-wedge detector flagged the supervisor's own idle pane.
The supervisor pane (the opencode window running firstmate) is legitimately
idle between events while the captain is away; stale detection must apply ONLY
to crewmate panes (fm-* windows). Add is_crewmate_window() and guard
classify_stale + stale_marker_record so a non-crewmate window never records a
stale marker that housekeeping would age into a false wedge.

Verified against opencode 1.17.x's real idle rendering (live pane_input_pending
+ full inject path). New tests/fm-daemon-opencode.test.sh pins both fixes; all
existing daemon/supervision suites (fm-wake-queue, fm-afk-inject-e2e,
fm-teardown, fm-daemon-opencode) stay green. shellcheck clean.
@tommy230 tommy230 closed this Jun 24, 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