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
2 changes: 1 addition & 1 deletion .agents/skills/harness-adapters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ When changing any primary turn-end hook, validate the real harness behavior in a

## Primary pre-arm (PreToolUse) seatbelt

Every verified primary harness also has a wired PreToolUse-equivalent hook that denies a watcher-arm anti-pattern (shell `&`, truncating pipe, bundling, broad `pkill -f fm-watch`) before it runs.
Every verified primary harness also has a wired PreToolUse-equivalent hook that denies supervision-arm anti-patterns (shell `&`, truncating pipe, bundling, broad `pkill -f fm-watch`) before they run.
`claude` and `codex` block directly through PreToolUse hooks; `grok` blocks the same way but requires every `$VAR` reference in its hook `command` string to carry an inline `:-default` or it fails to launch the hook entirely.
`opencode` and `pi` block by throwing from `tool.execute.before` / returning `{block: true}` from `tool_call`.
The exact hook files, commands, output-shaping quirks (Claude Code only honors the deny when stdout is empty), and validation transcripts are owned by `docs/arm-pretool-check.md`.
Expand Down
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ config/backend runtime session-provider backend override for new tasks; LOCAL,
config/cmux-socket-password optional cmux control-socket password; LOCAL, gitignored; read fresh on every cmux CLI call and passed through without ever overriding an operator's own ambient CMUX_SOCKET_PASSWORD when absent (docs/cmux-backend.md "Setup")
config/wedge-alarm optional away-mode wedge-alarm active-alert directives; LOCAL, gitignored; absent means auto (macOS Notification Center when available); see docs/wedge-alarm.md
config/x-mode.env generated X-mode watcher cadence; LOCAL, gitignored; source before arming watcher when present
config/telegram.env optional direct Telegram receive credentials; LOCAL, gitignored; when present, the locked session-start digest emits the `bin/fm-tg-recv-arm.sh` tracked-background arm step
config/fm-tg-recv.sh optional local direct Telegram receiver implementation; LOCAL, gitignored; `bin/fm-tg-recv-arm.sh` owns only the tracked arm/attach wrapper
config/bridge-vessel Bridge inbox vessel name; LOCAL, gitignored; used only when FM_BRIDGE_VESSEL is unset, and absent (with FM_BRIDGE_VESSEL also unset) disables Bridge inbox scans (docs/configuration.md "Bridge inbox check (FM_BRIDGE_*)")
data/ personal fleet records; LOCAL, gitignored as a whole
backlog.md task queue, dependencies, history
Expand Down Expand Up @@ -139,9 +141,9 @@ A lock-refused session must not spawn, steer, merge, drain the wake queue, repai
A file that does not exist prints an explicit `ABSENT` marker, never confused with an empty-but-present file: absence is meaningful (`captain.md` absent means use the firstmate repo's built-in defaults, `projects.md` absent means rebuild it from the clones under `projects/`, etc.).
5. **Fleet-state digest** - the compact backlog listing owned by `bin/fm-session-start.sh`; every `state/<id>.meta`; a bounded tail of each task's `state/<id>.status` (labeled as wake-EVENT history, not current state, with the full log path printed for a deeper read); the `state/.afk` flag; and one cheap alive/dead read of each task's recorded backend endpoint.
That liveness line is a fast presence check only, not a full state read - when you need a crew's actual current state (a run-step, not just "is the pane there"), read it with `bin/fm-crew-state.sh <id>` as before; the digest deliberately skips that deeper, slower read for every task so it stays fast and bounded.
6. **Supervision operating instructions and next step** - after the wake queue and before context, the digest emits exactly one operating block for the detected primary harness.
6. **Supervision operating instructions and next step** - after the wake queue and before context, the digest emits exactly one operating block for the detected primary harness plus the optional direct Telegram receiver arm step when `config/telegram.env` exists.
The closing reminder points back to that emitted block and preserves only the lock, afk, X-mode, and read-once reminders.
The script itself never starts supervision; the emitted harness protocol owns the exact wait or wake mechanism.
The script itself never starts long-lived polls; the emitted harness protocol and `bin/fm-tg-recv-arm.sh` own the exact tracked-background wait or wake mechanisms.

Bootstrap detects first, asks for consent, and installs only after the captain approves in the current session.
Do not dispatch until the required tools are present and GitHub authentication is good.
Expand Down Expand Up @@ -322,6 +324,7 @@ Fleet supervision is an always-loaded operational contract; `docs/architecture.m
Whenever work is under way, keep exactly one live supervision cycle using the emitted protocol for this primary harness.
X mode may require that same live cycle with no fleet work.
Do not substitute another harness's wait shape, use shell `&`, or create a second cycle when a healthy one already exists.
When the session-start digest reports direct Telegram receive as active, keep `bin/fm-tg-recv-arm.sh` armed as its own separate tracked background task; it starts or attaches to the receiver for this home.
After every actionable wake, resume the emitted protocol as the final action before ending the turn.
No turn ends blind while work is under way, including turns described as holding or waiting.

Expand Down
7 changes: 4 additions & 3 deletions bin/fm-arm-command-policy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// The tokenizer and command-position analysis (Lexer, splitProgram,
// commandPosition) are exported so the sibling cd-guard policy
// (bin/fm-cd-command-policy.mjs) reuses the same proven parser instead of
// duplicating shell lexing; see docs/cd-guard.md. The watcher-arm decision
// duplicating shell lexing; see docs/cd-guard.md. The supervision-arm decision
// procedure below stays private to this file. The CLI entry point at the bottom
// runs only when this module is invoked directly, never on import.

Expand Down Expand Up @@ -44,7 +44,7 @@ function parseArguments(argv) {
}

function rawMentionsProtected(command) {
return /(?:^|[/\s'"`(])fm-watch(?:-(?:arm|checkpoint))?\.sh\b/.test(normalizeLineContinuations(command));
return /(?:^|[/\s'"`(])(?:fm-watch(?:-(?:arm|checkpoint))?|fm-tg-recv-arm)\.sh\b/.test(normalizeLineContinuations(command));
}

function rawMentionsBroadKill(command) {
Expand Down Expand Up @@ -597,6 +597,7 @@ export function commandPosition(tokens) {

const PROTECTED_SCRIPTS = [
{ relative: "bin/fm-watch-arm.sh", kind: "arm" },
{ relative: "bin/fm-tg-recv-arm.sh", kind: "arm" },
{ relative: "bin/fm-watch-checkpoint.sh", kind: "checkpoint" },
{ relative: "bin/fm-watch.sh", kind: "watch" },
];
Expand All @@ -611,7 +612,7 @@ function protectedIdentity(value, root) {

function hasUnclassifiableProtectedExpansion(word, root) {
if (!word?.unquotedExpansion || protectedIdentity(word.value, root)) return false;
return /(?:^|\/)fm-watch/.test(word.value);
return /(?:^|\/)(?:fm-watch|fm-tg-recv-arm)/.test(word.value);
}

function shellInvocation(position) {
Expand Down
25 changes: 13 additions & 12 deletions bin/fm-arm-pretool-check.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Stable PreToolUse transport for the watcher-arm command policy.
# Stable PreToolUse transport for the supervision-arm command policy.
#
# A firstmate primary must arm the watcher or run a Codex checkpoint as a
# standalone verified harness call.
Expand Down Expand Up @@ -104,23 +104,23 @@ fi
[ -n "$CMD" ] || exit 0

# Strict-superset prefilter (transport only; owns zero classification semantics).
# Every protected watcher execution and every broad watcher kill resolves to the
# fm-watch byte sequence AFTER the classifier's byte normalization, so a command
# that cannot contain fm-watch even after that normalization can never be a
# deniable watcher command and is fast-allowed without the Node policy owner.
# Every protected arm/watch execution and every broad watcher kill resolves to a
# protected byte sequence AFTER the classifier's byte normalization, so a command
# that cannot contain one even after that normalization can never be a deniable
# command and is fast-allowed without the Node policy owner.
# We mirror the classifier's cheapest byte transforms here (drop line-
# continuation and escape backslashes, quotes, and newlines) so obfuscated
# protected paths such as fm-watc\<newline>h-arm.sh or fm-"watch"-arm.sh still
# delegate. Stripping only these non-alphanumeric bytes can never destroy an
# existing fm-watch run.
# protected paths such as fm-watc\<newline>h-arm.sh, fm-"watch"-arm.sh, or
# fm-tg-recv-"arm".sh still delegate. Stripping only these non-alphanumeric
# bytes can never destroy an existing protected run.
#
# The fast path may allow ONLY when BOTH hold: (a) the stripped/normalized text
# lacks the fm-watch watcher substring, AND (b) the raw command carries no
# lacks every protected substring, AND (b) the raw command carries no
# quoting-decoder marker - a $ immediately followed by a single quote (ANSI-C
# $'...') or a double quote (bash locale $"..."), both of which the classifier
# decodes and can therefore reconstruct fm-watch from bytes this cheap byte
# strip cannot. This marker set is COUPLED to the classifier's decoder set in
# bin/fm-arm-command-policy.mjs: adding any new quote/expansion form the
# decodes and can therefore reconstruct protected paths from bytes this cheap
# byte strip cannot. This marker set is COUPLED to the classifier's decoder set
# in bin/fm-arm-command-policy.mjs: adding any new quote/expansion form the
# classifier decodes REQUIRES extending this marker set in the same change, or
# the prefilter stops being a strict superset. Otherwise the command always
# delegates to the classifier - the single owner of every decision. Any deeper
Expand All @@ -137,6 +137,7 @@ case "$CMD" in
*)
case "$PREFILTER" in
*fm-watch*) ;;
*fm-tg-recv-arm*) ;;
*) exit 0 ;;
esac
;;
Expand Down
36 changes: 27 additions & 9 deletions bin/fm-session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@
# every state/*.meta, a bounded state/*.status tail,
# state/.afk, and a cheap per-task endpoint-liveness read:
# read-only, always runs.
# 6. closing reminder - prints the context-specific watcher next step; this
# script points back to the emitted harness supervision
# block and deliberately never arms the watcher itself.
# 6. closing reminder - prints the context-specific watcher and optional
# Telegram receiver next steps; this script points back
# to the emitted harness supervision block and deliberately
# never runs long-lived polls itself.
#
# On a Pi primary, the supervision-block step also checks whether Pi's two
# tracked primary extensions are loaded and prints a PI_WATCH_EXTENSION
Expand Down Expand Up @@ -299,7 +300,22 @@ else
fi
fi

# --- 4. supervision operating instructions ----------------------------------
# --- 4. direct Telegram receiver ---------------------------------------------
TELEGRAM_PRESENT=0
[ -f "$CONFIG/telegram.env" ] && TELEGRAM_PRESENT=1

subsection "TELEGRAM RECEIVER"
if [ "$TELEGRAM_PRESENT" -eq 0 ]; then
printf '%s\n' 'inactive (config/telegram.env absent)'
elif [ "$READ_ONLY" -eq 1 ]; then
printf '%s\n' 'skipped (read-only session) - the session holding the lock owns Telegram receiver arming.'
elif [ ! -x "$CONFIG/fm-tg-recv.sh" ]; then
printf '%s\n' 'TELEGRAM_RECEIVER: config/telegram.env exists but config/fm-tg-recv.sh is missing or not executable; direct Telegram receive is not armed'
else
printf '%s\n' "TELEGRAM_RECEIVER: active - run bin/fm-tg-recv-arm.sh as its own tracked background task, never shell &; it starts or attaches to this home's receiver"
fi

# --- 5. supervision operating instructions ----------------------------------
AFK_PRESENT=0
[ -e "$STATE/.afk" ] && AFK_PRESENT=1
X_MODE_PRESENT=0
Expand All @@ -324,15 +340,15 @@ fi
--afk "$AFK_PRESENT" \
--x-mode "$X_MODE_PRESENT"

# --- 4. context digest -----------------------------------------------------
# --- 6. context digest -----------------------------------------------------
section "CONTEXT"
print_file_or_absent "$DATA/projects.md" "data/projects.md"
print_file_or_absent "$DATA/secondmates.md" "data/secondmates.md"
print_file_or_absent "$DATA/captain.md" "data/captain.md"
print_file_or_absent "$DATA/captain-shared.md" "data/captain-shared.md (shared, main-authoritative, read-only in secondmate homes)"
print_file_or_absent "$DATA/learnings.md" "data/learnings.md"

# --- 5. fleet-state digest ---------------------------------------------
# --- 7. fleet-state digest ---------------------------------------------
section "FLEET STATE"
print_backlog_compact "$DATA/backlog.md" "data/backlog.md"

Expand Down Expand Up @@ -386,7 +402,7 @@ else
printf 'absent\n'
fi

# --- 6. closing reminder -----------------------------------------------
# --- 8. closing reminder -----------------------------------------------
section "NEXT STEP"
if [ "$READ_ONLY" -eq 1 ]; then
cat <<'EOF'
Expand All @@ -406,13 +422,15 @@ elif [ -f "$CONFIG/x-mode.env" ]; then
cat <<EOF
Follow the supervision operating instructions block above for harness '$PRIMARY_HARNESS'.
X mode is active, so the emitted block's cadence instruction applies.
This script never starts supervision itself.
If the Telegram receiver section is active, keep that separate background task armed too.
This script never starts long-lived polls itself.

EOF
else
cat <<EOF
Follow the supervision operating instructions block above for harness '$PRIMARY_HARNESS'.
This script never starts supervision itself.
If the Telegram receiver section is active, keep that separate background task armed too.
This script never starts long-lived polls itself.

EOF
fi
Expand Down
Loading