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
4 changes: 2 additions & 2 deletions .agents/skills/firstmate-coding-guidelines/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ A new skill is dead weight if nothing loads it.
Every new skill needs its load trigger declared in its description plus an inline `AGENTS.md` pointer in the operating section whose always-loaded rule depends on it, because not every harness surfaces skill descriptions; `agent-skill-trigger-index` holds the complete list.
State the trigger as a condition ("load before X", "load on Y wake"), never as a vague pointer.
Briefs for tasks that touch firstmate's own tracked material should tell the crewmate to load this skill.
`bin/fm-brief.sh`'s `REPO` argument is a caller-supplied string with no reliable signal that it names firstmate's own repo, unlike a project registered in `data/projects.md`, so there is no clean point inside the scaffold to detect this case automatically.
Firstmate adds this skill's load instruction to firstmate-repo briefs by hand instead.
The [`bin/fm-brief.sh` header](../../../bin/fm-brief.sh) owns automatic verification criteria.
Firstmate still adds this skill's load instruction to firstmate-repo briefs by hand.
`CONTRIBUTING.md`'s "Development" section carries the same instruction as a durable reminder.

## Compatibility and enforcement
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ See the [no-mistakes quick start](https://kunchenguid.github.io/no-mistakes/star
Tracked changes to firstmate itself use the risk-based documentation rule above on a feature branch and require an explicit merge approval.
Before making any such change, load the agent-only `firstmate-coding-guidelines` skill (`.agents/skills/firstmate-coding-guidelines/SKILL.md`).
It has the knowledge-placement rules that keep `AGENTS.md` from regrowing after each diet pass.
There is no reliable way for `bin/fm-brief.sh`'s scaffold to detect that a task's repo is firstmate itself, so firstmate adds this skill's load line to firstmate-repo briefs by hand.
The [`bin/fm-brief.sh` header](bin/fm-brief.sh) owns automatic verification criteria; firstmate still adds this skill's load line to firstmate-repo briefs by hand.
A crewmate picking up such a brief should load the skill even if the brief predates this instruction.
When supervising live crewmates, keep firstmate's own long validation or build commands in the background so watcher wakes can still be handled.
Crewmate validation follows the installed no-mistakes version's SKILL.md and live `axi` help instead of duplicating gate mechanics in firstmate docs.
Expand Down
58 changes: 54 additions & 4 deletions bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@
# --herdr-lab is mandatory when the task will issue Herdr lifecycle commands.
# It adds the hard isolation contract backed by bin/fm-herdr-lab.sh.
# The flag must be explicit because {TASK} is filled after scaffolding and the
# caller-supplied repo string cannot reliably identify this repo. Briefs made
# without it carry a loud declaration so an omitted contract cannot be silent.
# caller-supplied repo string cannot be relied on to identify this repo for a
# safety gate. Briefs made without it carry a loud declaration so an omitted
# contract cannot be silent.
# For ship tasks, --mode is REQUIRED and shapes the definition of done. Firstmate
# resolves it per task at intake (AGENTS.md section 7); data/projects.md holds the
# captain's standing posture as context, and this script never reads it:
Expand All @@ -50,6 +51,17 @@
# "# Acceptance criteria" section and creates the append-only evidence ledger at
# data/<task-id>/evidence.jsonl. bin/fm-receipt-check.sh owns the section parser,
# evidence gate, conservative binary risk plan, and validation timing.
# When the repo argument resolves to a directory whose git common dir equals
# this code root's git common dir (any worktree of it counts), the ship scaffold
# appends reserved criterion AC99 as the section's last line; keep it and use
# AC1..AC98 for task criteria. projects/<name> resolves under FM_HOME; unresolved
# names, non-git directories, and other repos get no extra criterion.
# AC99 requires bin/fm-test-run.sh --changed green and
# FM_LINT_JOBS=1 bin/fm-lint.sh clean, recorded as an evidence line with the branch
# head before validation planning. No local full-suite run is required; broad
# regression is owned by the PR GitHub CI per .no-mistakes.yaml.
# For local-only, AC99 drops the CI clause and binds the branch-head evidence to
# reporting "ready in branch" instead of validation planning.
# Ship briefs begin with a worktree-isolation assertion before the branch step.
# --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.
Expand Down Expand Up @@ -436,6 +448,39 @@ fi

REPO=${POS[1]}

# True when the ship task's repo argument resolves to a checkout (main checkout
# or any worktree) of the same git repository as this code root. This is a
# best-effort convenience for pre-filling the AC99 verification criterion, not
# a safety gate: a repo string that does not resolve to a directory - the
# common bare project-name case - simply gets the plain scaffold.
repo_is_firstmate_code_root() {
local dir=$1 dir_common root_common
case "$dir" in
projects/*) dir="$FM_HOME/projects/${dir#projects/}" ;;
esac
[ -d "$dir" ] || return 1
dir_common=$(cd "$dir" 2>/dev/null && common_dir=$(git rev-parse --git-common-dir 2>/dev/null) && cd "$common_dir" 2>/dev/null && pwd -P) || return 1
root_common=$(cd "$FM_ROOT" 2>/dev/null && common_dir=$(git rev-parse --git-common-dir 2>/dev/null) && cd "$common_dir" 2>/dev/null && pwd -P) || return 1
[ -n "$dir_common" ] && [ -n "$root_common" ] && [ "$dir_common" = "$root_common" ]
}

# Reserved acceptance criterion, appended last so task criteria AC1..AC98 never
# collide. The wording follows .no-mistakes.yaml: targeted local verification,
# CI owns broad regression, no local full-suite run.
FIRSTMATE_VERIFICATION_AC=
if repo_is_firstmate_code_root "$REPO"; then
case "$MODE" in
local-only)
# shellcheck disable=SC2016 # single quotes are deliberate: the backticks are literal brief text
FIRSTMATE_VERIFICATION_AC='- AC99: changed tests green via `bin/fm-test-run.sh --changed` and `FM_LINT_JOBS=1 bin/fm-lint.sh` clean, recorded as an evidence line with the branch head before reporting ready in branch; no local full-suite run is required.'
;;
*)
# shellcheck disable=SC2016 # single quotes are deliberate: the backticks are literal brief text
FIRSTMATE_VERIFICATION_AC='- AC99: changed tests green via `bin/fm-test-run.sh --changed` and `FM_LINT_JOBS=1 bin/fm-lint.sh` clean, recorded as an evidence line with the branch head before validation planning; no local full-suite run is required; broad regression is owned by the PR GitHub CI per .no-mistakes.yaml.'
;;
esac
fi

if [ "$HERDR_LAB" -eq 1 ]; then
HERDR_LAB_HELPER=$(shell_quote "$FM_ROOT/bin/fm-herdr-lab.sh")
# shellcheck disable=SC2016 # single quotes are deliberate: these lines are literal brief text whose backtick-wrapped $(...) and "$HERDR_LAB_SESSION" snippets must reach the reading agent verbatim, not expand at scaffold time; only the '"$VAR"' break-outs interpolate.
Expand Down Expand Up @@ -577,7 +622,8 @@ ${ORCHESTRATION_FRONTMATTER:+$ORCHESTRATION_FRONTMATTER}You are a crewmate: an a
{TASK}

# Acceptance criteria
- AC1: {ACCEPTANCE CRITERION}
- AC1: {ACCEPTANCE CRITERION}${FIRSTMATE_VERIFICATION_AC:+
$FIRSTMATE_VERIFICATION_AC}

${ORCHESTRATION_SECTION:+$ORCHESTRATION_SECTION}$HERDR_SECTION

Expand Down Expand Up @@ -642,4 +688,8 @@ if ! FM_HOME="$FM_HOME" FM_DATA_OVERRIDE="$DATA" FM_STATE_OVERRIDE="$STATE" \
exit 1
fi
BRIEF_COMMITTED=1
echo "scaffolded: $BRIEF (ship, mode=$MODE; replace {TASK} and every {ACCEPTANCE CRITERION})"
if [ -n "$FIRSTMATE_VERIFICATION_AC" ]; then
echo "scaffolded: $BRIEF (ship, mode=$MODE; replace {TASK} and every {ACCEPTANCE CRITERION}; AC99 is pre-filled with the firstmate verification criterion and must be kept)"
else
echo "scaffolded: $BRIEF (ship, mode=$MODE; replace {TASK} and every {ACCEPTANCE CRITERION})"
fi
84 changes: 84 additions & 0 deletions tests/fm-brief.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,89 @@ test_completion_boundary_contract_in_briefs() {
pass "fm-brief.sh: ship and scout briefs carry the bounded completion contract"
}

# When the ship task's repo argument resolves to a checkout of the same git
# repository as this code root, the scaffold appends the reserved AC99
# verification criterion (targeted local tests plus lint, CI owns broad
# regression - per .no-mistakes.yaml) as the last criterion; every other repo's
# scaffold stays free of it. fm-receipt-check.sh must parse the result.
test_firstmate_repo_ship_brief_prefills_verification_criterion() {
local home brief filled criteria out status other_repo mode non_git_root
home="$TMP_ROOT/firstmate-ac99-home"
mkdir -p "$home/data"

for mode in no-mistakes direct-PR; do
out=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "brief-fm-ac99-$mode" "$ROOT" --mode "$mode" 2>&1); status=$?
expect_code 0 "$status" "firstmate-repo $mode brief should scaffold"
assert_contains "$out" "AC99 is pre-filled" \
"firstmate-repo scaffold did not announce the pre-filled AC99"
assert_contains "$out" "replace {TASK} and every {ACCEPTANCE CRITERION}" \
"firstmate-repo scaffold dropped the placeholder-replacement instruction"
brief="$home/data/brief-fm-ac99-$mode/brief.md"
# shellcheck disable=SC2016 # Literal backticks must remain unexpanded.
assert_grep '- AC99: changed tests green via `bin/fm-test-run.sh --changed` and `FM_LINT_JOBS=1 bin/fm-lint.sh` clean, recorded as an evidence line with the branch head before validation planning;' "$brief" \
"firstmate-repo AC99 cannot be evidenced before PR creation"
assert_grep 'broad regression is owned by the PR GitHub CI per .no-mistakes.yaml' "$brief" \
"firstmate-repo AC99 did not identify CI as the broad regression owner"
assert_no_grep 'checks-green PR-ready gate' "$brief" \
"firstmate-repo AC99 claimed CI enforcement across all PR paths"
assert_grep 'no local full-suite run is required' "$brief" \
"firstmate-repo AC99 did not exclude local full-suite runs"
assert_no_grep 'CI run URL' "$brief" \
"firstmate-repo AC99 still requires a pre-validation CI receipt"
[ "$(grep -oE '^- AC[0-9]+' "$brief" | tail -1)" = "- AC99" ] \
|| fail "AC99 is not the last acceptance criterion in the firstmate-repo brief"
[ "$(grep -n -- '- AC1:' "$brief" | head -1 | cut -d: -f1)" -lt \
"$(grep -n -- '- AC99:' "$brief" | cut -d: -f1)" ] \
|| fail "AC99 did not appear after the AC1 scaffold line"

filled="$TMP_ROOT/brief-fm-ac99-$mode-filled.md"
sed 's/{ACCEPTANCE CRITERION}/the change works as specified/' "$brief" > "$filled"
"$ROOT/bin/fm-receipt-check.sh" --parse-criteria "$filled" --require AC99 >/dev/null 2>&1 \
|| fail "fm-receipt-check --require AC99 rejected the filled firstmate brief"
criteria=$("$ROOT/bin/fm-receipt-check.sh" --parse-criteria "$filled") \
|| fail "fm-receipt-check could not parse the filled firstmate brief"
printf '%s\n' "$criteria" | cut -f1 | grep -Fx AC1 >/dev/null \
|| fail "parsed criteria lost AC1"
printf '%s\n' "$criteria" | cut -f1 | grep -Fx AC99 >/dev/null \
|| fail "parsed criteria lost AC99"
done

out=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" brief-fm-ac99-lo "$ROOT" --mode local-only 2>&1); status=$?
expect_code 0 "$status" "firstmate-repo local-only brief should scaffold"
brief="$home/data/brief-fm-ac99-lo/brief.md"
# shellcheck disable=SC2016 # Literal backticks must remain unexpanded.
assert_grep '- AC99: changed tests green via `bin/fm-test-run.sh --changed`' "$brief" \
"firstmate-repo local-only brief missing the AC99 verification criterion"
assert_grep 'ready in branch' "$brief" \
"local-only AC99 did not bind evidence to the branch-ready report"
assert_no_grep 'GitHub CI' "$brief" \
"local-only AC99 referenced CI that a local-only delivery never runs"

FM_HOME="$home" "$ROOT/bin/fm-brief.sh" brief-fm-ac99-bare some-proj --mode no-mistakes >/dev/null 2>&1 \
|| fail "bare-name ship brief did not scaffold"
assert_no_grep 'AC99' "$home/data/brief-fm-ac99-bare/brief.md" \
"bare project name gained the firstmate-only AC99 criterion"

other_repo="$TMP_ROOT/unrelated-repo"
mkdir -p "$other_repo"
git -C "$other_repo" init -q
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" brief-fm-ac99-other "$other_repo" --mode no-mistakes >/dev/null 2>&1 \
|| fail "foreign-repo ship brief did not scaffold"
assert_no_grep 'AC99' "$home/data/brief-fm-ac99-other/brief.md" \
"a checkout of a different repository gained the firstmate-only AC99 criterion"

non_git_root="$TMP_ROOT/non-git-code-root"
mkdir -p "$non_git_root"
cp -R "$ROOT/bin" "$non_git_root/bin"
GIT_CEILING_DIRECTORIES="$TMP_ROOT" FM_HOME="$home" FM_ROOT_OVERRIDE="$non_git_root" \
"$non_git_root/bin/fm-brief.sh" brief-fm-ac99-non-git "$non_git_root" --mode no-mistakes >/dev/null 2>&1 \
|| fail "non-git code-root ship brief did not scaffold"
assert_no_grep 'AC99' "$home/data/brief-fm-ac99-non-git/brief.md" \
"physical equality without Git identity gained the firstmate-only AC99 criterion"

pass "fm-brief.sh: firstmate-repo ship briefs pre-fill AC99, other repos stay unchanged"
}

test_script_parses
test_no_heredoc_in_command_substitution
test_help_includes_entire_header
Expand All @@ -1038,5 +1121,6 @@ test_scouting_delegation_section_in_ship_and_scout
test_pause_verb_override_renders_all_brief_scaffolds
test_scout_and_secondmate_load_captain_hold_policy
test_scout_and_secondmate_scaffold
test_firstmate_repo_ship_brief_prefills_verification_criterion
test_concurrent_ship_scaffold_has_one_owner
test_ship_scaffold_rejects_destination_swap_and_retries_cleanly
Loading