From 6f0caf8de8081ca1df52d3c0d36b062f4bae2513 Mon Sep 17 00:00:00 2001 From: NewAiCoder Date: Sat, 26 Sep 2026 23:44:59 -0400 Subject: [PATCH 1/3] fix(spawn): refuse direct-PR where CI requires PRs raised via no-mistakes --- bin/fm-spawn.sh | 28 +++++++++++++++++++++++++++- tests/fm-task-delivery.test.sh | 31 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index 4cf13b28330..83fa807ddbb 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -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. @@ -573,6 +575,23 @@ case "$EFFORT" in *) echo "error: --effort must be one of low, medium, high, xhigh, max, ultra" >&2; exit 1 ;; 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 @@ -603,6 +622,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 "${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 ;; diff --git a/tests/fm-task-delivery.test.sh b/tests/fm-task-delivery.test.sh index aaa52e9b3b5..10e4a0ff6a5 100755 --- a/tests/fm-task-delivery.test.sh +++ b/tests/fm-task-delivery.test.sh @@ -794,7 +794,38 @@ 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 < "$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-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 From a64862cf001758b565fc01047eb7a31320b849b5 Mon Sep 17 00:00:00 2001 From: NewAiCoder Date: Sat, 26 Sep 2026 23:50:03 -0400 Subject: [PATCH 2/3] no-mistakes(review): Resolve projects/ alias before attestation guard --- bin/fm-spawn.sh | 18 +++++++++--------- tests/fm-task-delivery.test.sh | 8 +++++++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index 83fa807ddbb..d19c00cae44 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -575,6 +575,14 @@ 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 @@ -623,7 +631,7 @@ else *) 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 "${POS[1]}") + 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 @@ -2122,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 diff --git a/tests/fm-task-delivery.test.sh b/tests/fm-task-delivery.test.sh index 10e4a0ff6a5..080b789419f 100755 --- a/tests/fm-task-delivery.test.sh +++ b/tests/fm-task-delivery.test.sh @@ -64,7 +64,7 @@ run_spawn() { # 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 } @@ -815,6 +815,12 @@ EOF 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/ spelling" + assert_contains "$out" "attestation-gate.yml" "projects/ 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" From 8bd48a3c7d1268f273e9b73482990ab584144767 Mon Sep 17 00:00:00 2001 From: NewAiCoder Date: Sat, 26 Sep 2026 23:53:11 -0400 Subject: [PATCH 3/3] no-mistakes(document): Document direct-PR refusal for no-mistakes-required repos --- AGENTS.md | 1 + docs/architecture.md | 1 + 2 files changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index a6af6bc6b3f..58bbf17935b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/docs/architecture.md b/docs/architecture.md index 9a7b97ab5aa..7ec33f1987c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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//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.