diff --git a/.agents/skills/harness-adapters/SKILL.md b/.agents/skills/harness-adapters/SKILL.md index 0e441fd6d2b..535325a3f3b 100644 --- a/.agents/skills/harness-adapters/SKILL.md +++ b/.agents/skills/harness-adapters/SKILL.md @@ -1,6 +1,9 @@ --- name: harness-adapters -description: Agent-only reference for firstmate harness operations. Use before spawning or recovering a crewmate or secondmate, handling a trust dialog, sending a harness-specific skill invocation, interrupting or exiting an agent, resuming an exited agent, or verifying a new harness adapter. Contains verified facts for claude, codex, opencode, pi, and grok. +description: >- + Agent-only reference for firstmate harness operations. + Use before spawning or recovering a crewmate or secondmate, handling a trust or permission dialog, sending a harness-specific skill invocation, interrupting or exiting an agent, resuming an exited agent, or verifying a new harness adapter. + Contains verified facts for claude, codex, opencode, pi, and grok. user-invocable: false metadata: internal: true @@ -47,6 +50,14 @@ When verifying a new adapter, record its env marker and command name in `bin/fm- For stuck recovery, the target window's harness is recorded as `harness=` in `state/.meta`. Use that value for interrupt, exit, resume, and skill-invocation facts. +## Mid-run permission prompts + +Firstmate launches every verified crewmate harness in its unattended mode, but a managed policy, a newly introduced tool class, or a failed autonomy flag can still produce a permission prompt after work has started. +`bin/fm-watch.sh` owns the deterministic pane matcher and the busy/no-progress system-dialog fallback. +`docs/permission-stall-detection.md` owns the empirical capture and macOS capability record. +On either permission wake, load `stuck-crewmate-recovery` and follow its permission-blocked branch before using any interrupt, exit, or relaunch mechanic below. +Never apply an adapter's startup trust-dialog acceptance rule to a mid-run command or capability grant. + ## Primary turn-end guard Every verified primary harness has an empirically validated hook path for the "no turn ends blind" guard. @@ -109,10 +120,13 @@ Natural language is acceptable if uncertain. | Exit command | `/exit` | | Interrupt | single Escape | | Skill invocation | `/` (e.g. `/no-mistakes`) | +| Protected mid-run grant shapes | `Do you want to proceed?` together with the `Esc to cancel · Tab to amend` footer, or `Quick safety check: Is this a project you created or one you trust?` together with `Yes, I trust this folder`, `No, exit`, and the confirmation footer. | First launch in a fresh worktree, or first ever on a machine, may show a trust or bypass-permissions confirmation. After every spawn, peek the pane within about 20 seconds. -If such a dialog is showing, accept it from an active firstmate session using `FM_HOME= bin/fm-send.sh --key Enter`, or the choice the dialog requires, unless `FM_HOME` is already set to the active firstmate home; verify the brief started processing. +If a workspace trust or bypass-permissions dialog is showing before the brief starts processing, accept it from an active firstmate session using `FM_HOME= bin/fm-send.sh --key Enter`, or the choice the dialog requires, unless `FM_HOME` is already set to the active firstmate home; verify the brief started processing. +The separate `Hooks need review` prompt with `Trust all on first launch` is Firstmate's own supervision-hook trust path and must also be accepted with Enter before the brief starts. +Once the brief has started processing, treat either protected shape in the table as a security-sensitive mid-run grant and escalate it to the captain without pressing an approval or denial key. Claude renders a predicted-next-prompt suggestion as dim/faint text inside an otherwise-empty composer after a turn completes. A plain `tmux capture-pane` cannot tell that ghost text apart from typed text. @@ -139,6 +153,7 @@ Claude Code's primary watcher protocol is the lowest-friction path: run `bin/fm- | Exit command | `/quit` (slash popup needs about 1 second between text and Enter; `fm-send` handles it) | | Interrupt | single Escape | | Skill invocation | `$` (e.g. `$no-mistakes`); `/` is claude-only and codex rejects it as "Unrecognized command" | +| Protected mid-run grant shapes | One of `Would you like to run the following command?`, `Would you like to grant these permissions?`, `Would you like to make the following edits?`, or `Do you want to approve network access to ""?`, together with its title-specific choices and `Press enter to confirm or esc to cancel`; or `Do you trust the contents of this directory?` together with `Yes, continue` and `No, quit`. | A `$` invocation opens a `$`-autocomplete (skill) popup, the same hazard as the `/` slash popup: submitting too fast lets the popup swallow the Enter, so the invocation never lands. `fm-send` handles it the same way it handles `/` - it gives the popup a longer settle (1.2s) between typing and the first Enter, with the target backend's submit retry as the safety net - but the `$` settle is scoped to `harness=codex`, read from the target metadata for exact task ids or legacy `fm-` labels. @@ -147,8 +162,9 @@ An explicit `session:window` target has no meta, so its harness is unknown and t This is why the validation trigger (`$no-mistakes`) to a codex crew now lands on the first Enter instead of biting the popup. Directory trust dialog on first run per repo root: "Do you trust the contents of this directory?" -Accept with Enter. +Accept with Enter only during the spawn-time peek before the brief starts processing. The decision persists for the repo, so later worktrees of the same project skip it. +If that shape appears after work has started, escalate it to the captain as a mid-run directory-trust grant without pressing an approval or denial key. Resume after exit with `codex resume `. The session id is printed on quit. diff --git a/.agents/skills/stuck-crewmate-recovery/SKILL.md b/.agents/skills/stuck-crewmate-recovery/SKILL.md index 2204c6bb7ef..d8b1d1e20e9 100644 --- a/.agents/skills/stuck-crewmate-recovery/SKILL.md +++ b/.agents/skills/stuck-crewmate-recovery/SKILL.md @@ -1,6 +1,9 @@ --- name: stuck-crewmate-recovery -description: Agent-only playbook for stuck firstmate direct reports. Use after a stale wake, looping pane, repeated confusion, an answered-by-brief question, an unresponsive crewmate, or a failed steer. Escalates from peek, to one-line steer, to harness-specific interrupt, to relaunch with progress, to failed status. +description: >- + Agent-only playbook for stuck firstmate direct reports. + Use after a stale wake, permission-prompt or system-dialog suspicion, looping pane, repeated confusion, an answered-by-brief question, an unresponsive crewmate, or a failed steer. + Routes permission grants to the captain before the ordinary peek, steer, interrupt, relaunch, and failed-status ladder. user-invocable: false metadata: internal: true @@ -8,12 +11,30 @@ metadata: # stuck-crewmate-recovery -Use this playbook when a direct report is stale, looping, repeatedly confused, asking a question its brief already answers, unresponsive, or when a steer failed to land. +Use this playbook when a direct report is stale, permission-blocked, looping, repeatedly confused, asking a question its brief already answers, unresponsive, or when a steer failed to land. -Load `harness-adapters` before sending an interrupt, exit command, resume command, or harness-specific skill invocation. +Load `harness-adapters` before handling a permission prompt or sending an interrupt, exit command, resume command, or harness-specific skill invocation. The target window's harness is recorded as `harness=` in `state/.meta`. -Escalate in order: +## Permission-blocked branch + +Handle permission evidence before the ordinary recovery ladder because approving, denying, interrupting, or relaunching can change the security decision or hide the only useful evidence. + +1. Peek the pane and read the requested command, tool, directory, or capability. +2. If the wake says `permission-prompt detected`, or the pane matches the target's mid-run permission shape in `harness-adapters`, do not press an approval or denial key and do not interrupt or relaunch the agent. +3. Tell the captain what action is waiting and ask them to approve or deny it in the visible prompt. + Keep the message product-facing, such as `Work is blocked waiting for permission to ; please approve or deny that request in the visible dialog.` + Do not name the harness, pane, task id, watcher, or other fleet machinery. +4. If the wake says `permission/system-dialog suspected`, remember that it is a timeout heuristic rather than a directly observed macOS dialog. + Inspect the pane for the foreground action and any clear evidence of ordinary progress. + If ordinary progress is visible, resume supervision and treat the wake as a conservative false positive. + Otherwise tell the captain `A macOS permission dialog may be blocking ; please grant or deny it in the visible dialog or in Privacy & Security in System Settings.` +5. Never try to grant a macOS TCC permission with keystrokes, AppleScript, Accessibility APIs, `tccutil`, or a settings-database edit. + macOS owns that decision and the captain must make it. +6. After the captain clears the dialog, verify that the pane changes or `bin/fm-crew-state.sh ` reports resumed work before returning to supervision. + If the permission was denied and the task still cannot proceed, use the ordinary ladder below with that denial as evidence. + +For a non-permission stall, escalate in order: 1. Peek the pane. 2. If the crewmate is waiting on a question its brief already answers, answer in one line via `FM_HOME= bin/fm-send.sh` from an active firstmate session unless `FM_HOME` is already set to the active firstmate home. diff --git a/AGENTS.md b/AGENTS.md index b0cbb7d6c6e..3136877408b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,7 +111,7 @@ state/ volatile runtime signals; gitignored .afk durable away-mode flag; present = sub-supervisor may inject escalations (set by /afk, cleared on user return) .lock per-home session lock written by fm-lock.sh: harness PID on line 1, holder process start time on line 2; direct readers parse only the first-line PID, while acquire/status treat it as held only when that PID is live with a matching start time (skipped for a legacy one-line lock, which is held while its PID is a live harness) and is not a Codex app-server .watch.lock .wake-queue.lock watcher singleton and queue serialization locks - .hash-* .count-* .stale-* .stale-since-* .paused-* .wedge-escalations-* .seen-* .hb-surfaced-* .last-* .heartbeat-streak watcher internals; never touch + .hash-* .count-* .stale-* .stale-since-* .paused-* .wedge-escalations-* .brief-started-* .seen-* .hb-surfaced-* .last-* .heartbeat-streak watcher internals; never touch .watch-triage.log watcher's absorbed-wake debug log (size-capped); never relied on, safe to delete .last-watcher-beat watcher liveness beacon, touched every poll (including while absorbing benign wakes); guard scripts read it .subsuper-* .supervise-daemon.* sub-supervisor internals; never touch @@ -561,11 +561,11 @@ Whenever at least one task is in flight, keep exactly one live supervision wait The emitted block is the only per-harness operating recipe in the session context. Do not substitute another harness's command shape for it. **Always-on wake triage (absorb only when provably working).** -`bin/fm-watch.sh` classifies every wake in bash and absorbs the benign majority without waking you: crews with positive working evidence (an actively-running no-mistakes step for their branch, or a busy pane, read via `bin/fm-crew-state.sh`), a declared `paused:` external wait until its bounded recheck cadence, and no-change heartbeats. +`bin/fm-watch.sh` classifies every wake in bash and absorbs the benign majority without waking you: crews with positive working evidence (an actively-running no-mistakes step for their branch, or a busy pane read via `bin/fm-crew-state.sh`) unless the separate permission-stall no-progress threshold has expired, a declared `paused:` external wait until its bounded recheck cadence, and no-change heartbeats. It never absorbs a crewmate that stopped without that evidence - whatever its stale status log claims - and only an actionable wake is queued durably and ends the supervision wait, so you resume the emitted protocol exactly once per actionable event. A `paused:` status is a deliberate external wait, not `blocked:`; its initial signal still surfaces once, and a forgotten pause re-surfaces for a recheck once per window. -Repeated provably-working stale escalations on one unchanged pane eventually add `demand-deep-inspection` to the wake reason so it is not mistaken for another routine validation wait. -`docs/architecture.md` ("Event-driven supervision") owns the full classification mechanism, its thresholds, and the shared classifier library; while `state/.afk` exists the daemon owns triage and the watcher surfaces every wake to it. +Repeated unchanged wedge or permission-stall escalations eventually add `demand-deep-inspection` to the wake reason so they are not mistaken for another routine validation wait. +`docs/architecture.md` ("Event-driven supervision") owns the general classification mechanism and shared classifier library, while `docs/permission-stall-detection.md` owns permission-prompt matching and the macOS timeout heuristic; while `state/.afk` exists the daemon owns triage and the watcher surfaces every wake to it. At the start of every wake-handling turn, run `bin/fm-wake-drain.sh` before peeking panes, reading status files beyond the reason line, or starting new work. Session-start recovery is the exception: `bin/fm-session-start.sh` already drained the queue when locked, or deliberately skipped the drain when read-only because another session owns it. The printed reason line is still useful, but the drained queue is the lossless backlog. @@ -600,7 +600,9 @@ On wake, in order of cheapness: 1. Read the reason line and drain queued wake records with `bin/fm-wake-drain.sh`. 2. `signal:` read the listed status files first; a wake lists every signal that landed within the coalescing grace window (e.g. a status write plus the same turn's turn-end marker), and each is ~30 tokens and usually sufficient. A status line is the wake *event*, not the crewmate's current state; when you need the live state - especially to confirm a `needs-decision`/`blocked`/`paused` status is still real and not already resolved-and-resumed - read it with `bin/fm-crew-state.sh `, which reconciles the authoritative run-step over the possibly-stale log line, and never `tail` the status log as the current-state source. -3. `stale:` the crewmate stopped without reporting; peek the pane (`bin/fm-peek.sh `) to diagnose. +3. `stale:` the crewmate stopped without reporting, a recognized mid-run permission prompt is waiting, or a busy pane exceeded the possible system-dialog no-progress threshold. + If the reason includes `permission-prompt detected` or `permission/system-dialog suspected`, load `stuck-crewmate-recovery` before taking any ordinary recovery action and follow its permission-blocked branch. + Otherwise peek the pane (`bin/fm-peek.sh `) to diagnose. If the stale reason includes `demand-deep-inspection`, inspect the pane, `bin/fm-crew-state.sh `, and the validation logs before resuming supervision. If the pane is waiting, looping, confused, or unresponsive, load `stuck-crewmate-recovery`. 4. `check:` a per-task poll fired (usually a merge, or X mode when enabled); act on it. @@ -655,7 +657,7 @@ Inline facts that must survive without a loaded skill: ### Stuck-crewmate recovery -On `stale`, looping, repeated confusion, an answered-by-brief question, an unresponsive pane, or a failed steer, load `stuck-crewmate-recovery`. +On `stale`, `permission-prompt detected`, `permission/system-dialog suspected`, looping, repeated confusion, an answered-by-brief question, an unresponsive pane, or a failed steer, load `stuck-crewmate-recovery`. That playbook escalates from peek, to one-line steer, to harness-specific interrupt, to relaunch with a progress note, to `failed` with evidence. ## 9. Escalation and captain etiquette @@ -774,10 +776,10 @@ It performs only fast-forward self-updates of firstmate and registered secondmat These skills are not captain-invocable; they are conditional operating references you must load at the trigger points below. - `bootstrap-diagnostics` - load whenever the session-start digest's bootstrap section prints any diagnostic or capability line (`MISSING:`, `MISSING_MANUAL:`, `BACKEND_INVALID:`, `ACCOUNT_ROUTING:`, `NEEDS_GH_AUTH`, `TANGLE:`, `CREW_HARNESS_OVERRIDE:`, `CREW_DISPATCH:`, `FLEET_SYNC:`, `SECONDMATE_SYNC:`, `SECONDMATE_LIVENESS:`, `TASKS_AXI:`, `NUDGE_SECONDMATES:`, `REPORT_RETENTION:`, or `FMX:`); silence needs no load. -- `harness-adapters` - load before spawning or recovering a crewmate or secondmate, handling a trust dialog, sending a harness-specific skill invocation, interrupting or exiting an agent, resuming an exited agent, or verifying a new harness adapter. +- `harness-adapters` - load before spawning or recovering a crewmate or secondmate, handling a trust or permission dialog, sending a harness-specific skill invocation, interrupting or exiting an agent, resuming an exited agent, or verifying a new harness adapter. - `operating-fundamentals` - load when intaking any captain ask, deciding whether to dispatch or work inline, supervising under load, handling a blocked lane or finished crew, protecting shared validation capacity, acting on an explicit captain order, or about to assert a fleet fact. - `firstmate-orca` - load before recovering or supervising legacy Orca-backed work, testing Orca backend behavior, debugging Orca task state, or reconciling Orca-backed task metadata. -- `stuck-crewmate-recovery` - load after a stale wake, looping pane, repeated confusion, an answered-by-brief question, an unresponsive crewmate, or a failed steer. +- `stuck-crewmate-recovery` - load after a stale wake, permission-prompt or system-dialog suspicion, looping pane, repeated confusion, an answered-by-brief question, an unresponsive crewmate, or a failed steer. - `secondmate-provisioning` - load before creating, seeding, validating, launching, handing backlog to, recovering, pushing inherited config into, or retiring a secondmate home, and before editing `data/secondmates.md`. - `fmx-respond` - load on an `x-mention ` `check:` wake to handle the mention, on an `x-mode-error ...` `check:` wake to report the X-mode configuration blocker, and on any milestone or terminal wake for an X-mode-linked task before posting its completion follow-up; relevant only when X mode is on. - `firstmate-codexapp` - load before coordinating a visible Codex Desktop thread, evaluating a Codex App backend request, or reconciling Codex Desktop host-tool smoke evidence for Firstmate work. diff --git a/bin/fm-watch.sh b/bin/fm-watch.sh index 4246204157f..a4ca5b575be 100755 --- a/bin/fm-watch.sh +++ b/bin/fm-watch.sh @@ -30,7 +30,12 @@ # also carries a "demand-deep-inspection" marker so the # wake payload itself, not just repetition, forces a # closer look instead of another routine supervision -# resume. Unless afk is active. +# resume. A recognized harness permission prompt surfaces +# immediately with an explicit permission-blocked reason. +# A pane that remains busy without meaningful output, +# status, or turn-end progress surfaces after +# FM_PERMISSION_STALL_ESCALATE_SECS as a possible macOS +# permission/system-dialog block. Unless afk is active. # check: