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
16 changes: 13 additions & 3 deletions bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
# local-only implement on branch, stop and report "ready in branch" (no push/PR);
# captain approves, firstmate merges to local main
# Ship briefs begin with a worktree-isolation assertion before the branch step.
# That assertion carries {FM_WORKTREE} and {FM_PRIMARY_CHECKOUT} placeholders
# because neither path exists yet at scaffold time; bin/fm-spawn.sh fills both in
# at launch, from the worktree it has already verified, and refuses to launch a
# brief left holding either placeholder.
# Scout tasks ignore mode - their deliverable is a report, not a merge.
# Every scaffold's status protocol distinguishes the configured
# declared-external-wait verb (FM_CLASSIFY_PAUSED_VERB, default "paused") from
Expand Down Expand Up @@ -369,9 +373,15 @@ $HERDR_SECTION
# Setup
You are in a disposable git worktree of $REPO, at a detached HEAD on a clean default branch.

**Verify isolation before anything else.** Run \`pwd -P\` and \`git rev-parse --show-toplevel\`; both must resolve to the disposable task worktree you were launched in, such as a treehouse pool path or an Orca-managed worktree, not the primary checkout firstmate operates from.
The path check is authoritative: \`git rev-parse --git-dir\` and \`git rev-parse --git-common-dir\` can help inspect the repo, but they do not prove you are outside the primary checkout.
If the top-level path is the primary checkout or not the worktree you were launched in, STOP - do not branch or commit here - append \`blocked: launched in primary checkout, not an isolated worktree\` to the status file and stop.
**Verify isolation before anything else.** Run \`cd "\$(git rev-parse --show-toplevel)" && pwd -P\` and compare what it prints against these two paths, which firstmate resolved and verified at launch:

- your isolated task worktree: {FM_WORKTREE}
- the primary checkout: {FM_PRIMARY_CHECKOUT}

If it equals the primary checkout, STOP - do not branch or commit here - append \`blocked: launched in primary checkout, not an isolated worktree\` to the status file and stop.
If it equals your task worktree, you are isolated: proceed.
If it is any third path, or the command fails, append \`blocked: isolation check read {the exact path or error}\` to the status file and stop.
\`git rev-parse --git-dir\` and \`git rev-parse --git-common-dir\` point into the primary checkout's \`.git\` for every linked worktree, including yours; that is expected and is not evidence that you are in the primary checkout.

1. First action: create your branch: \`git checkout -b fm/$ID\`$SETUP2

Expand Down
55 changes: 54 additions & 1 deletion bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@
# Before a secondmate launch, the home is locally fast-forwarded to the primary
# default-branch commit when safe; skipped syncs warn and launch unchanged.
# Ship/scout spawns refuse to launch unless the resolved task path is a real
# git worktree root distinct from the primary project checkout.
# git worktree root distinct from the primary project checkout. Once verified,
# both paths are written into the brief's isolation facts
# ({FM_WORKTREE}/{FM_PRIMARY_CHECKOUT} from bin/fm-brief.sh), and a brief still
# holding either placeholder refuses to launch.
# Batch dispatch: pass one or more `id=repo` pairs instead of a single <id> <project>, e.g.
# fm-spawn.sh fix-a-k3=projects/foo add-b-q7=projects/bar [--scout]
# Each pair re-execs this script in single-task mode, so the single path stays the only
Expand Down Expand Up @@ -929,6 +932,54 @@ validate_spawn_worktree() { # <source> <inspect-target>
echo "error: $source did not yield an isolated worktree (resolved '$WT'; worktree root '${wt_top:-none}'; primary '$PROJ_ABS'); refusing to launch to avoid tangling the primary checkout. Inspect target $inspect_target" >&2
exit 1
fi
# The verified physical worktree root, for fill_isolation_facts below. Physical
# because the worker compares against `git rev-parse --show-toplevel`, which
# reports the resolved path even when the shell reached the worktree through a
# symlink, while $WT keeps whatever raw path the backend reported.
WT_REAL=$wt_real
}

# Fill the ship brief's isolation facts with the two paths the worker cannot
# derive for itself: its own task worktree and the primary checkout.
# bin/fm-brief.sh writes {FM_WORKTREE}/{FM_PRIMARY_CHECKOUT} placeholders because
# neither path exists when the brief is scaffolded, and a worker asked to judge
# in prose whether it is "in the primary checkout" can refuse correct isolation -
# a firstmate-repo crewmate sees the primary checkout's path several times in its
# own brief and its own worktree path nowhere.
# Rewriting by line prefix rather than a one-shot placeholder swap keeps a
# relaunch into a different worktree correct instead of pinning the brief to the
# first slot it ever used. A brief carrying neither placeholder nor fact line -
# every brief scaffolded before this contract, and every secondmate charter - is
# left untouched. A placeholder surviving the rewrite is fatal: a worker reading a
# literal {FM_WORKTREE} has no isolation check at all.
fill_isolation_facts() { # <brief> <worktree-real> <primary-real>
local brief=$1 worktree=$2 primary=$3 tmp worktree_fact=0 primary_fact=0
grep -q '^- your isolated task worktree: ' "$brief" 2>/dev/null && worktree_fact=1
grep -q '^- the primary checkout: ' "$brief" 2>/dev/null && primary_fact=1
if [ "$worktree_fact" = 1 ] && [ "$primary_fact" = 0 ]; then
echo "error: $brief is missing the '- the primary checkout:' isolation fact line; refusing to launch a worker whose isolation check is missing an operand" >&2
exit 1
fi
if [ "$worktree_fact" = 0 ] && [ "$primary_fact" = 1 ]; then
echo "error: $brief is missing the '- your isolated task worktree:' isolation fact line; refusing to launch a worker whose isolation check is missing an operand" >&2
exit 1
fi
if [ "$worktree_fact" = 1 ]; then
tmp="$brief.fm-isolation-facts.$$"
if ! { FM_FILL_WORKTREE="$worktree" FM_FILL_PRIMARY="$primary" awk '
/^- your isolated task worktree: / { print "- your isolated task worktree: " ENVIRON["FM_FILL_WORKTREE"]; next }
/^- the primary checkout: / { print "- the primary checkout: " ENVIRON["FM_FILL_PRIMARY"]; next }
{ print }
' "$brief" > "$tmp" && mv "$tmp" "$brief"; }; then
rm -f "$tmp"
echo "error: could not write the isolation facts into $brief; refusing to launch a brief whose isolation check has no paths to compare" >&2
exit 1
fi
fi
if grep -q -e '{FM_WORKTREE}' -e '{FM_PRIMARY_CHECKOUT}' "$brief" 2>/dev/null; then
echo "error: $brief still contains an unfilled isolation placeholder; refusing to launch a worker whose isolation check cannot be evaluated" >&2
exit 1
fi
}

herdr_projection_meta_field_exact() { # <meta> <key>
Expand Down Expand Up @@ -1230,6 +1281,7 @@ EOF
exit 1
fi
validate_spawn_worktree "orca worktree create" "$W"
fill_isolation_facts "$BRIEF" "$WT_REAL" "$PROJ_ABS_REAL"
if [ -z "$ORCA_TERMINAL" ]; then
ORCA_TERMINAL=$(fm_backend_orca_terminal_create "$ORCA_WORKTREE_ID" "$W") || exit 1
fi
Expand Down Expand Up @@ -1456,6 +1508,7 @@ if [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ]; then
fi

validate_spawn_worktree "treehouse get" "$T"
fill_isolation_facts "$BRIEF" "$WT_REAL" "$PROJ_ABS_REAL"
fi

# Per-task temp root: /tmp/fm-<id>/ with Go's build temp nested at gotmp/. Go won't
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ Only a named non-default branch checked out in `FM_ROOT` is a worktree tangle.
`fm-tangle-lib.sh` resolves the default branch from `origin/HEAD`, then local `main` or `master`, and classifies that named non-default primary branch as the tangle.
`fm-guard.sh` prints the repair command on the next mutable fleet action, while `bin/fm-session-start.sh` reports the same condition through bootstrap as a `TANGLE:` line at session start.
If another live session holds the fleet lock, both surfaces keep the alarm but switch to read-only wording with no repair command.
Ship briefs also tell the crewmate to verify `pwd -P` and `git rev-parse --show-toplevel` before creating `fm/<id>`, then stop with a blocked status if it landed in the primary checkout.
After validating the task root, `fm-spawn.sh` writes the physically resolved task-worktree and primary-checkout paths into newly scaffolded ship briefs so the crewmate can compare its physical Git top level against named operands before creating `fm/<id>` and stop if it is not in the verified worktree.

## No-mistakes gate authority boundary

Expand Down
19 changes: 17 additions & 2 deletions tests/fm-backend-orca.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ test_spawn_preserves_orca_metadata_when_pathless_worktree_cleanup_fails() {
}

test_spawn_writes_orca_metadata_and_launches_harness() {
local proj wt data state config id out log
local proj wt data state config id out log proj_real wt_real
id="orcaspawnz1"
proj="$TMP_ROOT/spawn-project"
wt="$TMP_ROOT/spawn-wt"
Expand All @@ -518,7 +518,12 @@ test_spawn_writes_orca_metadata_and_launches_harness() {
config="$TMP_ROOT/spawn-config"
fm_git_worktree "$proj" "$wt" "fm/$id"
mkdir -p "$data/$id" "$state" "$config"
printf 'brief\n' > "$data/$id/brief.md"
cat > "$data/$id/brief.md" <<'EOF'
brief

- your isolated task worktree: {FM_WORKTREE}
- the primary checkout: {FM_PRIMARY_CHECKOUT}
EOF
touch "$state/.last-watcher-beat"
orca_case spawn
log="$LOG"
Expand All @@ -541,6 +546,16 @@ test_spawn_writes_orca_metadata_and_launches_harness() {
assert_grep "terminal=term-spawn" "$state/$id.meta" "meta missing terminal handle"
assert_grep "orca_worktree_id=wt-spawn" "$state/$id.meta" "meta missing Orca worktree id"
assert_grep "worktree=$wt" "$state/$id.meta" "meta missing Orca worktree path"
proj_real=$(cd "$proj" && pwd -P)
wt_real=$(cd "$wt" && pwd -P)
assert_grep "- your isolated task worktree: $wt_real" "$data/$id/brief.md" \
"Orca spawn did not fill the verified physical worktree into the brief"
assert_grep "- the primary checkout: $proj_real" "$data/$id/brief.md" \
"Orca spawn did not fill the physical primary checkout into the brief"
assert_no_grep "{FM_WORKTREE}" "$data/$id/brief.md" \
"Orca spawn left the worktree isolation placeholder unfilled"
assert_no_grep "{FM_PRIMARY_CHECKOUT}" "$data/$id/brief.md" \
"Orca spawn left the primary-checkout isolation placeholder unfilled"
assert_not_contains "$(cat "$log")" $'orca\x1f''terminal'$'\x1f''create' \
"spawn should reuse the implicit terminal returned by Orca worktree creation"
assert_contains "$(cat "$log")" $'orca\x1f''terminal'$'\x1f''send'$'\x1f''--terminal'$'\x1f''term-spawn'$'\x1f''--text'$'\x1f''export GOTMPDIR=/tmp/fm-orcaspawnz1/gotmp'$'\x1f''--enter'$'\x1f''--json' \
Expand Down
51 changes: 51 additions & 0 deletions tests/fm-brief.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,56 @@ test_ship_project_memory_wording() {
pass "fm-brief.sh: ship project-memory wording carries the AGENTS.md authoring bar"
}

# The isolation assertion must ship as an evaluable comparison against two named
# paths, never as prose the worker has to interpret. A worker cannot derive
# either path itself - a firstmate-repo crewmate sees the primary checkout's path
# repeatedly in its own brief and its worktree path nowhere - so the scaffold
# emits placeholders that bin/fm-spawn.sh fills in from the worktree it has
# already verified. Placeholders belong only where something fills them: the
# scout scaffold and the secondmate charter carry no isolation assertion, so a
# leaked placeholder there would be a permanently unfillable literal.
test_ship_isolation_assertion_names_both_paths() {
local home id brief scout charter
home="$TMP_ROOT/isolation-facts-home"
mkdir -p "$home/data"
id="brief-isolation-d1"
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" some-proj >/dev/null 2>&1
brief="$home/data/$id/brief.md"
assert_present "$brief" "brief was not scaffolded"
assert_grep "- your isolated task worktree: {FM_WORKTREE}" "$brief" \
"ship brief lost the fillable task-worktree isolation fact"
assert_grep "- the primary checkout: {FM_PRIMARY_CHECKOUT}" "$brief" \
"ship brief lost the fillable primary-checkout isolation fact"
assert_grep "If it equals the primary checkout, STOP" "$brief" \
"ship brief lost the stop branch for the primary checkout"
assert_grep "If it equals your task worktree, you are isolated: proceed." "$brief" \
"ship brief lost the proceed branch for a correctly isolated worker"
assert_grep "blocked: launched in primary checkout, not an isolated worktree" "$brief" \
"ship brief lost the isolation refusal status line"
assert_grep "If it is any third path, or the command fails" "$brief" \
"ship brief lost the third-path branch that reports what was actually read"
assert_no_grep "not the worktree you were launched in, STOP" "$brief" \
"ship brief kept the unverifiable launched-in referent that refused correct isolation"

FM_HOME="$home" "$ROOT/bin/fm-brief.sh" brief-isolation-d2 some-proj --scout >/dev/null 2>&1
scout="$home/data/brief-isolation-d2/brief.md"
assert_present "$scout" "scout brief was not scaffolded"
assert_no_grep "{FM_WORKTREE}" "$scout" \
"scout brief leaked an isolation placeholder nothing fills"
assert_no_grep "{FM_PRIMARY_CHECKOUT}" "$scout" \
"scout brief leaked an isolation placeholder nothing fills"

FM_SECONDMATE_CHARTER="fixture charter" FM_HOME="$home" \
"$ROOT/bin/fm-brief.sh" brief-isolation-d3 --secondmate --no-projects >/dev/null 2>&1
charter="$home/data/brief-isolation-d3/brief.md"
assert_present "$charter" "secondmate charter was not scaffolded"
assert_no_grep "{FM_WORKTREE}" "$charter" \
"secondmate charter leaked an isolation placeholder nothing fills"
assert_no_grep "{FM_PRIMARY_CHECKOUT}" "$charter" \
"secondmate charter leaked an isolation placeholder nothing fills"
pass "fm-brief.sh: the ship isolation assertion names both paths and placeholders stay where they are filled"
}

test_herdr_lab_contract_is_explicit_and_complete() {
local home id brief
home="$TMP_ROOT/herdr-lab-home"
Expand Down Expand Up @@ -659,6 +709,7 @@ test_ship_modes_generate_clean_briefs
test_faster_paths_use_configured_authority_without_stacked_review
test_no_mistakes_dod_wording
test_ship_project_memory_wording
test_ship_isolation_assertion_names_both_paths
test_herdr_lab_contract_is_explicit_and_complete
test_herdr_lab_contract_quotes_foreign_firstmate_path
test_herdr_lab_omission_is_loud_for_ship_and_scout
Expand Down
Loading
Loading