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
12 changes: 10 additions & 2 deletions bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,14 @@ case "$MODE" in
esac
DOD=$(fm_dod_block "$MODE" "$ID") || exit 1

# A mode=no-mistakes ship task's status report routes through the guarded
# fm-status-append.sh helper instead of a bare echo: it refuses a `done:` line
# that names no PR URL, printing the exact next step instead, so "committed,
# gates green" mechanically cannot pass as done. Other modes have no pipeline
# step to skip, so they keep the bare echo.
REPORT_CMD=$(fm_status_report_line "$MODE" "$FM_ROOT" "$STATUS_FILE")
REPORT_GUARD_NOTE=$(fm_status_report_guard_note "$MODE" "$STATUS_FILE")

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 All @@ -417,7 +425,7 @@ $RULE1
2. Stay inside this worktree; modify nothing outside it.
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\`
\`$REPORT_CMD\`
States: working, needs-decision, blocked, $PAUSED_VERB, done, failed.
Each append wakes firstmate, so report sparingly: only phase changes a supervisor
would act on (setup done, bug reproduced, fix implemented, validation passed) and the
Expand All @@ -428,7 +436,7 @@ $RULE1
Use \`$PAUSED_VERB: {why}\` - distinct from \`blocked:\` - ONLY when you are deliberately idling on a
known external wait you expect to clear on its own (an upstream release, a rate-limit reset,
a scheduled window): firstmate then leaves your idle pane alone and rechecks it on a long
cadence instead of treating it as a possible wedge. Use \`blocked:\` when you are stuck and need help.
cadence instead of treating it as a possible wedge. Use \`blocked:\` when you are stuck and need help.$REPORT_GUARD_NOTE
5. If you hit the same obstacle twice, append \`blocked: {why}\` and stop; firstmate will help.
6. If a decision belongs above the implementation worker (product choices, destructive actions, ask-user findings),
append \`needs-decision: {summary of options}\` and stop. Firstmate will reply with the decision.
Expand Down
23 changes: 22 additions & 1 deletion bin/fm-crew-state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@
# 4. No run for this crew (pre-validation, or kind=scout): fall back to the
# recorded backend's pane busy state, then the status log's last line only
# when its verb maps to a recognized run-state. Decision-only events such as
# `resolved` never become current state or detail.
# `resolved` never become current state or detail. A mode=no-mistakes ship
# task's own `done:` with no run ever attributed and no validated pr=
# recorded in state/<id>.meta is reported as parked, not done: it declared
# done without ever starting the pipeline it still owes.
# 5. Missing meta or torn-down worktree: report unknown · none. If no run is
# attributed to this crew, a dead endpoint also reports unknown · none rather
# than trusting a stale status log.
Expand Down Expand Up @@ -108,8 +111,18 @@ WT=$(meta_value worktree)
KIND=$(meta_value kind)
HARNESS=$(meta_value harness)
REMOTE_HOST=$(meta_value remote_host)
MODE=$(meta_value mode)
[ -n "$KIND" ] || KIND=ship

# 0 if this is a mode=no-mistakes ship task that has declared done without the
# pipeline ever recording a validated PR (bin/fm-pr-check.sh's pr= line). Such a
# task is NOT done - a worker that stops at "committed, gates green" has not run
# /no-mistakes yet - so callers use this to keep that status-log verb from being
# read as the real terminal state.
crew_declared_done_without_pr() {
[ "$KIND" = ship ] && [ "$MODE" = no-mistakes ] && ! grep -q '^pr=' "$META" 2>/dev/null
}

# A torn-down (or never-created) worktree has no current state to read. A
# remote secondmate's recorded worktree is a path on ITS host, so the local
# probe proves nothing for it - the remote arm below reads the true source.
Expand Down Expand Up @@ -691,6 +704,14 @@ fi
# `unknown` verdict as the "not a state" test needs no second verb list here.
if [ -n "$LOG_VERB" ]; then
LOG_STATE=$(map_log_state "$LOG_LINE")
# A no-mistakes ship task's own `done:` line means "implementation complete",
# never "shipped" - no-mistakes still owns review, fixes, push, PR, and CI. With
# no run attributed at all (this fallback) and no validated pr= recorded, that
# line is reported as parked (needing firstmate's attention), not done, so a
# crew that stopped short of the pipeline never reads as finished.
if [ "$LOG_STATE" = "done" ] && crew_declared_done_without_pr; then
emit parked status-log "implementation complete, pipeline not started - run /no-mistakes"
fi
if [ "$LOG_STATE" != unknown ]; then
emit "$LOG_STATE" status-log "$(status_line_note "$LOG_LINE")"
fi
Expand Down
53 changes: 45 additions & 8 deletions bin/fm-dod-lib.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
#!/usr/bin/env bash
# Single owner of a ship task's mode-specific "Definition of done" block.
# Sourced by bin/fm-brief.sh, which renders it into a generated ship brief, and by
# bin/fm-promote.sh, which renders it into the ship instructions a promoted scout
# Single owner of a ship task's mode-specific "Definition of done" block, and of
# the mode-specific status-report command that goes with it.
# Sourced by bin/fm-brief.sh, which renders both into a generated ship brief, and by
# bin/fm-promote.sh, which renders both into the ship instructions a promoted scout
# receives. Both paths must hand the worker the same contract: a promoted
# no-mistakes worker that never received the ask-user escalation rule or the
# `--yes` ban is the exact delivery hole this single owner exists to close.
# no-mistakes worker that never received the ask-user escalation rule, the
# `--yes` ban, or the guarded status-report command is the exact delivery hole
# this single owner exists to close.
# fm_dod_block <no-mistakes|direct-PR|local-only> <task-id> prints the block on
# stdout with no trailing blank line. The caller validates the mode; an unknown
# mode is refused rather than silently rendered as the pipeline contract.
# The block opens with the fixed machine-readable "Delivery contract: mode=<mode>"
# line that bin/fm-spawn.sh checks a ship brief against.
# fm_status_report_line <mode> <fm-root> <quoted-status-file> prints the exact
# status-report command a worker on that mode must run: the guarded
# bin/fm-status-append.sh helper for mode=no-mistakes (the only mode with a
# pipeline step to skip), otherwise the bare `echo ... >> status-file` every
# other mode keeps. fm_status_report_guard_note <mode> <quoted-status-file>
# prints the accompanying warning against bypassing the helper (empty for
# every mode but no-mistakes), formatted to append inline after a sentence
# (leading newline, no trailing one).
# Every heredoc here stays outside a command substitution: `VAR=$(cat <<EOF ...)`
# breaks parsing of the whole file on Bash 3.2 (tests/fm-brief.test.sh).

Expand Down Expand Up @@ -41,9 +51,15 @@ EOF
cat <<EOF
# Definition of done
Delivery contract: mode=no-mistakes
The task is complete only when committed on your branch.
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.
"Committed, gates green" is NOT done. This mode still owns review, fixes, tests,
documentation, push, PR, and CI, and YOU drive that pipeline - firstmate does not
send you a follow-up instruction to start it. The one and only \`done:\` this task
ever reports is after CI is green with a PR; there is no earlier or intermediate
done state, so never append \`done:\` for the implementation/commit alone.

The moment your implementation is committed on your branch, in the SAME turn:
1. Append \`working: implementation committed, starting /no-mistakes\` to the status file (nonterminal - do not stop here).
2. Immediately invoke /no-mistakes yourself. Do not stop and wait for a firstmate instruction between the commit and this step.

You drive no-mistakes by responding to its gates, not by implementing fixes.
Follow the guidance no-mistakes itself provides for the mechanics: it loads when you invoke /no-mistakes, and \`no-mistakes axi run --help\` plus the \`help\` lines in each \`axi\` response are authoritative and version-matched to the installed binary.
Expand All @@ -65,3 +81,24 @@ EOF
return 1 ;;
esac
}

fm_status_report_line() { # <mode> <fm-root> <quoted-status-file>
local mode=$1 fm_root=$2 status_file_q=$3
case "$mode" in
no-mistakes)
printf '%s %s "{state}: {one short line}"' "$(printf '%q' "$fm_root/bin/fm-status-append.sh")" "$status_file_q"
;;
*)
printf 'echo "{state}: {one short line}" >> %s' "$status_file_q"
;;
esac
}

fm_status_report_guard_note() { # <mode> <quoted-status-file>
local mode=$1 status_file_q=$2
[ "$mode" = no-mistakes ] || return 0
cat <<EOF

This mode=no-mistakes task's status file is guarded: appending \`done:\` this way is refused, with the exact next step printed instead, unless the line itself names the pipeline's PR URL. Never bypass the helper with a bare \`echo ... >> $status_file_q\`.
EOF
}
25 changes: 21 additions & 4 deletions bin/fm-promote.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
# delivers them. Those instructions carry the scratch-state inventory, the clean
# default-branch base, the fm/<task-id> branch, and - rendered from
# bin/fm-dod-lib.sh, the single owner an ordinary ship brief also uses - the
# mode-specific Definition of done, so a promoted worker receives exactly the same
# delivery contract as a briefed one, including the no-mistakes mode's ask-user
# escalation rule and --yes ban.
# mode-specific Definition of done and status-report command, so a promoted
# worker receives exactly the same delivery contract as a briefed one,
# including the no-mistakes mode's ask-user escalation rule, --yes ban, and
# guarded bin/fm-status-append.sh status-report command (a scout is never
# mode=no-mistakes, so its own status-report rule is always the bare echo that
# command replaces for a promoted no-mistakes worker).
# A scout records no delivery posture, so promotion is where this task's delivery
# contract is decided: --mode and --yolo are REQUIRED and written into the meta
# alongside the kind= flip. Firstmate resolves both at promotion time, having just
Expand Down Expand Up @@ -136,6 +139,20 @@ grep -qx 'kind=scout' "$META" || { echo "error: task $ID is not a scout task (ki
INSTRUCTIONS="$DATA/$ID/ship-instructions.md"
mkdir -p "$DATA/$ID"
[ ! -d "$INSTRUCTIONS" ] || { echo "error: ship instructions path is a directory: $INSTRUCTIONS" >&2; exit 1; }

# A promoted worker's status-report command is rendered from the same single
# owner (bin/fm-dod-lib.sh) an ordinary ship brief uses, not hand-copied from
# the scout brief's bare echo: a scout is never mode=no-mistakes, so its rule 4
# is always the bare echo, and that command carrying over unchanged into a
# promoted no-mistakes worker's instructions is the exact structural hole the
# guarded helper (bin/fm-status-append.sh) exists to close.
STATUS_FILE_Q=$(printf '%q' "$STATE/$ID.status")
REPORT_CMD=$(fm_status_report_line "$MODE" "$FM_ROOT" "$STATUS_FILE_Q")
REPORT_GUARD_NOTE=$(fm_status_report_guard_note "$MODE" "$STATUS_FILE_Q")
CARRYOVER_RULE="6. These ship instructions supersede the scout delivery rules and report-based Definition of done. Everything else in your original instructions carries over unchanged: the instruction inbox and its acknowledgement; the escalation rules, including ask-user; and every safety rule.
7. Report status by appending one line:
\`$REPORT_CMD\`$REPORT_GUARD_NOTE"

TMP="$DATA/$ID/.ship-instructions.md.${BASHPID:-$$}"
{
cat <<EOF
Expand All @@ -147,7 +164,7 @@ Your scout task has been promoted to a ship task, mode=$MODE. Your window, workt
3. Return to a clean default-branch base, then create your branch: \`git checkout -b fm/$ID\`.
4. Carry over only the intended fix changes. Leave scratch commits, debug edits, and experiment files behind.
5. If you reproduced a bug, turn that reproduction into a regression test.
6. These ship instructions supersede the scout delivery rules and report-based Definition of done. Everything else in your original instructions carries over unchanged: the status protocol; the instruction inbox and its acknowledgement; the escalation rules, including ask-user; and every safety rule.
$CARRYOVER_RULE

EOF
fm_dod_block "$MODE" "$ID"
Expand Down
69 changes: 69 additions & 0 deletions bin/fm-status-append.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# fm-status-append.sh - guarded status-line append for a crew's status file.
#
# A crewmate reports its own state by appending one line to state/<id>.status
# (AGENTS.md section 3's status protocol). For a mode=no-mistakes ship task,
# done only means the pipeline reported CI green with a PR - no-mistakes still
# owns review, fixes, tests, documentation, push, PR, and CI, so a worker that
# stops at "committed, gates green" without ever starting it is not done.
# Brief wording alone has repeatedly failed to stop that early stop (see git
# history for this file's introducing PR), so this is the mechanical
# backstop: bin/fm-brief.sh's generated no-mistakes ship brief routes its
# status-report command through this helper instead of a bare
# `echo ... >> status-file`. Other delivery modes and kinds keep the bare
# echo, since they have no pipeline step to skip.
#
# The gate cannot key off state/<id>.meta's pr= line: firstmate writes that
# (via bin/fm-pr-check.sh) only AFTER seeing the worker's own done report
# (AGENTS.md section 7), so pr= is never present yet at the moment this exact
# call fires - checking it here would refuse every legitimate final done too.
# Instead this checks the line's own shape: the DOD's only prescribed final
# done text is `done: PR {url} checks green`, so a done: line that names a
# real http(s) URL is accepted, and one that does not (e.g. "committed, gates
# green", or the DOD's earlier "done: {summary}" wording) is refused.
#
# Usage: fm-status-append.sh <status-file> <status-line>
# <status-file> the crew's state/<id>.status path; its sibling
# state/<id>.meta (same basename, .meta instead of .status)
# is read for kind= and mode=.
# <status-line> the exact line to append, e.g. "done: implemented".
#
# Refuses (exit 1, printing the required next step to stderr instead of
# appending) only a `done:` line on a mode=no-mistakes ship task that names no
# URL. Every other line, mode, and kind appends exactly as a bare echo would -
# this is a drop-in replacement, not a new contract.
set -eu

[ $# -eq 2 ] || { echo "usage: fm-status-append.sh <status-file> <status-line>" >&2; exit 2; }
STATUS_FILE=$1
LINE=$2
META="${STATUS_FILE%.status}.meta"

meta_value() { # <key>
[ -f "$META" ] || return 0
grep "^$1=" "$META" 2>/dev/null | tail -1 | cut -d= -f2- || true
}

case "$LINE" in
done:*)
KIND=$(meta_value kind)
[ -n "$KIND" ] || KIND=ship
MODE=$(meta_value mode)
if [ "$KIND" = ship ] && [ "$MODE" = no-mistakes ]; then
case "$LINE" in
*https://*|*http://*) ;;
*)
cat >&2 <<'EOF'
refused: this is a mode=no-mistakes task, so "committed, gates green" is not done.
Run /no-mistakes now and respond to its gates until it reports CI green, then
report done as: done: PR {url} checks green
EOF
exit 1
;;
esac
fi
;;
esac

mkdir -p "$(dirname "$STATUS_FILE")" 2>/dev/null || true
printf '%s\n' "$LINE" >> "$STATUS_FILE"
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ The `data/secondmates.md` line contract is owned by the [`secondmate-provisionin
Each task's mode and `yolo` merge posture are firstmate's decision at intake.
The mode is passed explicitly to `bin/fm-brief.sh`, and both values are passed explicitly to `bin/fm-spawn.sh` and `bin/fm-promote.sh`; each command refuses to guess the values it consumes.
A ship brief records its mode as a fixed machine-readable line and the spawn refuses to launch on a different one, so the worker's instructions and the recorded task delivery cannot diverge.
`bin/fm-dod-lib.sh` is the one owner of that mode's definition of done, rendered both into a generated ship brief and into the ship instructions a promoted scout receives, so a promoted worker cannot be handed a weaker contract than a briefed one.
`bin/fm-dod-lib.sh` is the one owner of that mode's definition of done and status-report command, rendered both into a generated ship brief and into the ship instructions a promoted scout receives, so a promoted worker cannot be handed a weaker contract than a briefed one.
`data/projects.md` records each project's standing posture and optional `+yolo` merge flag as the captain's default and as context for that decision, including the conditional `no-mistakes-prod-only` policy; a ship spawn that drops below the registered rigor prints a deviation notice and continues.
`bin/fm-project-mode.sh` remains the one registry parser for the mechanical consumers that have no task in hand: fleet sync's `local-only` skip and home seeding's refusal and no-mistakes initialization.
When a selected delivery path calls for a diff, `bin/fm-review-diff.sh` refreshes the authoritative base and, when task meta records `pr=`, always fetches and compares against `refs/pull/<n>/head` by default (recorded `pr_head=` is only an offline fallback) before falling back to the local branch with a warning.
Expand Down
1 change: 1 addition & 0 deletions docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize
| `fm-decision-hold.sh` | One-release compatibility shim mapping the retired decision commands onto fm-captain-hold.sh |
| `fm-brief.sh` | Scaffold ship (explicit `--mode`), scout, secondmate-charter, and Herdr-lab briefs |
| `fm-dod-lib.sh` | One owner of the ship task's mode-specific definition of done, rendered by both the brief scaffold and a scout promotion |
| `fm-status-append.sh` | Guarded status-line append a mode=no-mistakes ship brief routes its status report through; refuses a `done:` line that names no PR URL |
| `fm-herdr-lab.sh` | Provision and guardedly operate an isolated, never-default Herdr lab session |
| `fm-install-herdr.sh` | Install CI's exact-version Herdr pin with official asset URL, SHA-256, and protocol checks |
| `fm-install-treehouse.sh`| Install CI's exact-version Treehouse pin for real-Herdr E2E that needs spawn worktrees |
Expand Down
2 changes: 1 addition & 1 deletion tests/fm-bearings-snapshot.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,7 @@ EOF
EOF
fm_write_meta "$mate/state/done.meta" \
"window=firstmate:fm-done" "worktree=$mate/projects/done" "project=sample" \
"harness=claude" "kind=ship" "mode=no-mistakes"
"harness=claude" "kind=ship" "mode=no-mistakes" "pr=https://github.com/sample/sample/pull/1"
fm_write_meta "$mate/state/failed.meta" \
"window=firstmate:fm-failed" "worktree=$mate/projects/failed" "project=sample" \
"harness=claude" "kind=ship" "mode=no-mistakes"
Expand Down
Loading
Loading