Skip to content
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ After an autonomous merge, give the captain a one-line full-URL or local-main ou

### Validate

For a no-mistakes ship, trigger validation on the same worker after its implementation commit, using the harness invocation owned by `harness-adapters`.
For a no-mistakes ship, treat the generated brief's non-terminal `needs-validation:` handoff as the trigger to validate on the same worker, using the harness invocation owned by `harness-adapters`.
The task worker that starts a no-mistakes run drives the pipeline and owns every `no-mistakes axi run` and `no-mistakes axi respond` call through the next gate or outcome.
Firstmate never invokes `no-mistakes axi respond` for a crew-owned run.
Once validation starts, prefer routing new requirements to follow-up work rather than expanding the current task, unless a new requirement completely invalidates the work being validated; however, the smallest downstream changes needed to keep already accepted product or engineering behavior correct, add behavioral tests where an executable contract exists, or keep documentation accurate remain within the current task even when they touch files not named at intake, and corrections required to satisfy already accepted intent are not new requirements.
Expand Down
11 changes: 7 additions & 4 deletions bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ esac
# shellcheck source=bin/fm-classify-lib.sh
. "$SCRIPT_DIR/fm-classify-lib.sh"
PAUSED_VERB=${FM_CLASSIFY_PAUSED_VERB:-$FM_CLASSIFY_PAUSED_VERB_DEFAULT}
NEEDS_VALIDATION_VERB=${FM_CLASSIFY_NEEDS_VALIDATION_VERB:-$FM_CLASSIFY_NEEDS_VALIDATION_VERB_DEFAULT}

resolve_directory_input() {
local name=$1 path=$2 resolved
Expand Down Expand Up @@ -374,6 +375,7 @@ fi
# delivery mode, validated above. The generated DOD opens with the fixed
# "Delivery contract: mode=<mode>" line that bin/fm-spawn.sh checks against its own
# explicit --mode before launching.
STATUS_STATES="working, needs-decision, blocked, $PAUSED_VERB, done, failed"
case "$MODE" in
direct-PR)
SETUP2=""
Expand Down Expand Up @@ -401,14 +403,15 @@ The configured merge authority approves the ready branch, then firstmate merges
EOF
;;
*) # no-mistakes
STATUS_STATES="working, $NEEDS_VALIDATION_VERB, needs-decision, blocked, $PAUSED_VERB, done, failed"
SETUP2="
2. Run \`no-mistakes doctor\`; if it reports the repo is not initialized here, run \`no-mistakes init\`."
RULE1='1. Never push to the default branch. Never merge a PR.'
IFS= read -r -d '' DOD <<EOF || true
# 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.
The implementation handoff is ready only when committed on your branch.
When implementation is complete and committed, append \`$NEEDS_VALIDATION_VERB: {summary}\` to the status file and stop.
Firstmate will then instruct you to run /no-mistakes to validate and ship a PR.

You drive no-mistakes by responding to its gates, not by implementing fixes.
Expand Down Expand Up @@ -455,13 +458,13 @@ $RULE1
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\`
States: working, needs-decision, blocked, $PAUSED_VERB, done, failed.
States: $STATUS_STATES.
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
needs-decision/blocked/paused/done/failed states. No step-by-step FYI progress lines;
firstmate reads your pane for that.
A mid-task \`working:\` line (including setup complete) is nonterminal: do not end the
turn after it; continue the same stage until a defined \`done:\` gate under Definition of done.
turn after it; continue the same stage until a stop/report gate defined under Definition of done.
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
Expand Down
35 changes: 28 additions & 7 deletions bin/fm-classify-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,24 @@ case $- in *u*) _fm_classify_nounset=on ;; *) _fm_classify_nounset=off ;; esac
[ "$_fm_classify_nounset" = on ] || set +u
unset _fm_classify_nounset

# The no-mistakes implementation-handoff verb. It is captain-relevant because
# firstmate must trigger validation, but deliberately non-terminal: only the
# later done: PR <url> checks green event reports completed validation.
FM_CLASSIFY_NEEDS_VALIDATION_VERB_DEFAULT='needs-validation'

# Captain-relevant status verbs. A status line carrying any of these is work
# firstmate must see. Lines without these verbs are no-verb signals: the watcher
# absorbs them only with positive provably-working evidence, while the daemon uses
# its away-mode classification. FM_CAPTAIN_RE overrides the whole set when a home
# needs a custom verb vocabulary; absent, this default applies.
# its away-mode classification. FM_CAPTAIN_RE replaces the general set when a home
# needs a custom verb vocabulary, but cannot suppress the required implementation
# handoff; absent, this default applies.
#
# Free-text tokens (PR ready, checks green, ready in branch, merged) exist only for
# legacy lines that lack a standard terminal verb. status_is_captain_relevant is
# verb-aware: a nonterminal working: or paused: line never becomes captain-relevant
# merely because its prose contains one of those tokens (for example
# "working: rebased onto merged #76").
FM_CLASSIFY_CAPTAIN_RE_DEFAULT='done:|needs-decision:|blocked:|failed:|PR ready|checks green|ready in branch|merged'
FM_CLASSIFY_CAPTAIN_RE_DEFAULT="done:|${FM_CLASSIFY_NEEDS_VALIDATION_VERB:-$FM_CLASSIFY_NEEDS_VALIDATION_VERB_DEFAULT}:|needs-decision:|blocked:|failed:|PR ready|checks green|ready in branch|merged"

# The deliberate-external-wait verb. A crew (or firstmate steering it) appends
# paused: <reason>
Expand Down Expand Up @@ -100,8 +106,9 @@ last_status_line() {
}

# 0 if the given (last) status line's leading verb is a real terminal captain verb
# (done, needs-decision, blocked, failed). Free-text tokens alone never count here;
# callers that need legacy free-text matching use status_is_captain_relevant.
# (done, needs-decision, blocked, failed). The captain-relevant needs-validation
# handoff is deliberately absent. Free-text tokens alone never count here; callers
# that need legacy free-text matching use status_is_captain_relevant.
status_is_terminal_verb() {
local line=$1 verb
[ -n "$line" ] || return 1
Expand All @@ -126,6 +133,9 @@ status_is_captain_relevant() {
working|resolved|captain-held|"${FM_CLASSIFY_PAUSED_VERB:-$FM_CLASSIFY_PAUSED_VERB_DEFAULT}")
return 1
;;
"${FM_CLASSIFY_NEEDS_VALIDATION_VERB:-$FM_CLASSIFY_NEEDS_VALIDATION_VERB_DEFAULT}")
return 0
;;
esac
if [ -z "${FM_CAPTAIN_RE+x}" ]; then
case "$verb" in
Expand All @@ -135,6 +145,16 @@ status_is_captain_relevant() {
printf '%s' "$line" | grep -qiE "${FM_CAPTAIN_RE:-$FM_CLASSIFY_CAPTAIN_RE_DEFAULT}"
}

# 0 if a status line is the non-terminal no-mistakes implementation handoff.
# The marker is separate from done: so a supervisor and deterministic state
# reader can tell "trigger validation" from "validation finished" by verb alone.
status_is_validation_handoff() { # <status-line>
local line=$1 verb
[ -n "$line" ] || return 1
verb=$(status_line_verb "$line")
[ "$verb" = "${FM_CLASSIFY_NEEDS_VALIDATION_VERB:-$FM_CLASSIFY_NEEDS_VALIDATION_VERB_DEFAULT}" ]
}

# 0 if a status line's leading verb is the pause verb (paused: <reason>). A pure
# read of the line itself, so the daemon's classify_stale can reuse the last line
# it already read without a fm-crew-state.sh call. Matches only the verb before the
Expand Down Expand Up @@ -1109,10 +1129,11 @@ EOF
# It is never authoritative current crew state, and consumers must not let an open
# phase outrank a structured home snapshot or fm-crew-state result.
_fm_status_open_activities_stream() {
local line verb key note resolve held open='' stripped pause
local line verb key note resolve held open='' stripped pause needs_validation
resolve=${FM_CLASSIFY_RESOLVE_VERB:-$FM_CLASSIFY_RESOLVE_VERB_DEFAULT}
held=${FM_CLASSIFY_CAPTAIN_HELD_VERB:-$FM_CLASSIFY_CAPTAIN_HELD_VERB_DEFAULT}
pause=${FM_CLASSIFY_PAUSED_VERB:-$FM_CLASSIFY_PAUSED_VERB_DEFAULT}
needs_validation=${FM_CLASSIFY_NEEDS_VALIDATION_VERB:-$FM_CLASSIFY_NEEDS_VALIDATION_VERB_DEFAULT}
while IFS= read -r line || [ -n "$line" ]; do
stripped=${line//[[:space:]]/}
[ -n "$stripped" ] || continue
Expand All @@ -1125,7 +1146,7 @@ _fm_status_open_activities_stream() {
[ -n "$open" ] && open="${open}"$'\n'
open="${open}${key}"$'\t'"${verb}"$'\t'"${note}"$'\n'
;;
done|failed|needs-decision|blocked|"$resolve"|"$held")
done|"$needs_validation"|failed|needs-decision|blocked|"$resolve"|"$held")
open=$(_fm_decision_drop "$open" "$key")
[ -n "$open" ] && open="${open}"$'\n'
;;
Expand Down
17 changes: 12 additions & 5 deletions bin/fm-crew-state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# fm-crew-state.sh - deterministic read of a crew's CURRENT state.
#
# Why this exists: state/<id>.status is an append-only, best-effort EVENT LOG.
# Crews append only wake-worthy transitions (done/needs-decision/blocked/paused/failed)
# Crews append only wake-worthy transitions (needs-validation/done/needs-decision/blocked/paused/failed)
# and nothing when they silently resume, so `tail -1` of that log reports the
# last EVENT, not the current STATE. After firstmate resolves a needs-decision
# or blocked and the crew resumes (responds to the gate, the pipeline fixes, it
Expand Down Expand Up @@ -135,6 +135,10 @@ map_log_state() { # <line>
echo paused
return
fi
if status_is_validation_handoff "$1"; then
echo parked
return
fi
case "$(status_line_verb "$1")" in
working) echo working ;;
needs-decision) echo parked ;;
Expand Down Expand Up @@ -370,7 +374,7 @@ nm_ci_checks_state() {
# status` answer was not this crew's own branch, attribution always failed and
# the caller fell straight through to the pane/log fallback below. (The
# PRIMARY cause of the 2026-07 herdr false-surface incidents turned out to be
# a separate bug in bin/fm-watch.sh's stale_is_terminal precedence - see that
# a separate bug in bin/fm-watch.sh's stale-actionable precedence - see that
# file's history - but this cross-branch path was independently confirmed
# dead code and is worth having actually work.)
#
Expand Down Expand Up @@ -562,10 +566,13 @@ if [ "$HAVE_RUN" = 1 ]; then
fi
fi

# Reconcile the status log. A needs-decision/blocked log line that the run-step
# has moved past (anything but a genuinely parked run) is deterministically
# stale: the gate resolved and the run resumed or finished.
# Reconcile the status log. A needs-validation handoff is superseded by any
# attributed validation run. A needs-decision/blocked line that the run-step has
# moved past (anything but a genuinely parked run) is deterministically stale.
case "$LOG_VERB" in
"${FM_CLASSIFY_NEEDS_VALIDATION_VERB:-$FM_CLASSIFY_NEEDS_VALIDATION_VERB_DEFAULT}")
RUN_DETAIL="$RUN_DETAIL${SEP}status-log superseded by validation run"
;;
needs-decision|blocked)
if [ "$RUN_STATE" != parked ]; then
if [ "$RUN_STATE" = working ]; then
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ During no-mistakes' `ci` monitor phase, it also reads the ci step log tail becau
The most recent recognized ci log marker wins, so checks-green monitoring reports done while a later re-arm, failed-check, or issue marker returns the crew to working.
Only when no matching run exists does it consult semantic busy state; exact busy reports working, exact idle permits fallback to a status-log event whose verb maps to a recognized run-state, and unknown or a dead pane stays unknown instead of trusting a stale log.
Decision-only events such as `resolved` never become current state or leak their prose into the current-state detail.
In that status-log fallback, a declared external wait reports the distinct `paused` state with its reason.
In that status-log fallback, a declared external wait reports the distinct `paused` state with its reason, while the generated brief's non-terminal `needs-validation:` implementation handoff reports `parked` until an attributed validation run supersedes it.
The semantic branch reports working only on an exact busy verdict and names the source that produced it; an unknown verdict never becomes working, never permits the status-log fallback, and never becomes a silent idle.
For whole-fleet read-only review, `bin/fm-fleet-snapshot.sh --json` emits schema `fm-fleet-snapshot.v1` from the backlog, task metadata, current crew state, endpoint probes, PR/report pointers, scout reports, bounded current summaries from registered secondmate homes, and secondmate return-channel guidance.
`bin/fm-fleet-view.sh` renders that snapshot as Markdown for humans, while `bin/fm-bearings-snapshot.sh` provides the bounded bearings projection, so both views consume one structured contract instead of reparsing raw fleet files.
Expand Down
3 changes: 2 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,8 @@ FM_WATCH_CYCLE_LOG_MAX_BYTES=262144 # size cap for the arm-owned watcher lifec
FM_WATCH_CYCLE_LOG_KEEP_LINES=1000 # newest complete lifecycle rows considered when the ledger is capped
FM_WATCHER_STALE_GRACE=300 # defaults to FM_GUARD_GRACE; seconds a live watcher lock may have a stale beacon before re-arm errors
FM_SIGNAL_GRACE=30 # seconds to coalesce nearby status and turn-end signals into one wake
FM_CAPTAIN_RE='done:|needs-decision:|blocked:|failed:|PR ready|checks green|ready in branch|merged' # captain-relevant status regex; nonterminal progress verbs remain excluded even when their prose matches
FM_CLASSIFY_NEEDS_VALIDATION_VERB=needs-validation # leading status verb for the non-terminal no-mistakes implementation handoff; firstmate triggers validation on the same worker
FM_CAPTAIN_RE='done:|needs-validation:|needs-decision:|blocked:|failed:|PR ready|checks green|ready in branch|merged' # captain-relevant status regex; routine nonterminal progress verbs remain excluded even when their prose matches
FM_CLASSIFY_PAUSED_VERB=paused # leading status verb for a declared external wait; excluded from FM_CAPTAIN_RE and distinct from blocked
FM_STALE_ESCALATE_SECS=240 # idle seconds before a provably-working stale pane escalates; stale panes whose crew is not provably working surface immediately unless they declare the pause verb
FM_BUSY_TURN_MAX_SECS=3600 # maximum age of a busy pane's latest state/<id>.turn-ended marker, or its state/<id>.meta spawn record before any turn completes, before the same wedge escalation used for a provably-working non-busy stale takes over; inspection-only, never an automatic interrupt or restart; a declared external wait or verified captain-held transfer takes the FM_PAUSE_RESURFACE_SECS recheck below instead
Expand Down
30 changes: 26 additions & 4 deletions tests/fm-brief.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ test_faster_paths_use_configured_authority_without_stacked_review() {
# Pin the specific line the bug lived on: the no-mistakes DOD's no-mistakes
# reference must render as plain prose with no dangling apostrophe artifact.
test_no_mistakes_dod_wording() {
local home id brief
local home id brief handoff_line validation_line final_line handoff_text legacy_handoff_text final_text
home="$TMP_ROOT/wording-home"
mkdir -p "$home/data"
id="brief-wording-b1"
Expand All @@ -345,13 +345,32 @@ test_no_mistakes_dod_wording() {
"no-mistakes DOD must keep direct requirements and exclude generic scaffold boilerplate from --intent"
assert_grep "exclude generic operational, status, delivery, and other scaffold boilerplate unless it is task-specific" "$brief" \
"no-mistakes DOD must exclude non-task-specific scaffold boilerplate from --intent"
assert_grep 'States: working, needs-validation, needs-decision, blocked, paused, done, failed.' "$brief" \
"no-mistakes brief did not advertise its distinct implementation handoff state"
# shellcheck disable=SC2016 # Literal backticks and braces are generated brief output.
handoff_text='append `needs-validation: {summary}` to the status file and stop.'
# shellcheck disable=SC2016 # Literal backticks and braces are generated brief output.
legacy_handoff_text='append `done: {summary}` to the status file and stop.'
# shellcheck disable=SC2016 # Literal backticks and braces are generated brief output.
final_text='append `done: PR {url} checks green` and stop. You are finished.'
assert_grep "$handoff_text" "$brief" \
"no-mistakes DOD did not render the non-terminal implementation handoff"
assert_no_grep "$legacy_handoff_text" "$brief" \
"no-mistakes DOD still reused done: for the pre-validation implementation handoff"
assert_grep "$final_text" "$brief" \
"no-mistakes DOD changed the terminal checks-green completion event"
handoff_line=$(grep -nF "$handoff_text" "$brief" | cut -d: -f1)
validation_line=$(grep -nF 'You drive no-mistakes by responding to its gates' "$brief" | cut -d: -f1)
final_line=$(grep -nF "$final_text" "$brief" | cut -d: -f1)
[ "$handoff_line" -lt "$validation_line" ] && [ "$validation_line" -lt "$final_line" ] \
|| fail "no-mistakes DOD no longer orders implementation handoff, triggered validation, and terminal PR completion"
# The apostrophe in "firstmate's authority check" is now structurally safe
# (no `$(...)` wrapper around the heredoc), so it renders verbatim instead of
# being reworded or escaped away. test_no_heredoc_in_command_substitution
# guards the structure that makes it safe.
assert_grep "firstmate's authority check" "$brief" \
"no-mistakes DOD lost the apostrophe prose that the structural fix makes parse-safe"
pass "fm-brief.sh: no-mistakes DOD keeps its apostrophe prose, now parse-safe"
pass "fm-brief.sh: no-mistakes DOD keeps two-phase handoff/validation semantics and parse-safe prose"
}

test_ship_project_memory_wording() {
Expand Down Expand Up @@ -669,7 +688,7 @@ test_herdr_lab_contract_applies_to_scouts_but_not_secondmates() {
}

test_pause_verb_override_renders_all_brief_scaffolds() {
local home kind id brief
local home kind id brief expected_states
home="$TMP_ROOT/pause-verb-home"
mkdir -p "$home/data"

Expand All @@ -679,18 +698,21 @@ test_pause_verb_override_renders_all_brief_scaffolds() {
ship)
FM_HOME="$home" FM_CLASSIFY_PAUSED_VERB=awaiting \
"$ROOT/bin/fm-brief.sh" "$id" firstmate --mode no-mistakes >/dev/null 2>&1
expected_states='States: working, needs-validation, needs-decision, blocked, awaiting, done, failed.'
;;
scout)
FM_HOME="$home" FM_CLASSIFY_PAUSED_VERB=awaiting \
"$ROOT/bin/fm-brief.sh" "$id" firstmate --scout >/dev/null 2>&1
expected_states='States: working, needs-decision, blocked, awaiting, done, failed.'
;;
secondmate)
FM_HOME="$home" FM_CLASSIFY_PAUSED_VERB=awaiting \
"$ROOT/bin/fm-brief.sh" "$id" --secondmate --no-projects >/dev/null 2>&1
expected_states='States: working, needs-decision, blocked, awaiting, done, failed.'
;;
esac
brief="$home/data/$id/brief.md"
assert_grep "States: working, needs-decision, blocked, awaiting, done, failed." "$brief" \
assert_grep "$expected_states" "$brief" \
"$kind brief did not render the configured pause verb in its states list"
# shellcheck disable=SC2016 # Literal backticks and braces must remain unexpanded.
assert_grep 'Use `awaiting: {why}`' "$brief" \
Expand Down
Loading