Skip to content
Open
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
34 changes: 28 additions & 6 deletions bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
# to launch a ship task whose explicit --mode disagrees, so an adjusted brief and the
# recorded task metadata cannot drift apart.
# Ship briefs begin with a worktree-isolation assertion before the branch step.
# Both crewmate scaffolds carry one shared rule against administering the
# infrastructure every lane shares - the no-mistakes daemon and the worktree pool
# their own slot came from - so ship and scout cannot drift apart. A secondmate
# charter omits it: that home allocates and returns slots for its own crewmates.
# --mode is refused on scout and secondmate scaffolds: a scout's deliverable is a
# report rather than a merge, and a charter is not a delivery contract.
# There is no --yolo flag here. The worker never owns merge decisions, so yolo is
Expand Down Expand Up @@ -298,6 +302,28 @@ EOF
HERDR_SECTION=${HERDR_SECTION%$'\n'}
fi

# One shared string keeps the ship and scout infrastructure rule identical.
# Rule 2 governs file edits, so it does not prohibit pool administration.
# The secondmate charter deliberately omits this rule because a secondmate
# legitimately allocates and returns slots for crewmates in its own home.
IFS= read -r -d '' SHARED_INFRA_RULE <<'EOF' || true
7. Never administer infrastructure that every lane shares. Two things are shared:
- The `no-mistakes` daemon - one instance serving every lane/home, so stopping, restarting, or
updating it kills other lanes' in-flight pipeline runs. On ANY no-mistakes daemon error,
append `blocked: {the daemon error}` and stop; only firstmate manages the daemon.
- The worktree pool your own worktree came from, and the repository every lane's worktree
shares. Never create, remove, return, prune, move, or reassign a worktree or pool slot, and
never write into a sibling slot's directory. Rule 2 does not cover this: removing a worktree
is administration rather than an edit outside your directory, and it lands on lanes that are
running right now. The act is the rule and commands are only examples of it - `treehouse`
get/return/remove/prune, the equivalent operations on any other worktree provider or runtime
backend, and `git worktree add|remove|move|prune`. A slot that looks unused is not evidence
that it is free, and returning your own worktree is firstmate's job at cleanup, not yours.
If you genuinely need a second checkout, another slot, or the daemon touched, append
`blocked: {what you need}` and stop; firstmate arranges it.
EOF
SHARED_INFRA_RULE=${SHARED_INFRA_RULE%$'\n'}

if [ "$KIND" = scout ]; then
cat > "$BRIEF" <<EOF
You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human.
Expand Down Expand Up @@ -332,9 +358,7 @@ The report is the only thing that survives, so anything worth keeping must be in
append \`needs-decision: {summary of options}\` and stop. Firstmate will reply with the decision.
A decision or blocker you opened stays open until a \`resolved\` line carrying its exact key lands; a later \`done:\` or \`working:\` line never closes it, even when the answer is what started that work.
Firstmate's reply normally writes that closing line at answer time; when a blocker or wait clears WITHOUT a firstmate reply, append \`resolved: {how it cleared}\` yourself (same \`[key=<slug>]\` if you opened it with one) as you resume.
7. Never stop, restart, or update the shared \`no-mistakes\` daemon - it is one instance serving
every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes
daemon error, append \`blocked: {the daemon error}\` and stop; only firstmate manages the daemon.
$SHARED_INFRA_RULE

# Definition of done
Write your findings to \`$DATA/$ID/report.md\`.
Expand Down Expand Up @@ -449,9 +473,7 @@ $RULE1
append \`needs-decision: {summary of options}\` and stop. Firstmate will reply with the decision.
A decision or blocker you opened stays open until a \`resolved\` line carrying its exact key lands; a later \`done:\` or \`working:\` line never closes it, even when the answer is what started that work.
Firstmate's reply normally writes that closing line at answer time; when a blocker or wait clears WITHOUT a firstmate reply, append \`resolved: {how it cleared}\` yourself (same \`[key=<slug>]\` if you opened it with one) as you resume.
7. Never stop, restart, or update the shared \`no-mistakes\` daemon - it is one instance serving
every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes
daemon error, append \`blocked: {the daemon error}\` and stop; only firstmate manages the daemon.
$SHARED_INFRA_RULE

# Project memory
If \`AGENTS.md\` or \`CLAUDE.md\` already exists, or if this task produced durable project-intrinsic knowledge, run \`$FM_ROOT/bin/fm-ensure-agents-md.sh .\` in the worktree.
Expand Down
66 changes: 66 additions & 0 deletions tests/fm-brief.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,71 @@ test_scout_and_secondmate_scaffold() {
pass "fm-brief: scout and secondmate code paths still scaffold well-formed briefs"
}

# Rule 2 governs file edits rather than pool administration, so every crewmate
# scaffold must prohibit the administrative act itself. The rule is emitted from
# one shared string so the ship and scout copies cannot drift apart.
test_crewmate_scaffolds_forbid_pool_administration() {
local home id brief mode ship_rule scout_rule
home="$TMP_ROOT/pool-admin-home"
mkdir -p "$home/data"

for mode in no-mistakes direct-PR local-only; do
id="brief-pool-$mode"
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" alpha --mode "$mode" >/dev/null 2>&1 \
|| fail "fm-brief.sh --mode $mode exited non-zero"
brief="$home/data/$id/brief.md"
assert_grep "worktree pool" "$brief" \
"$mode ship brief did not name the shared worktree pool"
assert_grep "create, remove, return, prune, move, or reassign" "$brief" \
"$mode ship brief did not state the prohibition around the act"
# shellcheck disable=SC2016 # Literal command text must remain unexpanded.
assert_grep 'git worktree add|remove|move|prune' "$brief" \
"$mode ship brief did not name the concrete git worktree commands"
assert_grep "treehouse" "$brief" \
"$mode ship brief did not name the treehouse mutation commands"
assert_grep "any other worktree provider" "$brief" \
"$mode ship brief pinned one provider instead of covering every provider"
assert_grep "sibling slot" "$brief" \
"$mode ship brief did not forbid writing into a sibling slot"
# shellcheck disable=SC2016 # Literal backticks and braces must remain unexpanded.
assert_grep 'blocked: {what you need}' "$brief" \
"$mode ship brief gave the prohibition no exit for a genuine second-checkout need"
done

FM_HOME="$home" "$ROOT/bin/fm-brief.sh" brief-pool-scout alpha --scout >/dev/null 2>&1 \
|| fail "fm-brief.sh --scout exited non-zero"
brief="$home/data/brief-pool-scout/brief.md"
assert_grep "worktree pool" "$brief" "scout brief did not name the shared worktree pool"
# shellcheck disable=SC2016 # Literal backticks and braces must remain unexpanded.
assert_grep 'blocked: {what you need}' "$brief" "scout brief gave the prohibition no exit"

# One shared string, not two copies: the emitted rule must be byte-identical
# across the ship and scout scaffolds so a later edit cannot fix one and miss
# the other.
ship_rule=$(awk '/^7\. Never administer/,/^$/' "$home/data/brief-pool-no-mistakes/brief.md")
scout_rule=$(awk '/^7\. Never administer/,/^$/' "$brief")
[ -n "$ship_rule" ] || fail "ship brief emitted no shared-infrastructure rule to compare"
[ "$ship_rule" = "$scout_rule" ] \
|| fail "ship and scout shared-infrastructure rules have drifted apart"

# The daemon half of the rule survived the fold.
assert_grep "no-mistakes" "$brief" "scout brief lost the shared no-mistakes daemon rule"
# shellcheck disable=SC2016 # Literal backticks and braces must remain unexpanded.
assert_grep 'blocked: {the daemon error}' "$brief" \
"scout brief lost the daemon-error reporting instruction"

# A secondmate runs its own home and legitimately allocates and returns slots
# for its own crewmates, so the crewmate prohibition must NOT reach its charter.
FM_SECONDMATE_CHARTER='Supervise the alpha domain.' \
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" brief-pool-mate --secondmate alpha >/dev/null 2>&1 \
|| fail "fm-brief.sh --secondmate exited non-zero"
assert_no_grep "create, remove, return, prune, move, or reassign" \
"$home/data/brief-pool-mate/brief.md" \
"secondmate charter must not inherit the crewmate pool-administration prohibition"

pass "fm-brief.sh: every crewmate scaffold forbids administering the shared worktree pool"
}

test_script_parses
test_no_heredoc_in_command_substitution
test_help_includes_entire_header
Expand All @@ -732,3 +797,4 @@ test_secondmate_directory_paths_are_absolute_and_output_is_stable
test_pause_verb_override_renders_all_brief_scaffolds
test_scout_and_secondmate_load_decision_hold_policy
test_scout_and_secondmate_scaffold
test_crewmate_scaffolds_forbid_pool_administration
Loading