Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .agents/skills/bootstrap-diagnostics/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: bootstrap-diagnostics
description: >-
Agent-only handling playbook for session-start bootstrap diagnostics.
Use whenever the session-start digest's bootstrap section prints an actionable diagnostic line - MISSING, MISSING_MANUAL, BACKEND_INVALID, NEEDS_GH_AUTH, TANGLE, CREW_DISPATCH invalid, FLEET_SYNC, PR_CHECK_MIGRATION, SECONDMATE_SYNC, SECONDMATE_LIVENESS, NUDGE_SECONDMATES, or FMX - or when a standalone bin/fm-bootstrap.sh run prints one of those lines.
Use whenever the session-start digest's bootstrap section prints an actionable diagnostic line - MISSING, MISSING_MANUAL, BACKEND_INVALID, NEEDS_GH_AUTH, TANGLE, CREW_DISPATCH invalid, FLEET_SYNC, PR_CHECK_MIGRATION, PI_QUESTION_RECOVERY, SECONDMATE_SYNC, SECONDMATE_LIVENESS, NUDGE_SECONDMATES, or FMX - or when a standalone bin/fm-bootstrap.sh run prints one of those lines.
A silent bootstrap section, or a BOOTSTRAP_INFO fact, means no skill load.
user-invocable: false
metadata:
Expand Down Expand Up @@ -43,6 +43,8 @@ When any diagnostic needs captain attention, report the plain consequence and re
Resume the emitted supervision protocol after finishing the session-start wake handling.
- Any other `PR_CHECK_MIGRATION:` refusal means migration did not complete safely, whether because watcher exclusion, a private path, a diagnostic, quarantine validation, or marker publication could not be proved.
Keep each affected poll unavailable, inspect the named private state path, and do not bypass the migration or execute a quarantined artifact; a completed safe-scan marker allows unrelated authenticated polls to continue while private repair remains pending.
- `PI_QUESTION_RECOVERY: failed: <detail>` - one or more pending Pi question records could not be reconciled before the wake queue was drained.
Inspect the named private record, keep late answers unavailable, and rerun `FM_HOME=<this-firstmate-home> bin/fm-pi-question-recover.sh --all` after correcting the concrete filesystem, process-identity, or schema problem.
- `SECONDMATE_SYNC: secondmate <id>: skipped: <reason>` - the local-HEAD secondmate sync left a live secondmate home on its existing checkout because the home was dirty, diverged, unsafe, on the wrong branch, missing the primary target commit, or otherwise not fast-forwardable, or because inherited local-material propagation failed; bootstrap continued, but inspect the reason because the secondmate's tracked instructions, inherited settings, or shared captain preferences may be stale after a primary update.
- `SECONDMATE_LIVENESS: secondmate <id>: skipped: <reason>|respawn failed: <reason>` - the session-start liveness sweep could not guarantee that a live secondmate's recorded endpoint is running a real agent process.
Investigate the reason because that secondmate is not guaranteed live.
Expand Down
39 changes: 39 additions & 0 deletions .agents/skills/pi-supervised-question/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
name: pi-supervised-question
description: >-
Agent-only procedure for a Pi crew question wake whose status names state/questions/<task-id>/<call-id>.request.json or key=ask-<call-id>, including a repeated or stale question after recovery.
user-invocable: false
metadata:
internal: true
---

# Pi supervised question

Use this only for a Pi crew's versioned supervised-question request.
Ordinary `needs-decision` events continue to follow the existing task and validation lifecycle.

## Handle the request

1. Drain the durable wake queue before reading the request, as required for every wake-handling turn.
2. Derive the task ID and call ID only from the validated relative path in the status event, then read `state/questions/<task-id>/<call-id>.request.json` under the active `FM_HOME`.
3. Confirm that `state/<task-id>.meta` still records `harness=pi` and that the request's `taskId`, `callId`, `protocolVersion`, and deadline match the event and current task.
4. Treat the request questions as the pending decision artifact, not as permission to answer them.
Existing authority still applies, so escalate captain-owned, destructive, irreversible, or security-sensitive choices and wait for the answer.
5. Build one `answers` entry per question in request order using the exact selected option label.
6. Publish through the guarded helper with answer JSON on standard input:

```sh
printf '%s\n' '<answer-json>' | FM_HOME=<active-home> bin/fm-pi-answer.sh <task-id> <call-id>
```

7. Never use `fm-send`, chat injection, direct response-file writes, or an answer to a previous call ID.
The agent is blocked inside the same tool call, so only the bridge response can resume it safely.
8. Verify that the status log appended `resolved: Pi question answered [key=ask-<call-id>]`, then reconcile the crew's current state and resume the normal supervision cycle.

## Stale or recovered calls

If the guarded helper reports that the owner, bridge, deadline, or resolution is stale, do not retry that call ID.
Run `FM_HOME=<active-home> bin/fm-pi-question-recover.sh <task-id>`, confirm the old request is terminal, and wait for the resumed Pi agent to reissue the question with a new call ID.
An already recorded answer may be summarized through the resumed agent's normal context, but it is never copied into a new response without applying current authority and validating the new options.

Completion requires either a matching keyed `resolved` event followed by continued crew execution, or a concrete captain escalation that preserves the still-pending call without chat injection.
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ state/ volatile runtime signals; gitignored
<id>.turn-ended touched by turn-end hooks
<id>.grok-turnend-token firstmate-owned grok hook registry token for the task; removed by teardown
<id>.meta written by fm-spawn: window=, worktree=, project=, harness=, model=, effort=, kind=, mode=, yolo=, tasktmp=; kind=secondmate also records home= and projects=; a non-default runtime backend records further backend-specific fields (docs/configuration.md "Runtime backend"; bin/fm-backend.sh, section 8); fm-pr-check, including through fm-pr-merge, records one canonical pr= and GitHub's pr_head= when available; fm-x-link appends x_request=, x_request_ts=, x_followups=, and optional x_platform=/x_reply_max_chars= for an X-mode-originated task (section 14)
questions/ private versioned Pi supervised-question requests, responses, owner identities, resolutions, and locks; docs/pi-supervised-questions.md owns the schema and lifecycle
<id>.check.sh authenticated slow poll; the watcher dispatches validated PR data and the byte-identified X shim through trusted repository scripts, runs registered custom checks from hash-validated private snapshots, and rejects every other state check without execution
<id>.check-trust private content binding created by fm-check-register.sh for an intentional custom check
<id>.pr-poll private validated data sidecar for the byte-static PR merge poll
Expand Down Expand Up @@ -129,7 +130,7 @@ A lock-refused session must not spawn, steer, merge, drain the wake queue, repai
1. **Lock** - acquires the per-home session lock first, before anything mutates shared state.
2. **Bootstrap** - detect-only checks (tool/version problems, GitHub auth, the worktree-tangle check, harness override, dispatch-profile validation, backlog-backend status) always run, but routine confirmations stay silent by default.
When the lock could not be acquired, the worktree-tangle check uses read-only advisory wording without a checkout repair command.
The five MUTATING sweeps - non-executing legacy PR-check migration, fleet sync, the local secondmate fast-forward sweep, the secondmate liveness sweep, and X-mode artifact writes - run only when this session actually holds the lock from step 1.
The six MUTATING sweeps - non-executing legacy PR-check migration, Pi question recovery, fleet sync, the local secondmate fast-forward sweep, the secondmate liveness sweep, and X-mode artifact writes - run only when this session actually holds the lock from step 1.
The secondmate liveness sweep deterministically guarantees every registered secondmate is actually running: it probes each live secondmate's endpoint for a real agent process (not just pane presence), respawns only on a confident dead reading, and reports only skipped or failed guarantees as `SECONDMATE_LIVENESS:` lines (`bin/fm-bootstrap.sh`; `bin/fm-backend.sh`'s `fm_backend_agent_alive`).
3. **Wake queue** - when locked, drains the durable wake queue and prints the raw records prominently as this turn's first work queue; a bounded, clearly labeled historical status-event annotation may follow a valid `signal` record but never replaces it or current-state reconciliation, and a lapsed watcher chain still surfaces here via the same guard alarm.
When the lock could not be acquired, the queue is left untouched because another session owns it, and the guard's tangle/watcher-liveness alarms still print in read-only advisory mode without drain, supervision repair, or checkout repair commands.
Expand Down Expand Up @@ -326,6 +327,7 @@ A declared `paused:` event means a bounded external wait expected to clear on it
Handle actionable wakes as follows:

1. For `signal:`, read the listed event lines first, then reconcile current state only where action depends on it.
When a Pi `needs-decision` event names `state/questions/<task-id>/<call-id>.request.json` or `key=ask-<call-id>`, load `pi-supervised-question` and answer only through its guarded bridge procedure.
2. For `stale:`, inspect the recorded endpoint and load `stuck-crewmate-recovery` for a stopped, looping, confused, or unresponsive worker; a deep-inspection reason also requires current-state and validation-log inspection.
3. For `check:`, act on the named poll result, including merges and X-mode events.
4. For `heartbeat:`, review the whole fleet from the structured fleet view, reconcile suspicious tasks and PR state, update the backlog, and never report an unchanged fleet as progress.
Expand Down Expand Up @@ -449,7 +451,7 @@ It performs guarded fast-forward updates of firstmate and registered secondmate

These skills are not captain-invocable; load them only at their precise triggers.

- `bootstrap-diagnostics` - load whenever the session-start digest's bootstrap section prints an actionable diagnostic line (`MISSING:`, `MISSING_MANUAL:`, `BACKEND_INVALID:`, `NEEDS_GH_AUTH`, `TANGLE:`, `CREW_DISPATCH: invalid`, `FLEET_SYNC:`, `PR_CHECK_MIGRATION:`, `SECONDMATE_SYNC:`, `SECONDMATE_LIVENESS:`, `NUDGE_SECONDMATES:`, or `FMX:`); silence and `BOOTSTRAP_INFO:` need no load.
- `bootstrap-diagnostics` - load whenever the session-start digest's bootstrap section prints an actionable diagnostic line (`MISSING:`, `MISSING_MANUAL:`, `BACKEND_INVALID:`, `NEEDS_GH_AUTH`, `TANGLE:`, `CREW_DISPATCH: invalid`, `FLEET_SYNC:`, `PR_CHECK_MIGRATION:`, `PI_QUESTION_RECOVERY:`, `SECONDMATE_SYNC:`, `SECONDMATE_LIVENESS:`, `NUDGE_SECONDMATES:`, or `FMX:`); silence and `BOOTSTRAP_INFO:` need no load.
- `diagnostic-reasoning` - load before scoping a reported bug and before acting on a diagnostic report.
- `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.
- `firstmate-orca` - load before switching to Orca, spawning or supervising Orca-backed work, smoke-testing Orca backend behavior, debugging Orca task state, or reconciling Orca-backed task metadata.
Expand All @@ -460,6 +462,7 @@ These skills are not captain-invocable; load them only at their precise triggers
- `fmx-respond` - load on an `x-mention <request_id>` `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.
- `firstmate-coding-guidelines` - load before changing firstmate's shared, tracked material, as defined by section 1's list, whether editing directly or briefing a crewmate for a firstmate-repo task.
- `pi-supervised-question` - load when a Pi `needs-decision` wake names a versioned `state/questions/` request or `key=ask-<call-id>`, including stale or reissued calls after recovery.

## 14. X mode

Expand Down
11 changes: 8 additions & 3 deletions bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# "CREW_DISPATCH: invalid config/crew-dispatch.json - <reason>",
# "FLEET_SYNC: <repo>: skipped|recovered|STUCK: <detail>",
# "PR_CHECK_MIGRATION: <private remediation>",
# "PI_QUESTION_RECOVERY: failed: <detail>",
# "TANGLE: <remediation>",
# "SECONDMATE_SYNC: secondmate <id>: skipped: <reason>",
# "NUDGE_SECONDMATES: secondmate <id>: send failed: <reason>",
Expand Down Expand Up @@ -66,9 +67,10 @@
# refresh relays any completed fm-fleet-sync.sh output before the
# aggregate timeout skip line with timeout and elapsed seconds.
# Set FM_FLEET_PRUNE=0 to skip branch pruning during that refresh.
# Set FM_BOOTSTRAP_DETECT_ONLY=1 to skip the five MUTATING sweeps
# (PR-check migration, secondmate_sync, secondmate_liveness_sweep,
# x_mode_setup, fleet_sync) while still printing every read-only detect line
# Set FM_BOOTSTRAP_DETECT_ONLY=1 to skip the six MUTATING sweeps
# (PR-check migration, Pi question recovery, secondmate_sync,
# secondmate_liveness_sweep, x_mode_setup, fleet_sync) while still
# printing every read-only detect line
# above; the TANGLE line switches to advisory-only wording with no
# checkout command. Used by
# fm-session-start.sh's read-only path when another live session holds
Expand Down Expand Up @@ -800,6 +802,9 @@ if [ "${FM_BOOTSTRAP_VERBOSE_FACTS:-0}" = 1 ] \
echo "BOOTSTRAP_INFO: tasks-axi available"
fi
if [ "${FM_BOOTSTRAP_DETECT_ONLY:-0}" != 1 ]; then
if ! recovery_error=$(FM_HOME="$FM_HOME" FM_STATE_OVERRIDE="$STATE" "$SCRIPT_DIR/fm-pi-question-recover.sh" --all 2>&1); then
echo "PI_QUESTION_RECOVERY: failed: ${recovery_error:-unknown recovery error}"
fi
secondmate_sync
secondmate_liveness_sweep
x_mode_setup
Expand Down
23 changes: 20 additions & 3 deletions bin/fm-classify-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,14 @@ status_is_paused_or_captain_held() { # <status-line>
# terminal line never clears an open captain decision.
#
# Decision key grammar (backward-compatible with the existing "<verb>: <note>"
# format): an OPTIONAL "[key=<slug>]" token sits between the verb and the colon,
# format): an OPTIONAL "[key=<slug>]" token normally sits between the verb and the colon,
# needs-decision [key=api-shape]: <summary>
# resolved [key=api-shape]: <how it was decided>
# The Pi supervised-question wire protocol is the one recognized suffix form,
# so its bounded wake line ends with the call identity without making arbitrary
# note prose look like status control syntax:
# needs-decision: <relative request path> [key=ask-<call-id>]
# resolved: Pi question answered [key=ask-<call-id>]
# A line with no token uses the key "default", preserving the historical
# one-open-decision-per-task behavior (a bare "resolved:" closes "default").
# The three parsers are pure reads of a single line; the verb parser strips any
Expand All @@ -145,7 +150,7 @@ status_line_note() { # <status-line> -> text after the first colon, trimmed
esac
}
_fm_decision_key() { # <status-line> -> key slug, or "default" when no token
local prefix=${1%%:*} k
local line=$1 prefix=${1%%:*} k
case "$prefix" in
*\[key=*\]*)
k=${prefix#*\[key=}
Expand All @@ -155,7 +160,19 @@ _fm_decision_key() { # <status-line> -> key slug, or "default" when no token
*) printf '%s' "$k" ;;
esac
;;
*) printf 'default' ;;
*)
case "$line" in
needs-decision:\ Pi\ question\ request\ state/questions/*\ \[key=ask-*\]|resolved:\ Pi\ question\ *\ \[key=ask-*\])
k=${line##* [key=}
k=${k%]}
case "$k" in
''|*[!A-Za-z0-9._-]*) return 1 ;;
*) printf '%s' "$k" ;;
esac
;;
*) printf 'default' ;;
esac
;;
esac
}
# Drop the record for <key> from a newline-terminated "<key>\t<verb>\t<note>" set.
Expand Down
Loading
Loading