Skip to content

fix(supervision): opencode-aware composer-idle detection + exclude supervisor pane from stale scan - #49

Closed
e-jung wants to merge 1 commit into
kunchenguid:mainfrom
e-jung:fm-daemon-opencode-fix-r7
Closed

e-jung wants to merge 1 commit into
kunchenguid:mainfrom
e-jung:fm-daemon-opencode-fix-r7

Conversation

@e-jung

@e-jung e-jung commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What

Two fixes for bin/fm-supervise-daemon.sh that together broke autonomous (permanent-afk) supervision when firstmate runs on opencode.

Bug 1 — pane_input_pending() false-positives on opencode's idle composer

opencode renders a bordered input widget (box-drawing ┃ + an "Ask anything…" placeholder) on the cursor line at a clean idle prompt. The composer-idle check read that as a non-empty line, returned "pending" forever, and the daemon deferred every injection indefinitely — so nothing was delivered while the user was away.

Fix: broadened the shared COMPOSER_IDLE_RE_DEFAULT to recognize opencode's border-only line and the "Ask anything…" placeholder as idle. Existing patterns for claude/codex/pi (bare \$ > ❯ % # prompts, esc (to )?interrupt, Working…) are unchanged.

Bug 2 — stale scan flagged the supervisor's own idle pane

While the user is away, the supervisor pane is legitimately idle; the stale detector flagged it as stale persisted (possible wedge) every cycle, flooding the escalation buffer. Fixed by excluding the supervisor pane from stale-wedge detection (only crewmate panes are candidates).

Verification (honest)

  • Live-verified on opencode (our harness): idle pane no longer false-defers; injection lands on walk-away.
  • claude/codex/pi: covered by the preserved shared-regex patterns + existing fake-tmux unit tests, but not live-retested — we only run opencode. The change is purely additive (no existing pattern narrowed/removed), and the new patterns match only a line that is solely a box-drawing border or the opencode placeholder, so a real typed line on another harness won't false-match.
  • New tests/fm-daemon-opencode.test.sh (byte-level fixtures from a real opencode 1.17.x pane) + all existing daemon suites green; shellcheck clean.

Why share upstream

Even if this doesn't merge, documenting the opencode idle-composer edge case + the additive fix may help anyone running firstmate's afk daemon on opencode. We hit it in production (permanent-afk deployment) and wanted to contribute the fix back.

AI disclosure: Human-reviewed.

Note: the PR must be raised via no-mistakes check fails due to the fork-routing gap (#293, being fixed separately). The 3 real checks (behavior tests / lint / invariants) pass.

…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.
@kunchenguid

Copy link
Copy Markdown
Owner

thanks for this. quick note on process - i require PRs to come through no-mistakes, which is why the "PR must be raised via no-mistakes" check is red here (this was opened directly).

the reason i ask for it: no-mistakes runs review, tests, lint, and docs on your branch before the PR is opened, so contributions arrive already validated. that keeps the quality bar high and keeps me from having to hand-review every change - i maintain this solo, so it's the only way i keep up.

can you re-raise it through no-mistakes? running the pipeline on your branch opens/updates the PR through the gate and turns that check green. the change looks useful, it just needs to come through that path. ping me if the setup gives you trouble.

@e-jung e-jung closed this Jul 17, 2026
pranaypratyush added a commit to pranaypratyush/firstmate that referenced this pull request Aug 24, 2026
* fix(afk): wait for Herdr launch pane readiness

* no-mistakes(review): Hardened Herdr launch regression coverage and rollback assertions

* no-mistakes(document): Document Herdr away-launch readiness boundary
@ki-za

ki-za commented Aug 29, 2026

Copy link
Copy Markdown

Scout plan (huddle-app second mate, 2026-08-29)

Proposed Options / Plan / Blocking / Lead decisions for this ticket, from the ha-theme-plan scout report.

Current state

  • app/src/theme/lume.css:4-17 declares the policy values as public --lume-* variables. It has no stable background, surface, divider, tier, or signal tokens and lacks the three proposed Herdr-grey swatches.
  • app/src/app.css:19-710 consumes the Lume variables directly. Markdown headings use the directory-icon value, code uses the green-icon value, and focus uses the match value (app/src/app.css:422-456), so component semantics are coupled to one theme's provenance.
  • No activeContribution state assigns exactly one member contribution to tier-bright; all panel borders currently share one colour (app/src/app.css:264,287,358).
  • Hex escapes the theme at app/index.html:6 and in the fake attachment SVG at app/src/lib/contract/fake.ts:9. No automated check rejects either hex or swatch names outside app/src/theme/.
  • app/src/main.ts:3-4 already gives one clean import seam: theme first, component CSS second. A validated data theme has no current runtime requirement.

Options

  • CSS theme (recommended). Keep one CSS file as the only swatch-to-role mapping. This matches the current import seam and adds no runtime state.
  • Validated data theme. Validate TypeScript or JSON and generate the same CSS. This adds a build boundary and becomes useful only when runtime theme switching is required.

Recommended Plan

Modules

  • Rewrite app/src/theme/lume.css to declare private theme-local swatches and the ten public role tokens: --background, --surface, --divider, --tier-faint, --tier-base, --tier-bright, and the four --signal-* tokens.
  • Migrate app/src/app.css to those role tokens only. Remove direct Lume, icon, match, warning, and border variable use.
  • Add activeContribution presentation state to app/src/lib/huddle/HuddleView.svelte; focus or pointer engagement changes it through event handlers, and the first member contribution is active when none has been engaged.
  • Remove the literal theme colour from app/index.html and set it from computed --background in app/src/main.ts. Replace the fake SVG's branded literals with a presentation fixture that consumes role tokens.
  • Add scripts/check-theme.ts, wire it into bun run check, and reject hex outside app/src/theme/, swatch names outside the active theme file, undeclared role tokens, and direct component use of theme-local swatches.

Seams

  • Components and app/src/app.css request role tokens only; app/src/theme/lume.css resolves them to swatches.
  • Use the final ticket mapping: Forest Floor → background; Pine Shadow → surface; Pine Bark → divider; Blue Slate → tier-faint; Moonlit Cream → tier-base; Frost Blue → tier-bright; Old Gold/Faded Blossom/Fern Green/Sunlit Ochre → question/error/success/selection.
  • Most UI elements use tier-faint; authored content uses tier-base; only activeContribution uses tier-bright. Signals apply only to their named meaning.
  • app/src/main.ts remains the default-theme entrance. No server, ledger, or contract client code imports a theme.

Validation criteria

  • Static check: every colour declaration outside app/src/theme/ is var()/color-mix() over an allowed role token; zero hex and zero swatch names occur elsewhere.
  • Component check: every visible element resolves to one placement role, tier, or signal; exactly one rendered member contribution has tier-bright in fresh, keyboard-focused, and pointer-focused states.
  • Held-out check: a test light theme changes only files under app/src/theme/ and renders the same fixture without component edits.
  • Picture evidence: capture the fresh, question, error, success, selection, keyboard-focus, and idle states in Lume and the held-out theme. Include computed token values beside the Lume capture so policy values remain auditable.
  • Run bun run check.

Blocking?

non-blocking

Lead decisions

None. The final #49 ruling already chooses component role tokens and a theme-local swatch boundary; CSS is the smallest reversible implementation of that ruling.

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.

3 participants