Skip to content
Closed
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,7 @@ The ship-brief Setup opens with a worktree-isolation assertion ahead of the bran
For a ship task the definition of done is shaped by the project's delivery mode (section 6): `no-mistakes` stops after the implementation commit, then firstmate triggers the harness-appropriate no-mistakes validation pipeline; `direct-PR` has the crewmate push and open the PR itself, and `local-only` has it stop at "ready in branch" for firstmate to review and merge locally.
The no-mistakes brief points to no-mistakes' version-matched guidance and keeps only firstmate-specific wrapper rules for `ask-user` escalation, `--yes` avoidance, and the CI-green done line.
The scaffold reads the mode via `fm-project-mode.sh`, so you do not pass it.
Two refinements are baked into every ship-mode definition of done and must be preserved when adjusting the scaffold: (1) task-specific acceptance criteria written in the Task body are declared part of the authoritative definition of done - a crewmate is not done merely for committing or for the pipeline going green, but only once those task-specific criteria are met too; this stops a crewmate from treating the formal DoD as the sole completion signal and ignoring the task body. (2) For a `local-only` project whose name is `no-mistakes` (the binary that runs the pipeline), the DoD adds a build+install+verify clause: the rebuilt binary must be installed to both `~/.local/bin/no-mistakes` and `~/.no-mistakes/bin/no-mistakes`, the daemon restarted, and the fix verified end-to-end before `done`, because a branch commit is otherwise unobservable. Because that clause writes outside the worktree, Rule 2 (the worktree-isolation rule) is correspondingly relaxed for this case to permit only the binary install and daemon restart it requires. Other `local-only` projects have no binary to install and get no such clause or Rule 2 relaxation; gate it on the repo name.
Ship briefs also include the project-memory contract: run `bin/fm-ensure-agents-md.sh` when the project already has agent-memory files or when the task produced durable project-intrinsic knowledge, then record proportionate learnings in `AGENTS.md`.
For scout tasks add `--scout`: the scaffold swaps the definition of done for the report contract (findings to `data/<id>/report.md`, no branch, no push, no PR) and declares the worktree scratch; scout is mode-agnostic.
Scout briefs do not include the project-memory step, because their deliverable is a report rather than a committed project change.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ tests/fm-grok-harness.test.sh # grok adapter spawn hook, token guard
tests/fm-fleet-sync.test.sh # project clone refresh: safe detached recovery, STUCK drift reports, benign skips, and bootstrap relay
tests/fm-x-mode.test.sh # X-mode poll, inbox context round-trip, reply threading, dismiss, dry-run preview, and .env-presence activation tests
tests/fm-tangle-guard.test.sh # primary-checkout tangle detection, read-only remediation suppression, and spawn/brief isolation tests
tests/fm-brief.test.sh # fm-brief.sh bash -n parse regression guard (issue #166) and clean no-mistakes/direct-PR/local-only brief generation tests
tests/fm-brief.test.sh # fm-brief.sh bash -n parse regression guard (issue #166), clean no-mistakes/direct-PR/local-only brief generation, no-mistakes local-only build+install+verify DoD clause with plain/default exclusion, task-body criteria elevated into the DoD, and overwrite refusal
tests/fm-spawn-batch.test.sh # batch dispatch and FM_HOME project-path scoping tests
tests/fm-spawn-dispatch-profile.test.sh # concrete dispatch profile flags: active-profile backstop, harness/model/effort meta, launch templates, batch forwarding, and secondmate exemption
tests/fm-update.test.sh # fast-forward-only self-update, reread, nudge, dedup, and skip-safety tests
Expand Down
27 changes: 24 additions & 3 deletions bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ read -r MODE _ <<EOF
$("$FM_ROOT/bin/fm-project-mode.sh" "$REPO")
EOF

RULE2='2. Stay inside this worktree; modify nothing outside it.'

case "$MODE" in
direct-PR)
SETUP2=""
Expand All @@ -179,20 +181,38 @@ case "$MODE" in
# Definition of done
This project ships **direct-PR**: you raise the PR yourself, without the no-mistakes pipeline.
The task is complete only when committed on your branch.
When it is implemented and committed, push your branch and open a PR with \`gh-axi\`, then append \`done: PR {url}\` to the status file and stop.
Any task-specific acceptance or completion criteria stated in the Task section above are part of this Definition of done - you are not done when you have merely committed; you are done only when those criteria are met as well.
When it is implemented and committed (and any task-specific criteria above are satisfied), push your branch and open a PR with \`gh-axi\`, then append \`done: PR {url}\` to the status file and stop.
Do NOT run /no-mistakes. The captain reviews and merges the PR; firstmate relays it.
EOF
)
;;
local-only)
SETUP2=""
RULE1="1. Never push to any remote and never open a PR. Work only on your \`fm/$ID\` branch; firstmate handles the merge into local \`main\`."
# no-mistakes develops the binary that runs the pipeline itself: a branch commit is
# not observable until the rebuilt binary is installed and the fix is verified
# end-to-end. Other local-only projects have no binary to install, so gate on repo.
NM_BUILD_CLAUSE=""
if [ "$REPO" = "no-mistakes" ]; then
RULE2='2. Stay inside this worktree; the only writes you may make outside it are the no-mistakes binary install and daemon restart spelled out in the Definition of done below (a local-only branch commit is otherwise unobservable).'
NM_BUILD_CLAUSE=$(cat <<EOF

If this task modifies no-mistakes source code, committing to your branch is NOT enough on its own. Before reporting done you MUST also:
- build the binary (\`make build\`),
- install it to BOTH \`~/.local/bin/no-mistakes\` AND \`~/.no-mistakes/bin/no-mistakes\`,
- restart the daemon (\`no-mistakes daemon stop && no-mistakes daemon start\`), and
- verify the fix end-to-end: reproduce the original failure against the freshly installed binary and confirm it is gone.
EOF
)
fi
DOD=$(cat <<EOF
# Definition of done
This project ships **local-only**: no remote, no PR, no pipeline.
The task is complete only when committed on your branch \`fm/$ID\`. Do NOT push, do NOT open a PR, do NOT merge.
Any task-specific acceptance or completion criteria stated in the Task section above are part of this Definition of done - you are not done when you have merely committed; you are done only when those criteria are met as well.$NM_BUILD_CLAUSE
Keep your branch a clean fast-forward onto the current default branch - if \`main\` has advanced, rebase onto it so the eventual merge stays a fast-forward.
When it is implemented and committed, append \`done: ready in branch fm/$ID\` to the status file and stop.
When it is implemented and committed (and any task-specific criteria above are satisfied), append \`done: ready in branch fm/$ID\` to the status file and stop.
Firstmate then reviews your branch diff, the captain approves, and firstmate merges it into local \`main\`.
EOF
)
Expand All @@ -204,6 +224,7 @@ EOF
DOD=$(cat <<EOF
# Definition of done
The task is complete only when committed on your branch.
Any task-specific acceptance or completion criteria stated in the Task section above are part of this Definition of done - a green pipeline is necessary but not sufficient; you are done only when those criteria are met as well.
When you believe it is complete, append \`done: {summary}\` to the status file and stop.
Firstmate will then instruct you to run /no-mistakes to validate and ship a PR.

Expand Down Expand Up @@ -239,7 +260,7 @@ If the top-level path is the primary checkout or not the worktree you were launc

# Rules
$RULE1
2. Stay inside this worktree; modify nothing outside it.
$RULE2
3. Use gh-axi for GitHub operations and chrome-devtools-axi for browser operations.
4. Report status by appending one line:
\`echo "{state}: {one short line}" >> $STATUS_FILE\`
Expand Down
2 changes: 1 addition & 1 deletion bin/fm-pr-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,4 @@ if ! caller_has_merge_method "$@"; then
merge_args=(--squash)
fi

gh-axi pr merge "$PR_NUMBER" --repo "$PR_OWNER/$PR_REPO" "${merge_args[@]}" "$@"
gh-axi pr merge "$PR_NUMBER" --repo "$PR_OWNER/$PR_REPO" ${merge_args[@]+"${merge_args[@]}"} "$@"
82 changes: 81 additions & 1 deletion tests/fm-brief.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
# parsing of the *entire rest of the script* - `bash -n` fails, not just the
# generated brief. A plain `cat > file <<EOF ... EOF` (not wrapped in `$(...)`)
# is unaffected, so the secondmate charter block does not need this guard.
#
# Also covers the local-only DoD regression: no-mistakes (the binary that runs
# the pipeline) must get a build+install+verify clause because a branch commit
# is not observable until the rebuilt binary is installed; other local-only
# projects must not. Also covers the elevation sentence that makes
# task-specific acceptance criteria in the Task body part of the authoritative
# Definition of done.
set -u

# shellcheck source=tests/lib.sh
Expand All @@ -26,10 +33,16 @@ test_script_parses() {

# Registry with one project per delivery mode, so each ship-mode DOD branch is
# exercised. A project absent from the registry defaults to no-mistakes.
# no-mistakes itself is registered local-only so the build+install+verify
# clause path is exercised, alongside a plain local-only project and an
# explicit no-mistakes-mode project.
write_registry() {
local home=$1
mkdir -p "$home/data"
cat > "$home/data/projects.md" <<'EOF'
- no-mistakes [local-only] - the gate tool (added 2026-07-01)
- otherproj [local-only] - a plain local project (added 2026-07-01)
- gatemate [no-mistakes] - a gated project (added 2026-07-01)
- direct-proj [direct-PR] - fixture for direct-PR mode (added 2026-07-01)
- local-proj [local-only] - fixture for local-only mode (added 2026-07-01)
EOF
Expand All @@ -41,7 +54,7 @@ EOF
# one of these DOD blocks, since a broken heredoc corrupts or empties the
# generated brief content, not just the script's own syntax.
test_ship_modes_generate_clean_briefs() {
local home id brief status
local home id proj brief status
home="$TMP_ROOT/ship-home"
write_registry "$home"

Expand Down Expand Up @@ -76,6 +89,73 @@ test_no_mistakes_dod_wording() {
pass "fm-brief.sh: no-mistakes DOD wording avoids the apostrophe regression"
}

# The local-only DoD must add a build+install+verify clause for no-mistakes
# (the binary that runs the pipeline) because a branch commit is otherwise
# unobservable until the rebuilt binary is installed and verified end-to-end.
# Other local-only projects must NOT get it, and non-local-only modes must NOT
# get it either.
test_local_only_dod_build_clause() {
local home
home="$TMP_ROOT/dod-home"
write_registry "$home"

# Case 1: no-mistakes local-only gets the build+install+verify clause.
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" dod-nm no-mistakes >/dev/null 2>&1
local b_nm="$home/data/dod-nm/brief.md"
assert_grep "If this task modifies no-mistakes source code" "$b_nm" \
"no-mistakes local-only brief missing build+install clause"
assert_grep "make build" "$b_nm" "no-mistakes local-only brief missing build step"
assert_grep "no-mistakes daemon stop && no-mistakes daemon start" "$b_nm" \
"no-mistakes local-only brief missing daemon restart"
assert_grep "verify the fix end-to-end" "$b_nm" "no-mistakes local-only brief missing end-to-end verify"

# Case 2: a plain local-only project must NOT get the binary clause.
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" dod-other otherproj >/dev/null 2>&1
local b_other="$home/data/dod-other/brief.md"
! grep -E "make build|\.no-mistakes/bin" "$b_other" >/dev/null || \
fail "plain local-only brief must not get the no-mistakes build clause"

# Case 3: no-mistakes (default) mode must NOT get the local-only binary clause.
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" dod-gate gatemate >/dev/null 2>&1
local b_gate="$home/data/dod-gate/brief.md"
! grep -E "make build|\.no-mistakes/bin/no-mistakes" "$b_gate" >/dev/null || \
fail "no-mistakes-mode brief must not get the local-only build clause"

pass "fm-brief.sh: local-only DoD build clause gated to the no-mistakes repo"
}

# The elevation sentence must be present in every ship mode, making the
# Task-body acceptance criteria part of the authoritative Definition of done.
test_dod_elevates_task_body_criteria() {
local home id
home="$TMP_ROOT/elev-home"
write_registry "$home"
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" elev-nm no-mistakes >/dev/null 2>&1
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" elev-other otherproj >/dev/null 2>&1
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" elev-gate gatemate >/dev/null 2>&1
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" elev-direct direct-proj >/dev/null 2>&1
for id in elev-nm elev-other elev-gate elev-direct; do
assert_grep "part of this Definition of done" "$home/data/$id/brief.md" \
"$id brief missing the DoD elevation sentence"
done
pass "fm-brief.sh: all ship-mode briefs elevate task-body criteria into the DoD"
}

# fm-brief.sh must refuse to clobber an existing brief.
test_refuses_to_overwrite() {
local home id rc
home="$TMP_ROOT/overwrite-home"
write_registry "$home"
id="brief-overwrite-c1"
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" no-mistakes >/dev/null 2>&1
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" no-mistakes >/dev/null 2>&1; rc=$?
[ "$rc" -ne 0 ] || fail "fm-brief.sh must refuse to overwrite an existing brief"
pass "fm-brief.sh: refuses to overwrite an existing brief"
}

test_script_parses
test_ship_modes_generate_clean_briefs
test_no_mistakes_dod_wording
test_local_only_dod_build_clause
test_dod_elevates_task_body_criteria
test_refuses_to_overwrite
8 changes: 4 additions & 4 deletions tests/fm-session-start.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ EOF
make_fake_toolchain "$fakebin"
make_fake_ps_claude "$fakebin"
# Force a MISSING diagnostic line so the bootstrap section is non-trivial.
rm -f "$fakebin/node"
rm -f "$fakebin/gh-axi"

printf 'window=fm-sess:w1\nkind=ship\n' > "$home/state/task-a.meta"

Expand All @@ -280,7 +280,7 @@ EOF
[ "$context_line" -lt "$fleet_line" ] || fail "CONTEXT did not precede FLEET STATE"
[ "$fleet_line" -lt "$next_line" ] || fail "FLEET STATE did not precede NEXT STEP"

missing_line=$(printf '%s\n' "$out" | grep -n 'MISSING: node' | head -1 | cut -d: -f1)
missing_line=$(printf '%s\n' "$out" | grep -n 'MISSING: gh-axi' | head -1 | cut -d: -f1)
[ -n "$missing_line" ] || fail "MISSING diagnostic did not appear at all"
[ "$missing_line" -lt "$fleet_line" ] || fail "actionable MISSING diagnostic was buried after the bulk fleet-state digest"

Expand Down Expand Up @@ -400,7 +400,7 @@ $rec
EOF
make_fake_toolchain "$fakebin"
make_fake_ps_claude "$fakebin"
rm -f "$fakebin/node"
rm -f "$fakebin/gh-axi"

append_wake "$home/state" signal task-z "needs-decision: pick a library"

Expand All @@ -409,7 +409,7 @@ EOF
# fm-lock.sh's own exact success text.
assert_contains "$out" "lock acquired: harness pid" "fm-lock.sh's real output did not appear (composition, not reimplementation)"
# fm-bootstrap.sh's own exact MISSING-tool line format.
assert_contains "$out" "MISSING: node (install:" "fm-bootstrap.sh's real detect line did not appear verbatim"
assert_contains "$out" "MISSING: gh-axi (install:" "fm-bootstrap.sh's real detect line did not appear verbatim"
# fm-wake-drain.sh's real drained record (raw tab-separated queue line).
assert_contains "$out" "$(printf 'signal\ttask-z\tneeds-decision: pick a library')" "fm-wake-drain.sh's real drained record did not appear"

Expand Down