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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ Resolve every ship task's concrete delivery mode and `yolo` merge posture at int
Pass the mode explicitly to the brief, and pass both values explicitly to the spawn and any scout promotion; each command refuses to guess the values it consumes.
A current explicit captain instruction wins; otherwise the project's registry entry is the captain's standing posture, and dropping below its rigor needs a reason you can state.
On a `no-mistakes-prod-only` project, classify the task's surface: internal-only tooling, automation, contributor or operator process, and release or submission work ships `direct-PR`, while product-facing, mixed, and uncertain work ships `no-mistakes`; never infer internal-only from file location or project name.
`fm-spawn.sh` refuses `direct-PR` on a project whose CI requires PRs raised via no-mistakes, so choose `no-mistakes` there.
An unregistered project or absent registry resolves to `no-mistakes` with yolo off, and the registration gap goes to the captain.
Record the resulting mode, `yolo` merge posture, and the one-line reason for any deviation in the backlog item note.

Expand Down
44 changes: 35 additions & 9 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
# the explicit mode carries less rigor than the project's standing posture, a
# loud one-line deviation notice is printed and the spawn continues.
# no-mistakes-prod-only is a registry policy rather than a task mode and is
# refused as a flag value.
# refused as a flag value. --mode direct-PR is also refused when the project's
# own checkout carries a workflow whose "PR must be raised via no-mistakes"
# check would fail a directly opened PR; use --mode no-mistakes there.
# Ship/scout launches always supply fm-dod-lib.sh's current worker role scope
# using the same private launch-brief overlay. This never rewrites a project's
# instruction files or a secondmate's charter.
Expand Down Expand Up @@ -573,6 +575,31 @@ case "$EFFORT" in
*) echo "error: --effort must be one of low, medium, high, xhigh, max, ultra" >&2; exit 1 ;;
esac

resolve_project_dir_arg() {
local path=$1
case "$path" in
projects/*) printf '%s/%s\n' "$PROJECTS" "${path#projects/}" ;;
*) printf '%s\n' "$path" ;;
esac
}

# A repository whose CI requires PRs to be raised via no-mistakes fails every
# directly opened PR, so a direct-PR ship spawn there is refused before anything
# is created. The requirement is read from the project's local checkout by the
# check's defining job name, never from the workflow file name, with no network.
# Prints the matching workflow's project-relative path, or nothing.
nm_attestation_workflow() {
local project=$1 wf
for wf in "$project"/.github/workflows/*.yml "$project"/.github/workflows/*.yaml; do
[ -f "$wf" ] || continue
if grep -qF -- 'PR must be raised via no-mistakes' "$wf" 2>/dev/null; then
printf '.github/workflows/%s\n' "${wf##*/}"
return 0
fi
done
return 0
}

# --relaunch reuses an existing task's endpoint, worktree, project, and kind,
# so every axis this block resolves for a fresh spawn instead comes from that
# task's own durable record below. Contradicting it on the command line is a
Expand Down Expand Up @@ -603,6 +630,13 @@ else
exit 1 ;;
*) echo "error: --mode must be one of no-mistakes, direct-PR, local-only (got '$MODE')" >&2; exit 1 ;;
esac
if [ "$MODE" = direct-PR ] && [ -n "${POS[1]:-}" ]; then
NM_REQUIRED_WORKFLOW=$(nm_attestation_workflow "$(resolve_project_dir_arg "${POS[1]}")")
if [ -n "$NM_REQUIRED_WORKFLOW" ]; then
echo "error: this project's $NM_REQUIRED_WORKFLOW requires PRs to be raised via no-mistakes, so a direct-PR pull request is guaranteed to fail that check; spawn with --mode no-mistakes" >&2
exit 1
fi
fi
case "$YOLO" in
on|off) ;;
*) echo "error: --yolo must be on or off (got '$YOLO')" >&2; exit 1 ;;
Expand Down Expand Up @@ -2096,14 +2130,6 @@ resolved_existing_dir() {
cd "$path" && pwd -P
}

resolve_project_dir_arg() {
local path=$1
case "$path" in
projects/*) printf '%s/%s\n' "$PROJECTS" "${path#projects/}" ;;
*) printf '%s\n' "$path" ;;
esac
}

path_is_ancestor_of() {
local ancestor=$1 path=$2
[ -n "$ancestor" ] || return 1
Expand Down
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ A ship brief records its mode as a fixed machine-readable line and the spawn ref
`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.
It is also the one owner of the no-mistakes `--intent` contract those workers follow.
`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.
A `--mode direct-PR` ship spawn is instead refused when the project's own checkout carries a workflow with the `PR must be raised via no-mistakes` check, because a directly opened PR is guaranteed to fail it; the refusal names that workflow and asks for `--mode no-mistakes`.
`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.
Where a no-mistakes pipeline stores evidence in the repo, it publishes that PR-viewable validation evidence to an orphan evidence branch that shares no history with code branches, so it never enters the crew branch or the default branch.
Expand Down
39 changes: 38 additions & 1 deletion tests/fm-task-delivery.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ run_spawn() { # <home> <fakebin> <spawn-args...>
shift 2
FM_ROOT_OVERRIDE='' FM_HOME="$home" \
FM_STATE_OVERRIDE="$home/state" FM_DATA_OVERRIDE="$home/data" \
FM_PROJECTS_OVERRIDE="$TMP_ROOT/projects-unused" FM_CONFIG_OVERRIDE="$home/config" \
FM_PROJECTS_OVERRIDE="${SPAWN_PROJECTS_DIR:-$TMP_ROOT/projects-unused}" FM_CONFIG_OVERRIDE="$home/config" \
FM_SPAWN_NO_GUARD=1 FM_BACKEND=tmux PATH="$fakebin:$PATH" \
"$SPAWN" "$@" 2>&1
}
Expand Down Expand Up @@ -794,7 +794,44 @@ EOF
pass "fm-spawn: every legacy worker receives scoped role instructions without changing project or primary instructions"
}

test_spawn_refuses_direct_pr_where_ci_requires_no_mistakes() {
local rec home proj fakebin out status
rec=$(make_home attested)
IFS='|' read -r home proj fakebin <<EOF
$rec
EOF
write_brief "$home" attested-a1 direct-PR
out=$(run_spawn "$home" "$fakebin" attested-a1 "$proj" claude --mode direct-PR --yolo off)
assert_not_contains "$out" "requires PRs to be raised via no-mistakes" "guard fired without the workflow"

mkdir -p "$proj/.github/workflows"
printf 'name: gate\njobs:\n attest:\n name: PR must be raised via no-mistakes\n runs-on: ubuntu-latest\n' \
> "$proj/.github/workflows/attestation-gate.yml"

out=$(run_spawn "$home" "$fakebin" attested-a1 "$proj" claude --mode direct-PR --yolo off)
status=$?
[ "$status" -ne 0 ] || fail "direct-PR should be refused when the workflow is present"
assert_contains "$out" "attestation-gate.yml" "refusal did not name the workflow"
assert_contains "$out" "--mode no-mistakes" "refusal did not name the fix"
assert_absent "$home/state/attested-a1.meta" "refused spawn wrote task metadata"

write_brief "$home" attested-a4 direct-PR
out=$(SPAWN_PROJECTS_DIR=${proj%/*} run_spawn "$home" "$fakebin" attested-a4 "projects/${proj##*/}" claude --mode direct-PR --yolo off)
status=$?
[ "$status" -ne 0 ] || fail "direct-PR should be refused for the projects/<name> spelling"
assert_contains "$out" "attestation-gate.yml" "projects/<name> spelling bypassed the guard"

write_brief "$home" attested-a2 no-mistakes
out=$(run_spawn "$home" "$fakebin" attested-a2 "$proj" claude --mode no-mistakes --yolo off)
assert_not_contains "$out" "requires PRs to be raised via no-mistakes" "no-mistakes mode was refused"
write_brief "$home" attested-a3 local-only
out=$(run_spawn "$home" "$fakebin" attested-a3 "$proj" claude --mode local-only --yolo off)
assert_not_contains "$out" "requires PRs to be raised via no-mistakes" "local-only mode was refused"
pass "fm-spawn: direct-PR is refused only where CI requires PRs raised via no-mistakes"
}

test_spawn_refreshes_legacy_worker_roles
test_spawn_refuses_direct_pr_where_ci_requires_no_mistakes
test_ship_spawn_requires_a_valid_delivery_contract
test_scout_and_secondmate_refuse_delivery_flags
test_spawn_refuses_a_brief_mode_mismatch
Expand Down
Loading