From 216b896af80688f3f3f4521884cfa0df20cc160f Mon Sep 17 00:00:00 2001 From: kunchenguid Date: Mon, 29 Jun 2026 16:14:07 -0700 Subject: [PATCH 1/5] feat(harness): split secondmate harness and inherit primary config into secondmate homes Add config/secondmate-harness so secondmates can run on a different adapter than crewmates. fm-harness.sh gains a `secondmate` mode resolving the chain config/secondmate-harness -> config/crew-harness -> own; `crew` mode is unchanged. fm-spawn resolves a --secondmate launch through that mode (durable: every respawn re-resolves), while an explicit per-spawn harness arg still wins and the unverified-adapter guard still holds. Add a generic, extensible inheritable-config mechanism (fm-config-inherit-lib.sh) that pushes the primary's declared LOCAL config into each secondmate home's config/ at secondmate spawn and on the bootstrap secondmate sweep. Exactly one item is wired today: config/crew-harness, so a secondmate's own crewmates use the primary's setting. Primary-authoritative (re-pushed every convergence, mirrors absence); config/secondmate-harness is deliberately not inherited since secondmates never spawn secondmates. config/ is gitignored, so this is a copy separate from the tracked-files fast-forward. Update AGENTS.md (layout, bootstrap, harness, spawn), the harness-adapters skill, docs/scripts.md, and .gitignore. New tests cover secondmate resolution and fallback, spawn/respawn honoring config/secondmate-harness, config propagation on spawn and sweep, the unverified-adapter guard, and backward compatibility. --- .agents/skills/harness-adapters/SKILL.md | 11 +- .gitignore | 1 + AGENTS.md | 22 +- bin/fm-bootstrap.sh | 13 + bin/fm-config-inherit-lib.sh | 59 +++ bin/fm-harness.sh | 40 ++- bin/fm-spawn.sh | 44 ++- docs/scripts.md | 3 +- tests/fm-secondmate-harness.test.sh | 437 +++++++++++++++++++++++ 9 files changed, 609 insertions(+), 21 deletions(-) create mode 100644 bin/fm-config-inherit-lib.sh create mode 100755 tests/fm-secondmate-harness.test.sh diff --git a/.agents/skills/harness-adapters/SKILL.md b/.agents/skills/harness-adapters/SKILL.md index 1b8ffadd98b..69b5d3de38d 100644 --- a/.agents/skills/harness-adapters/SKILL.md +++ b/.agents/skills/harness-adapters/SKILL.md @@ -12,18 +12,25 @@ Crewmates default to the same harness firstmate is running on unless `config/cre The captain may override that file at bootstrap or later; a per-task instruction such as "run this one on codex" overrides it for that dispatch only. `default` means mirror firstmate's own harness. +Secondmates have their own harness knob, so a secondmate can run on a different adapter than crewmates. +`config/secondmate-harness` is the harness the primary uses to launch SECONDMATE agents, resolved through the fallback chain `config/secondmate-harness` -> `config/crew-harness` -> firstmate's own. +An absent or `default` `config/secondmate-harness` therefore behaves exactly as the crew harness did before this knob existed (secondmates launched on the crew harness); setting it splits the two. +`config/crew-harness` is inherited by secondmate homes (the primary pushes it down so a secondmate's own crewmates use the primary's value), while `config/secondmate-harness` is the primary's own setting and is never inherited - secondmates do not spawn secondmates. + Each adapter splits into mechanics and knowledge. The mechanics, including launch command, autonomy flag, and turn-end hook, live in `bin/fm-spawn.sh`. The supervision knowledge lives here: busy signature, exit command, interrupt, dialogs, resume behavior, skill invocation, and quirks. Never dispatch a crewmate or secondmate on an unverified adapter. -If `config/crew-harness` names an unverified adapter, tell the captain and fall back to firstmate's own harness until that adapter is verified. +If `config/crew-harness` or `config/secondmate-harness` names an unverified adapter, tell the captain and fall back to firstmate's own harness until that adapter is verified. If the captain asks for a new harness, propose verifying it first: spawn a trivial supervised task using `fm-spawn`'s raw-launch-command escape hatch, confirm every fact empirically, then record the mechanics in `fm-spawn`, the busy signature in `fm-watch.sh` and `fm-tmux-lib.sh` defaults, any needed `FM_COMPOSER_IDLE_RE` empty-composer override, and the verified knowledge here. ## Detection `bin/fm-harness.sh` prints firstmate's own harness, using verified env markers first and then process ancestry. -`bin/fm-harness.sh crew` resolves the effective crewmate harness from `config/crew-harness`. +`bin/fm-harness.sh crew` resolves the effective crewmate harness from `config/crew-harness` (absent or `default` -> own). +`bin/fm-harness.sh secondmate` resolves the secondmate-launch harness through the chain `config/secondmate-harness` -> `config/crew-harness` -> own, so an unset `config/secondmate-harness` matches the crew harness. +`bin/fm-spawn.sh` uses `crew` mode for a crewmate/scout launch and `secondmate` mode for a `--secondmate` launch, re-resolving on every spawn so the split is durable across respawns; an explicit per-spawn harness arg overrides either. On `unknown`, ask the captain instead of guessing. A captain override always beats detection. When verifying a new adapter, record its env marker and command name in `bin/fm-harness.sh`. diff --git a/.gitignore b/.gitignore index c6095e8b79f..341c8ce77ef 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ data/ .DS_Store .env config/crew-harness +config/secondmate-harness config/x-mode.env diff --git a/AGENTS.md b/AGENTS.md index 581671169fe..08d860b0746 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,7 +71,8 @@ README.md public overview and development notes .claude/skills symlink to .agents/skills for claude compatibility bin/ helper scripts, committed; read each script's header before first use .env optional X-mode pairing token; LOCAL, gitignored; presence-gates section 14 -config/crew-harness crewmate harness override; LOCAL, gitignored; absent or "default" = same as firstmate +config/crew-harness crewmate harness override; LOCAL, gitignored; absent or "default" = same as firstmate. Inherited: the primary pushes this into every secondmate home's config/ (section 4), so a secondmate's own crewmates use the primary's value +config/secondmate-harness harness the PRIMARY uses to launch SECONDMATE agents; LOCAL, gitignored; absent or "default" falls back to config/crew-harness then firstmate's own (section 4). The primary's own setting; NOT inherited into secondmate homes (secondmates do not spawn secondmates) config/x-mode.env generated X-mode watcher cadence; LOCAL, gitignored; source before arming watcher when present data/ personal fleet records; LOCAL, gitignored as a whole backlog.md task queue, dependencies, history @@ -115,6 +116,8 @@ Set `FM_FLEET_PRUNE=0` to temporarily disable that branch pruning. Bootstrap also sweeps every live secondmate home, fast-forwarding each one's worktree to firstmate's own current default-branch commit so the fleet stays converged on whatever version firstmate is on. This is a purely local fast-forward (every secondmate home is a worktree of this same repo, sharing one object store), never a fetch from origin and never a surprise pull: the version followed is simply whatever the primary is currently on, which only the captain changes deliberately via `git pull` or `/updatefirstmate`. A tracked-files fast-forward never touches the gitignored operational dirs, so a secondmate's backlog, projects, and in-flight work are never disturbed; a dirty, diverged, or in-flight home is skipped untouched. +The same sweep also propagates the primary's declared inheritable config (`config/crew-harness` today; section 4) into each live secondmate home's `config/`, so every secondmate's own crewmates stay on the primary's settings. +Because `config/` is gitignored this is a separate, primary-authoritative copy independent of the tracked-files fast-forward: it re-converges every live home whether or not its tracked files advanced, and it touches only the declared inheritable items (never `config/secondmate-harness`). The sweep reports the `NUDGE_SECONDMATES:` line below only when a running secondmate actually advanced with an instruction change, so firstmate knows which ones to live-converge. Silence means all good: say nothing and move on. Otherwise it prints one line per problem or capability fact; handle each: @@ -158,10 +161,20 @@ The recorded harness is used for every dispatch until changed; a per-task instru Resolve `default` with `bin/fm-harness.sh`; resolve the active crewmate harness with `bin/fm-harness.sh crew`. Verified adapter names are `claude`, `codex`, `opencode`, `pi`, and `grok`. +Secondmates can run on a different harness than crewmates. +`config/secondmate-harness` (a single adapter name; local, gitignored) is the harness the primary uses to launch SECONDMATE agents; resolve it with `bin/fm-harness.sh secondmate`, which follows the fallback chain `config/secondmate-harness` -> `config/crew-harness` -> your own harness. +So an absent or `default` `config/secondmate-harness` behaves exactly as before this knob existed - secondmates launch on the crew harness - and setting it splits the two: e.g. primary `config/crew-harness=codex` with `config/secondmate-harness=claude` runs the secondmate AGENTS on claude while all crewmates (the primary's and the secondmates' own) run on codex. +`bin/fm-spawn.sh` resolves a `--secondmate` launch through `secondmate` mode and a crewmate/scout launch through `crew` mode; an explicit per-spawn harness arg still overrides either kind. +The split is durable: every secondmate respawn (recovery, `/updatefirstmate`, restart) re-resolves from `config/secondmate-harness`, so it survives restarts without being recorded per-task. + +`config/crew-harness` is inherited; `config/secondmate-harness` is not. +The primary pushes its declared inheritable config (`config/crew-harness` today) down into each secondmate home's `config/` - at secondmate spawn and on the bootstrap secondmate sweep (section 3) - so a secondmate's OWN crewmates use the primary's settings (primary `config/crew-harness=codex` makes a secondmate's crewmates spawn on codex too). +The mechanism is generic over a single declared list (`fm-config-inherit-lib.sh`), primary-authoritative (re-pushed every convergence, mirroring absence), and easy to extend; `config/secondmate-harness` is deliberately excluded because secondmates never spawn secondmates. + Each adapter splits into mechanics and knowledge. The mechanics (launch command, autonomy flag, turn-end hook) live in `bin/fm-spawn.sh`; the knowledge you need while supervising (busy signature, exit, interrupt, dialogs, quirks, skill invocation, resume) lives in the agent-only `harness-adapters` skill. -**Never dispatch a crewmate on an unverified adapter.** -If `config/crew-harness` names an unverified one, tell the captain and fall back to your own harness until it is verified. +**Never dispatch a crewmate or secondmate on an unverified adapter.** +If `config/crew-harness` or `config/secondmate-harness` names an unverified one, tell the captain and fall back to your own harness until it is verified. If the captain asks for a new harness, load `harness-adapters`, verify it empirically with a trivial supervised task, then commit the script and knowledge changes. Load `harness-adapters` before any spawn, recovery, trust-dialog handling, harness-specific skill invocation, interrupt, exit, resume, or adapter verification. @@ -346,7 +359,7 @@ bin/fm-spawn.sh =projects/ =projects/ [--scout] # batc Dispatch several tasks in one call by passing `id=repo` pairs instead of a single ` `; each pair is spawned through the same single-task path, a shared `--scout` applies to all, and the looping happens inside the script so you never hand-write a multi-task shell loop. If one pair fails, the rest still run and the batch exits non-zero. -The script resolves the harness (`fm-harness.sh crew`), owns the verified launch templates, resolves the project's delivery mode (`fm-project-mode.sh`) for ship/scout tasks, and records `harness=`, `kind=`, `mode=`, and `yolo=` in the task's meta; a non-flag third argument containing whitespace is treated as a raw launch command (only for verifying new adapters). +The script resolves the harness (`fm-harness.sh crew` for crewmate/scout tasks, `fm-harness.sh secondmate` for `kind=secondmate`; section 4), owns the verified launch templates, resolves the project's delivery mode (`fm-project-mode.sh`) for ship/scout tasks, and records `harness=`, `kind=`, `mode=`, and `yolo=` in the task's meta; a non-flag third argument containing whitespace is treated as a raw launch command (only for verifying new adapters). For `kind=secondmate`, the same script launches in the registered or explicit firstmate home instead of running `treehouse get` for a project, records `home=` and `projects=`, and uses the charter brief as the launch prompt. For ship and scout tasks, the script creates the window (in your current tmux session, or a dedicated `firstmate` session when you are outside tmux), runs `treehouse get`, waits for the worktree subshell, asserts the resolved worktree is a genuine isolated worktree distinct from the primary checkout (aborting the spawn otherwise, to prevent the worktree tangle of section 8), installs the turn-end hook, records `state/.meta`, and launches the agent with the brief. @@ -355,6 +368,7 @@ For `kind=secondmate`, the script creates the same kind of window but starts dir Before launching a secondmate, the script fast-forwards its home worktree to firstmate's own current default-branch commit, so a freshly spawned or recovery-respawned secondmate always starts on firstmate's current version. This is a purely local fast-forward of tracked files - never a fetch from origin, and never touching the gitignored operational dirs - so the secondmate's backlog, projects, and any prior in-flight work are untouched; a dirty, diverged, or in-flight home is left as-is and launches unchanged. If that pre-launch fast-forward is skipped, `fm-spawn.sh` prints a concise warning to stderr and still launches the secondmate from its unchanged checkout. +The spawn also propagates the primary's declared inheritable config (`config/crew-harness` today; section 4) into the secondmate home's `config/`, so the secondmate's own crewmates inherit the primary's settings; this is a separate gitignored-file copy from the tracked-files fast-forward and a primary with no inheritable config set is a no-op. No nudge is needed at spawn because the agent reads `AGENTS.md` fresh on launch. Project worktrees start at detached HEAD on a clean default branch; ship briefs tell the crewmate to create its branch, while scout briefs keep the worktree scratch. After spawning, peek the pane to confirm the crewmate is processing the brief and handle any trust dialog with `harness-adapters`. diff --git a/bin/fm-bootstrap.sh b/bin/fm-bootstrap.sh index d5c1e469608..b8aac264132 100755 --- a/bin/fm-bootstrap.sh +++ b/bin/fm-bootstrap.sh @@ -50,6 +50,8 @@ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}" . "$SCRIPT_DIR/fm-tangle-lib.sh" # shellcheck source=bin/fm-ff-lib.sh . "$SCRIPT_DIR/fm-ff-lib.sh" +# shellcheck source=bin/fm-config-inherit-lib.sh +. "$SCRIPT_DIR/fm-config-inherit-lib.sh" # shellcheck source=bin/fm-x-lib.sh . "$SCRIPT_DIR/fm-x-lib.sh" @@ -125,6 +127,17 @@ secondmate_sync() { esac done < "$tmp" rm -f "$tmp" + # Inheritable-config propagation: push the primary's declared LOCAL config + # (config/crew-harness today) into every VALIDATED live secondmate home swept + # above (FF_SEEN_HOMES is exactly that set). config/ is gitignored, so this is a + # separate copy from the tracked-files fast-forward; primary-authoritative, so + # it runs whether or not the home's tracked files advanced, keeping the fleet + # converged on the primary. Silent (fm-config-inherit-lib.sh); a primary with no + # inheritable config set is a complete no-op. + local home + for home in $FF_SEEN_HOMES; do + propagate_inheritable_config "$CONFIG" "$home/config" || true + done [ -n "$FF_NUDGE_WINDOWS" ] && echo "NUDGE_SECONDMATES:$FF_NUDGE_WINDOWS" return 0 } diff --git a/bin/fm-config-inherit-lib.sh b/bin/fm-config-inherit-lib.sh new file mode 100644 index 00000000000..732e3ebe2b5 --- /dev/null +++ b/bin/fm-config-inherit-lib.sh @@ -0,0 +1,59 @@ +# shellcheck shell=bash +# Inheritable-config propagation: the PRIMARY firstmate pushes a declared, +# extensible set of LOCAL (gitignored) config items down into each secondmate +# home's config/, so a secondmate's OWN crewmates inherit the primary's settings +# (e.g. primary config/crew-harness=codex makes a secondmate's crewmates spawn on +# codex too). +# +# Usage: . bin/fm-config-inherit-lib.sh (no FM_* setup required) +# +# Why this is separate from the tracked-files fast-forward (fm-ff-lib.sh): config/ +# is gitignored, so a tracked-files fast-forward never carries these items. This +# is an explicit copy run at the two convergence points the primary owns - a +# secondmate spawn (bin/fm-spawn.sh) and the bootstrap secondmate sweep +# (bin/fm-bootstrap.sh). It is PRIMARY-AUTHORITATIVE: the primary's value wins and +# is re-pushed on every convergence, so the fleet stays converged on the primary; +# an item the primary does not set is mirrored as absence downstream. +# +# Extensible by design: FM_INHERITABLE_CONFIG is the single declared list of +# config-dir-relative items the primary propagates. Add an item there and every +# convergence point inherits it - no other change needed. Only crew-harness is +# wired today. config/secondmate-harness is deliberately NOT in the list: it is +# the primary's own setting for launching secondmates, and a secondmate never +# spawns secondmates, so it must not flow downstream. + +# The declared inheritable set (space-separated, config-dir-relative item paths). +# Extend here to inherit more of the primary's local config; override via the +# environment only in tests. Items must not contain whitespace. +FM_INHERITABLE_CONFIG="${FM_INHERITABLE_CONFIG:-crew-harness}" + +# propagate_inheritable_config +# Copy each declared inheritable item from the primary's config dir (src) into a +# secondmate home's config dir (dest). SILENT on success - callers parse stdout, +# so this writes nothing there. A source item that is present is copied only when +# its content differs (idempotent: a re-run never churns mtimes). A source item +# that is absent is mirrored as a missing destination item, so clearing the +# primary's value clears it downstream too (primary-authoritative). The +# destination dir is created lazily, only when there is actually something to +# write, so a primary with no inheritable config set is a complete no-op (it +# leaves the secondmate home exactly as it was - the backward-compatible path). +# Returns non-zero only when the destination cannot be created or written. +propagate_inheritable_config() { + local src_config=$1 dest_config=$2 item src dest + [ -n "$src_config" ] || return 1 + [ -n "$dest_config" ] || return 1 + for item in $FM_INHERITABLE_CONFIG; do + src="$src_config/$item" + dest="$dest_config/$item" + if [ -f "$src" ]; then + if [ ! -f "$dest" ] || ! cmp -s "$src" "$dest"; then + mkdir -p "$dest_config" 2>/dev/null || return 1 + cp "$src" "$dest" 2>/dev/null || return 1 + fi + elif [ -e "$dest" ]; then + # Primary has no value for this item: mirror the absence downstream. + rm -f "$dest" 2>/dev/null || true + fi + done + return 0 +} diff --git a/bin/fm-harness.sh b/bin/fm-harness.sh index 01236ba75f1..067ebbd9752 100755 --- a/bin/fm-harness.sh +++ b/bin/fm-harness.sh @@ -1,8 +1,14 @@ #!/usr/bin/env bash # Detect the agent harness this process tree runs on. -# Usage: fm-harness.sh print own harness: claude|codex|opencode|pi|grok|unknown -# fm-harness.sh crew print the effective crewmate harness -# (config/crew-harness; "default" resolves to own) +# Usage: fm-harness.sh print own harness: claude|codex|opencode|pi|grok|unknown +# fm-harness.sh crew print the effective CREWMATE harness +# (config/crew-harness; "default" resolves to own) +# fm-harness.sh secondmate print the harness the PRIMARY uses to launch +# SECONDMATE agents: config/secondmate-harness -> +# config/crew-harness -> own. "default" or absent +# defers to the crew resolution, so an unset +# secondmate-harness behaves exactly as the crew +# harness did before this knob existed. # Detection layers: verified environment markers first, then process ancestry. # Record each newly verified env marker here. set -u @@ -49,10 +55,28 @@ detect_own() { echo unknown } -if [ "${1:-}" = "crew" ]; then - crew= +# Resolve the effective crewmate harness: config/crew-harness (a bare adapter +# name) wins; absent or "default" mirrors firstmate's own harness. +resolve_crew() { + local crew= [ -f "$CONFIG/crew-harness" ] && crew=$(tr -d '[:space:]' < "$CONFIG/crew-harness" || true) if [ -z "$crew" ] || [ "$crew" = "default" ]; then detect_own; else echo "$crew"; fi -else - detect_own -fi +} + +# Resolve the harness the PRIMARY uses to launch SECONDMATE agents: a fallback +# chain config/secondmate-harness -> config/crew-harness -> own. An absent or +# "default" config/secondmate-harness defers to the crew resolution, so an unset +# secondmate-harness behaves exactly as before this knob existed (a secondmate +# launched on the crew harness). config/secondmate-harness is the PRIMARY's own +# setting and is never inherited downstream - secondmates do not spawn secondmates. +resolve_secondmate() { + local sm= + [ -f "$CONFIG/secondmate-harness" ] && sm=$(tr -d '[:space:]' < "$CONFIG/secondmate-harness" || true) + if [ -z "$sm" ] || [ "$sm" = "default" ]; then resolve_crew; else echo "$sm"; fi +} + +case "${1:-}" in + crew) resolve_crew ;; + secondmate) resolve_secondmate ;; + *) detect_own ;; +esac diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index 14125e82468..cb527ade4fd 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -3,10 +3,17 @@ # its isolated firstmate home. # Usage: fm-spawn.sh [harness|launch-command] [--scout] # fm-spawn.sh [] [harness|launch-command] --secondmate -# With no harness arg, the harness comes from fm-harness.sh crew (config/crew-harness, -# falling back to firstmate's own harness). A bare adapter name (claude|codex| -# opencode|pi|grok) overrides it for this spawn. A non-flag string containing whitespace -# is treated as a RAW launch command - the escape hatch for verifying new adapters. +# With no harness arg, the harness comes from fm-harness.sh: a crewmate/scout +# spawn resolves the CREW harness (config/crew-harness, falling back to firstmate's +# own); a --secondmate spawn resolves the SECONDMATE harness (config/secondmate-harness +# -> config/crew-harness -> own), so the secondmate-vs-crewmate split is DURABLE +# across every respawn (recovery, /updatefirstmate, restart). A bare adapter name +# (claude|codex|opencode|pi|grok) overrides it for this spawn (either kind). A +# non-flag string containing whitespace is treated as a RAW launch command - the +# escape hatch for verifying new adapters. +# A --secondmate spawn also propagates the primary's declared inheritable config +# (config/crew-harness today) into the secondmate home's config/, so the +# secondmate's OWN crewmates inherit the primary's settings (fm-config-inherit-lib.sh). # --scout records kind=scout in the task's meta (report deliverable, scratch worktree; # see AGENTS.md task lifecycle); --secondmate records kind=secondmate and launches in a # provisioned firstmate home; the default is kind=ship. @@ -40,9 +47,12 @@ FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}" STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}" DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}" PROJECTS="${FM_PROJECTS_OVERRIDE:-$FM_HOME/projects}" +CONFIG="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}" SUB_HOME_MARKER=".fm-secondmate-home" # shellcheck source=bin/fm-ff-lib.sh . "$SCRIPT_DIR/fm-ff-lib.sh" +# shellcheck source=bin/fm-config-inherit-lib.sh +. "$SCRIPT_DIR/fm-config-inherit-lib.sh" # Skip the watcher guard when re-exec'd for one pair of a batch (FM_SPAWN_NO_GUARD is # set by the batch loop below), so the guard runs once for the batch, not once per pair. [ -n "${FM_SPAWN_NO_GUARD:-}" ] || "$FM_ROOT/bin/fm-guard.sh" || true @@ -163,8 +173,21 @@ case "$ARG3" in done ;; '') - HARNESS=$("$FM_ROOT/bin/fm-harness.sh" crew) - LAUNCH=$(launch_template "$HARNESS" "$KIND") || { echo "error: no launch template for harness '$HARNESS' (from config/crew-harness or detection); pass a raw launch command to use an unverified adapter" >&2; exit 1; } + # No explicit harness: resolve from config. A secondmate AGENT launches on the + # secondmate harness (config/secondmate-harness -> config/crew-harness -> own); + # every other kind uses the crew harness. Resolving here on every spawn is what + # makes the split DURABLE - a respawn (recovery, /updatefirstmate, restart) + # re-resolves, so config/secondmate-harness keeps governing secondmate launches + # across restarts. The launch_template lookup below is the unverified-adapter + # guard for both kinds: a harness with no template aborts the spawn. + if [ "$KIND" = secondmate ]; then + HARNESS=$("$FM_ROOT/bin/fm-harness.sh" secondmate) + harness_src='config/secondmate-harness (falling back to config/crew-harness)' + else + HARNESS=$("$FM_ROOT/bin/fm-harness.sh" crew) + harness_src='config/crew-harness' + fi + LAUNCH=$(launch_template "$HARNESS" "$KIND") || { echo "error: no launch template for harness '$HARNESS' (from $harness_src or detection); pass a raw launch command to use an unverified adapter" >&2; exit 1; } ;; *) HARNESS=$ARG3 @@ -340,6 +363,15 @@ if [ "$KIND" = secondmate ]; then else echo "warning: secondmate $ID sync skipped before launch: primary default-branch commit cannot be resolved" >&2 fi + # Inheritable-config propagation: push the primary's declared LOCAL config + # (config/crew-harness today) into this secondmate home's config/, so the + # secondmate's OWN crewmates inherit the primary's settings. config/ is + # gitignored, so this is a separate copy from the local-HEAD fast-forward above; + # primary-authoritative and re-pushed on every convergence. config/secondmate-harness + # is the primary's own knob and is deliberately NOT in the inheritable set + # (fm-config-inherit-lib.sh). A primary with no inheritable config set is a no-op. + propagate_inheritable_config "$CONFIG" "$PROJ_ABS/config" \ + || echo "warning: secondmate $ID config inheritance failed for $PROJ_ABS/config" >&2 if [ -f "$PROJ_ABS/data/charter.md" ]; then BRIEF="$PROJ_ABS/data/charter.md" else diff --git a/docs/scripts.md b/docs/scripts.md index 0f9862c926b..40e54315ea1 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -24,6 +24,7 @@ Each file also starts with a short header comment. | `fm-crew-state.sh` | Print one stable current-state line for a crew by reconciling its matching no-mistakes run-step, even when the pane has closed, with pane and status-log fallback | | `fm-tangle-lib.sh` | Shared default-branch resolution and primary-checkout tangle classification sourced by bootstrap and guard | | `fm-ff-lib.sh` | Shared guarded fast-forward helper for `/updatefirstmate` origin pulls and no-fetch local secondmate syncs | +| `fm-config-inherit-lib.sh` | Shared primary->secondmate inheritable-config propagation (a declared, extensible item list - `config/crew-harness` today) sourced by spawn and bootstrap | | `fm-tasks-axi-lib.sh` | Shared `tasks-axi` compatibility probe sourced by bootstrap and teardown | | `fm-wake-drain.sh` | Atomically drain queued watcher wakes before handling supervision work, then run the watcher-liveness guard | | `fm-wake-lib.sh` | Shared durable wake queue and portable lock helpers sourced by the watcher, drain, arm, guard, and daemon | @@ -34,7 +35,7 @@ Each file also starts with a short header comment. | `fm-pr-check.sh` | Record `pr=` and a verified `pr_head=` when available for a PR-ready task, then arm the watcher's merge poll | | `fm-promote.sh` | Promote a scout task in place so it becomes a protected ship task | | `fm-teardown.sh` | Return a clean, landed ship worktree or retire/release a secondmate home; requires scout reports, checks child work, removes firstmate-owned hook artifacts, and prints the backlog reminder | -| `fm-harness.sh` | Detect the running harness; resolve the effective crewmate harness | +| `fm-harness.sh` | Detect the running harness; resolve the effective crewmate (`crew`) or secondmate-launch (`secondmate`) harness | | `fm-lock.sh` | Per-home firstmate session lock | | `fm-x-lib.sh` | Shared X-mode `.env`, alternate env-file, relay, dry-run config, reply-thread splitting, and task-to-X-request meta-link helpers | | `fm-x-poll.sh` | Do one bounded X relay poll; without `FMX_PAIRING_TOKEN` it is silent, with a pending mention it stashes the full inbox JSON, including `in_reply_to`, and prints `x-mention ` | diff --git a/tests/fm-secondmate-harness.test.sh b/tests/fm-secondmate-harness.test.sh new file mode 100755 index 00000000000..403c8fc9dbe --- /dev/null +++ b/tests/fm-secondmate-harness.test.sh @@ -0,0 +1,437 @@ +#!/usr/bin/env bash +# Tests for the secondmate-vs-crewmate harness split and the primary->secondmate +# inheritable-config propagation. +# +# Two capabilities are under test: +# A) Harness split. config/secondmate-harness sets the harness the PRIMARY uses +# to launch SECONDMATE agents, independent of config/crew-harness (the +# crewmate harness). fm-harness.sh secondmate resolves the fallback chain +# config/secondmate-harness -> config/crew-harness -> own; an absent or +# "default" secondmate-harness behaves exactly as the crew harness did before +# this knob existed (full backward-compat). fm-spawn.sh resolves a secondmate +# launch through that mode, durably (every respawn re-resolves), while an +# explicit per-spawn harness arg still wins. +# B) Inheritance. The primary pushes a declared, extensible set of LOCAL +# (gitignored) config items - config/crew-harness today - down into each +# secondmate home's config/, so the secondmate's OWN crewmates inherit the +# primary's settings. It is primary-authoritative (re-pushed at secondmate +# spawn and on the bootstrap secondmate sweep) and config/secondmate-harness +# is deliberately NOT inherited (secondmates do not spawn secondmates). +set -u + +# shellcheck source=tests/lib.sh +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +# shellcheck source=bin/fm-ff-lib.sh +. "$ROOT/bin/fm-ff-lib.sh" +# shellcheck source=bin/fm-config-inherit-lib.sh +. "$ROOT/bin/fm-config-inherit-lib.sh" + +BASE_PATH=${FM_TEST_BASE_PATH:-/usr/bin:/bin:/usr/sbin:/sbin} +fm_git_identity fmtest fmtest@example.com +TMP_ROOT=$(fm_test_tmproot fm-secondmate-harness) + +# =========================================================================== +# A) fm-harness.sh secondmate resolution + fallback (deterministic detect_own) +# =========================================================================== +# detect_own is pinned to claude via CLAUDECODE=1 so the "fall through to own" +# cases are reproducible. Each row sets crew-harness / secondmate-harness in a +# fresh config dir (a literal '-' means leave the file absent) and asserts BOTH +# the secondmate resolution AND that crew resolution is unchanged (backward-compat). +#