Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .agents/skills/agent-skill-trigger-index/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ metadata:

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

- `operational-home-layout` - load when locating, interpreting, or changing Firstmate home, config, data, state, project, or generated runtime paths.
- `session-start-recovery` - load when the session-start digest reports unfinished checks, actionable diagnostics, recovery inputs, or output requiring interpretation.
- `bootstrap-diagnostics` - load whenever the session-start digest's bootstrap or network-checks section prints an actionable diagnostic line (`MISSING:`, `MISSING_MANUAL:`, `PRESENTATION_UNAVAILABLE:`, `BACKEND_INVALID:`, `NEEDS_GH_AUTH`, `TANGLE:`, `STARTUP_MEMORY_BUDGET:`, `CREW_DISPATCH: invalid`, `FLEET_SYNC:`, `NETWORK_CHECKS:`, `HOME_SUMMARY:`, `BACKLOG_RECONCILE:`, `SECONDMATE_SYNC:`, `SECONDMATE_LIVENESS:`, `SECONDMATE_HANDOFF:`, `NUDGE_SECONDMATES:`, or `FMX:`), or when `BOOTSTRAP_INFO:` says an interrupted backlog cleanup may have left an endpoint or local copy; silence and other `BOOTSTRAP_INFO:` facts need no load.
- `diagnostic-reasoning` - load before scoping a reported bug and before acting on a diagnostic report.
- `ask-user-authority` - load before deciding any ask-user finding.
- `validation-supervision` - load when a ship starts or already has an active no-mistakes validation run, including a mid-run requirement change or finding, and before deciding or answering any ask-user finding.
- `ship-landing` - load when a ship reports a PR or ready branch, when deciding or monitoring landing, and before task cleanup.
- `scout-completion` - load when a scout reports completion, presents a visual artifact for iteration, or is being considered for promotion to implementation.
- `quota-array-dispatch` - load before choosing among a matched crew-dispatch profile array from current quota-axi default TOON.
- `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 @@ -21,6 +26,7 @@ These skills are not captain-invocable; load them only at their precise triggers
- `stuck-crewmate-recovery` - load when the session-start digest reports an ordinary direct report's endpoint dead or its metadata has no window, after a stale wake, looping pane, repeated confusion, an answered-by-brief question, an unresponsive crewmate, or a failed steer, and whenever a live worker reports its no-mistakes pipeline dead, unreachable, or timed out.
- `secondmate-provisioning` - load before creating, seeding, validating, launching, handing backlog to, recovering, pushing inherited local material into, or retiring a secondmate home, and before editing `data/secondmates.md`.
- `captain-hold-lifecycle` - load before treating an investigation or visual review as complete, before ending a visual review that exposed a captain decision, when recording or routing the captain's answer, and on any `RECORD DIVERGENCE` line from the wake drain.
- `away-quiet-supervision` - load whenever /afk or /quiet is invoked, an away or quiet record exists, or a marked away-supervisor message arrives.
- `process-event-sources` - load before arming a long-polling source, before registering a deterministic condition->action watch (do X as soon as Y is true), on any `procevent <adapter> <source-id> <sequence>` check wake, and on any `process-event source stranded` or `process-event source failed to start` check wake.
Never run a registered source's blocking command yourself in a conversational turn.
- `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 Relay configuration blocker, on a `public-followup ...` `check:` wake or a startup-surfaced public commitment, and on any milestone or terminal wake for a Relay-linked task before posting its completion follow-up; relevant only when Relay is on.
Expand Down
1 change: 1 addition & 0 deletions .agents/skills/operational-home-layout/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ config/crew-harness crewmate harness override; LOCAL, gitignored; absent or "de
config/claude-permission-mode optional one-token permission posture for every Claude worker launch: absent or "bypass" keeps --dangerously-skip-permissions, "auto" launches with --permission-mode auto; LOCAL, gitignored; inherited by secondmate homes; see docs/configuration.md "Claude permission mode"
config/claude-account config/pi-account optional per-home worker account pin for Claude and Pi launches; LOCAL, gitignored, not inherited; absent keeps today's ambient account; present refuses a launch unless the pinned account resolves and is signed in (section 4 owns the refusal rule); see docs/configuration.md "Worker account pin"
config/crew-dispatch.json optional crewmate dispatch profiles; LOCAL, gitignored; firstmate-maintained but human-editable natural-language rules that choose a per-task harness/model/effort profile (section 4). Inherited by secondmate homes
config/project-capacity optional per-machine count of workers each named project admits at once, read from the root home by every local home; LOCAL, gitignored; see docs/configuration.md "Project capacity"
config/secondmate-harness harness the PRIMARY uses to launch SECONDMATE agents, optionally followed by a model and effort token on the same line ("<harness> [<model>] [<effort>]"; section 4); LOCAL, gitignored; absent or "default" harness falls back to config/crew-harness then firstmate's own. The primary's own setting; NOT inherited into secondmate homes (secondmates do not spawn secondmates)
config/backlog-backend backlog backend override; LOCAL, gitignored; absent or "tasks-axi" = the configured tasks-axi backend, "manual" = force routine backlog updates to hand-editing; inherited by secondmate homes (section 10)
config/backend runtime session-provider backend override for new tasks; LOCAL, gitignored; absent = falls through to runtime auto-detection (the runtime firstmate itself is executing inside), then tmux; tmux is the verified reference backend (docs/tmux-backend.md), herdr has its own required CI lane (docs/herdr-backend.md), while zellij, orca, and cmux remain experimental with no dedicated real-backend CI lane (docs/zellij-backend.md, docs/orca-backend.md, docs/cmux-backend.md) - herdr and cmux can also be selected by runtime auto-detection, zellij and orca never are (always explicit), and codex-app is not accepted; see docs/codex-app-backend.md; inherited by secondmate homes under the primary-authoritative contract in secondmate-provisioning
Expand Down
30 changes: 24 additions & 6 deletions .opencode/plugins/fm-primary-watch-arm.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { spawn, spawnSync } from "node:child_process";
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
import { existsSync, readFileSync, realpathSync } from "node:fs";
import { resolve } from "node:path";
import { encodeFirstmateOperationalInput } from "./lib/fm-operational-input.js";

Expand Down Expand Up @@ -117,14 +117,32 @@ async function isPrimaryRoot(root, home) {
return gitDir.stdout.trim() === commonDir.stdout.trim();
}

// bin/fm-supervision-lib.sh's fm_supervision_needed is the single owner of the
// arm condition set (the turn-end guard decides with the same shared
// predicate), so this plugin can never disagree with the guard again. Away
// mode stays a local decline: its daemon owns supervision. X-mode homes arm
// before their relay poll is registered in the state directory.
function shouldArm(paths) {
if (existsSync(`${paths.state}/.afk`)) return false;
if (existsSync(`${paths.config}/x-mode.env`)) return true;
try {
return readdirSync(paths.state).some((name) => name.endsWith(".meta"));
} catch {
return false;
}
return supervisionNeeded(paths);
}

// fm_supervision_needed <state-dir> exits 0 exactly when the shared predicate
// says the home needs supervision; exit 0 means arm here.
function supervisionNeeded(paths) {
const result = spawnSync(
"bash",
[
"-c",
'. "$1/bin/fm-supervision-lib.sh" && fm_supervision_needed "$2"',
"fm-primary-watch-arm",
paths.root,
paths.state,
],
{ stdio: "ignore" },
);
return result.status === 0;
}

async function sessionOwnsLock(paths) {
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ Tracked files hold shared instructions and tooling; `data/` holds durable privat

Load `operational-home-layout` when locating, interpreting, or changing Firstmate home, config, data, state, project, or generated runtime paths.


A `state/<id>.status` line is a wake event, not current-state truth; `bin/fm-crew-state.sh` owns current-state reconciliation.
Treat `data/captain.md` as the domain-local record of captain preferences, optional `data/captain-shared.md` as the main-authoritative shared captain-preference file for secondmate inheritance, and `data/learnings.md` as curated home-local knowledge, regardless of harness memory.

Expand Down Expand Up @@ -196,6 +195,7 @@ An unregistered project or absent registry resolves to `no-mistakes` with yolo o
Record the resulting mode, `yolo` merge posture, and the one-line reason for any deviation in the backlog item note.

Treat file or subsystem overlap as a risk signal rather than an automatic reason to wait, and dispatch isolated work immediately with no concurrency cap when each change can be independently implemented and validated and the selected delivery path can reconcile ordinary rebases or conflicts.
A project's declared machine capacity (`config/project-capacity`) still bounds that dispatch: a spawn beyond it exits 75 without launching, and its item stays queued rather than blocked.
Serialize only for a true semantic dependency, shared mutable external state, incompatible concurrent migration, or another concrete condition that makes independent progress or reconciliation unsafe; same-file editing alone is insufficient, and genuine blockers remain durable.
Write the task-specific brief under section 11 before spawning.
Fill the task subsections according to section 11.
Expand Down Expand Up @@ -367,7 +367,7 @@ A decision is simply a task held for the captain: create the task with `bin/fm-t
When a main-side thread such as a pending captain decision or relay reminder is worth durable tracking, file it as its own work item and hold it through that wrapper.
Captain calls discovered by investigations or visual reviews follow `captain-hold-lifecycle`, which owns their completion gate and recorded-answer rules.
When the automatic transition gate applies, dispatch and completion move the item themselves - `bin/fm-spawn.sh` and `bin/fm-teardown.sh` own those transitions and refuse rather than report success without them - so what remains yours is filing the item before dispatch, recording decisions, and keeping notes current; `docs/configuration.md` owns gate applicability and the manual-backend exception.
Re-evaluate queued work after every teardown and heartbeat, dispatching items only when dependencies and time gates have cleared.
Re-evaluate queued work after every teardown and heartbeat, and also after a recorded PR-ready handoff when `config/project-capacity` caps that project, dispatching items only when dependencies, time gates, and project capacity have cleared.

`.tasks.toml`, `docs/configuration.md`, and current `tasks-axi --help` own the backlog schema, compatibility, retention, and routine command syntax.
Use compatible `tasks-axi` when the configured backend selects it, always through `bin/fm-tasks-axi.sh` so the call reaches this home's backlog from any directory, and the documented manual path otherwise; keep only the configured recent Done entries.
Expand Down
18 changes: 15 additions & 3 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -773,14 +773,26 @@ fm_backend_herdr_projection_workspace_label() { # <task-id> <projection-id>
printf '└ %s · p:%s' "$(fm_backend_herdr_projection_concise_task_label "$1")" "$2"
}

# fm_backend_herdr_presentation_session_lock_path: one machine-private lock
# fm_backend_herdr_presentation_session_lock_path: one account-private lock
# path per live named Herdr session/socket, shared across every Firstmate home
# that uses that session.
# of this OS account that uses that session.
# The path is never under any one home's state/ and secondmates never write the
# primary home. Returns non-zero when the named session's socket cannot be
# resolved unambiguously.
# The namespace directory is suffixed with this account's uid, so another OS
# account on the same host can never create it first by ordinary use and lock
# this account out; a deliberately pre-created name still fails the ownership
# and mode checks below and is refused, never adopted, chowned, or removed.
# The uid rather than $XDG_RUNTIME_DIR names it because that variable can differ
# or be absent between login contexts of one account, which would split one
# session's lock across processes.
fm_backend_herdr_presentation_lock_namespace() {
printf '%s' '/tmp/firstmate-herdr-presentation'
local uid
uid=$(id -u 2>/dev/null) || return 1
case "$uid" in
''|*[!0-9]*) return 1 ;;
esac
printf '/tmp/firstmate-herdr-presentation-%s' "$uid"
}

fm_backend_herdr_presentation_lock_namespace_mode() {
Expand Down
25 changes: 15 additions & 10 deletions bin/fm-pending-reply-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
# to a secondmate, this library records a durable parent-owned pending-reply
# expectation BEFORE delivery, embeds a privacy-safe correlation id in the
# outbound message, and later resolves that expectation only from a correlated
# parent status line or status-pointed document - never from transport success,
# chat content, or unrelated status activity.
# line in the asked task's own parent status log, or a document it points to -
# never from transport success, chat content, unrelated status activity, or
# another task's line that echoes or quotes the token.
#
# Safety property (captain direction 2026-07-22): a secondmate agent may ignore
# the marker and answer only in its visible conversation. The parent must notice
Expand Down Expand Up @@ -194,14 +195,12 @@ fm_pending_reply_extract_corr() { # <text>
printf '%s' "$text" | grep -oE "$FM_PENDING_REPLY_CORR_RE" 2>/dev/null | head -1 | cut -d= -f2- | tr 'A-F' 'a-f' || true
}

# 0 if <text> carries the exact correlation token for <corr_id>.
# 0 if <text> carries the exact correlation token for <corr_id>, as a whole
# word: xcorr=<id> or corr=<id>ff is a different token, not this one.
fm_pending_reply_text_has_corr() { # <text> <corr_id>
local text=$1 corr=$2 token
token=$(fm_pending_reply_corr_token "$corr")
case "$text" in
*"$token"*) return 0 ;;
esac
return 1
local text=$1 corr=$2 re
re="(^|[^[:alnum:]_])$(fm_pending_reply_corr_token "$corr")([^[:alnum:]_]|\$)"
[[ $text =~ $re ]]
}

# Sanitize a short request summary: single line, bounded, no control chars.
Expand Down Expand Up @@ -689,7 +688,13 @@ _fm_pending_reply_try_resolve_locked() { # <state-dir> <corr_id> [status-file-o
case "$delivery_state" in attempted|confirmed) ;; *) return 1 ;; esac
unconfirmed=1
fi
status_file=${status_override:-$(fm_pending_reply_get "$rec" parent_status)}
status_file=$(fm_pending_reply_get "$rec" parent_status)
# Only the asked task's own status log answers its request: another mate's
# line echoing or quoting this corr= token must leave the request open.
if [ -n "$status_override" ]; then
[ "$status_override" -ef "$status_file" ] || return 1
status_file=$status_override
fi
if [ -z "$status_override" ] && [ "$unconfirmed" = 0 ]; then
signature=$(fm_pending_reply_file_signature "$status_file")
previous=$(fm_pending_reply_get "$rec" parent_status_scan_signature)
Expand Down
2 changes: 2 additions & 0 deletions bin/fm-pr-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
# draft state does not refuse, matching how the head read below is optional.
# bin/fm-pr-merge.sh records through this script with FM_PR_CHECK_MERGE=1 and
# skips this refusal, because its own merge-time draft refusal is authoritative.
# The recorded pr= also frees the task's place in a declared project capacity
# (bin/fm-project-capacity-lib.sh).
#
# --team-review records whether the task's already-recorded PR is out with the
# project's human reviewers, as team_review=<in-review|done> bound to that URL by
Expand Down
13 changes: 8 additions & 5 deletions bin/fm-procevent-lavish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
# It is read-only over the capture: it does not arm, poll, or change
# what Lavish delivered. The freeform message (tag=message) is its
# own labeled field, printed first and distinct from per-element
# annotations; it is labeled SESSION-ENDING MESSAGE only when the
# session ended. Declared and presented item counts,
# annotations; it is labeled SESSION-ENDING MESSAGE, and counted as
# session_ending_message_count, only when the session ended, and is
# otherwise CAPTAIN MESSAGE and captain_message_count. Declared and
# presented item counts,
# plus a completeness verdict, follow before all annotations so a
# partial read is obvious. Each annotation retains its element uid,
# selector, tag, and text. A non-choice freeform comment (`prompt`)
Expand Down Expand Up @@ -783,9 +785,9 @@ cmd_read() {
return if !@lines || (@lines == 1 && $lines[0] eq "");
print "| $_\n" for @lines;
}
my $ended = $session_ended =~ /^(?:true|True|TRUE)$/;
if (@messages) {
my $message_label = $session_ended =~ /^(?:true|True|TRUE)$/
? "SESSION-ENDING MESSAGE" : "CAPTAIN MESSAGE";
my $message_label = $ended ? "SESSION-ENDING MESSAGE" : "CAPTAIN MESSAGE";
print "$message_label\n";
for my $i (0 .. $#messages) {
print "$message_label PART ", ($i + 1), " of ", scalar(@messages), "\n" if @messages > 1;
Expand All @@ -806,7 +808,8 @@ cmd_read() {
print "lifecycle: $lifecycle\n";
print "session_ended: ", (length $session_ended ? $session_ended : "(unset)"), "\n";
print "annotation_count: ", scalar(@annotations), "\n";
print "session_ending_message_count: ", scalar(@messages), "\n";
my $message_count_key = $ended ? "session_ending_message_count" : "captain_message_count";
print "$message_count_key: ", scalar(@messages), "\n";
print "\n";
if (@annotations) {
print "ANNOTATIONS\n";
Expand Down
Loading