Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2cc8cf0
fix(bin): refuse a no-mistakes run that kept no change as validated
Sep 8, 2026
e45f044
no-mistakes(review): refuse unverifiable coarse completed run and dro…
Sep 8, 2026
6b6cdd7
no-mistakes(review): keep coarse completed done only on ledger PR evi…
Sep 8, 2026
6237998
no-mistakes(review): drop unreachable pending_pr plumbing from ledger…
Sep 8, 2026
5904a74
no-mistakes(document): document vacuous-run and coarse PR-evidence cr…
Sep 8, 2026
ea80b74
fix(bin): omit a GitHub merge strategy for merge-queue branches
Aug 24, 2026
62a8454
no-mistakes(review): report queued GitHub merges and refuse forge-dec…
Aug 24, 2026
0964b69
no-mistakes(review): state-proven merge verdicts and drop unreachable…
Aug 24, 2026
4dbd805
no-mistakes(review): tighten forge-report assertion and correct verif…
Aug 24, 2026
2334e95
no-mistakes(document): note forge-decides merge outcome report in arc…
Aug 24, 2026
dcd2ca6
fix(bin): refuse a forge-decides method combined with an explicit str…
Aug 27, 2026
a99edf6
no-mistakes(review): name forge-decides retry in queue-governed merge…
Aug 30, 2026
d2a649d
no-mistakes(review): apply queue retry echo guard to every rules outcome
Aug 30, 2026
aa914bb
no-mistakes(review): derive queue retry echo guard from caller arguments
Aug 30, 2026
7dd5908
no-mistakes(document): record queue retry echo guard in verification …
Aug 30, 2026
8269467
fix(bin): consolidate reliable PR delivery
Sep 8, 2026
69b774c
no-mistakes(review): Remove intermediate queue aliases and unused tea…
Sep 8, 2026
84d9c5b
no-mistakes(document): Correct queue syntax documentation and stale c…
Sep 8, 2026
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
7 changes: 7 additions & 0 deletions .agents/skills/project-management/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ Initialization configures the local gate and does not vendor a no-mistakes skill
Do not create a commit merely because initialization ran.
If doctor reports an environment, authentication, or daemon problem, resolve that blocker before dispatching work and never restart the shared daemon from a project operation.

The repository that a pipeline opens its PR against is the clone's own `origin` remote, and `--fork-url` only chooses where branches are pushed while the PR still targets `origin`.
A clone that contributes upstream therefore keeps `origin` on the parent repository on purpose and pairs it with `--fork-url`, which is that posture working as intended and not a target to correct; the paragraph below applies only when a project's registered delivery target is wrong for the repository the fleet is asked to land work in.
For that case the delivery target is changed by repointing that clone's `origin` and running `no-mistakes init` again, which also refreshes the gate mirror to the newly registered target; `no-mistakes status` then reports the new target and keeps it across a daemon restart.
Which repository a given project should deliver into is the captain's decision, so confirm the intended target before repointing anything and follow any contribution workflow the project documents for itself.
Editing the gate mirror's own remote URL is not a supported way to change the target: it leaves the registration and the mirror's tracking refs pointing at the old repository, so work keeps landing in the previous target and a rebase can silently resolve against a base the new target never had.
For an autonomous GitHub task, `bin/fm-pr-check.sh` independently reads live push permission for the URL-derived repository before accepting the resulting PR as ready, so an accidental read-only target is reported instead of looking landable.

## Remove

Project removal is destructive.
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ Require the matching `resolved` event, forbid `--yes`, and require the worker to
Resume fleet supervision immediately after the decision lands.

Judge validation by the currently attributed run step through `bin/fm-crew-state.sh`, not by shell liveness or the last status event.
Running, fixing, or CI states remain working; parked approval or fix-review states require the worker to follow the active gate help; passed or checks-passed is done; failed or cancelled is failed exactly as `bin/fm-crew-state.sh` prints it - only that state line reclassifies an orphaned ci monitor after green checks as held-for-merge done, or a terminal failed record with the daemon unreachable as unknown, never the raw run record.
Running, fixing, or CI states remain working; parked approval or fix-review states require the worker to follow the active gate help; passed or checks-passed is done; failed or cancelled is failed exactly as `bin/fm-crew-state.sh` prints it - only that state line reclassifies an orphaned ci monitor after green checks as held-for-merge done, a terminal failed record with the daemon unreachable as unknown, or a successful run that skipped every mandatory delivery phase as failed, never the raw run record.
A worker hand-editing, committing, aborting, or restarting during an active validation run duplicates pipeline ownership outside the supersession sequence above; steer it back to the gate response flow.
The worker reports the PR when CI first becomes green rather than waiting for merge monitoring to finish.

Expand Down
84 changes: 79 additions & 5 deletions bin/fm-crew-state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,18 @@
# coarse runs-ledger fallback (no steps table, no ci log), a terminal
# FAILED record whose daemon an explicit probe proves down reads unknown,
# never failed: an instrument failure must not read as work failure
# (nm_daemon_probe_down).
# (nm_daemon_probe_down). Conversely a terminal SUCCESS run whose steps
# table shows every mandatory delivery phase skipped reads failed, never
# done: when a branch's whole diff vanishes into the rebase base,
# no-mistakes still records the run completed although nothing was
# reviewed, tested, pushed, or opened as a PR, and validity must not be
# inferred from that word alone (nm_run_skipped_every_mandatory_step;
# 2026-09-08 fm-nm-depot-livraison-non-modifiable incident). The coarse
# fallback carries no steps table, and the runs ledger names no run id to
# fetch one with, so its only delivery evidence is the row's own PR URL:
# a COMPLETED row carrying one proves push and pr ran and reads done,
# while a bare COMPLETED row proves nothing either way and reads
# unknown-unverified rather than validated.
# 3. Reconcile the status log: if its last line says needs-decision/blocked but
# the run-step shows the run moved on, the log is deterministically stale and
# is flagged superseded. A genuinely parked run plus a needs-decision log
Expand Down Expand Up @@ -453,6 +464,43 @@ nm_reclassify_failed_run_as_held_green() {
return 0
}

# The delivery phases a no-mistakes ship run must actually execute before its
# result may be read as validated. `intent` and `rebase` are excluded: they
# prepare a run rather than validate or deliver it.
NM_MANDATORY_STEPS="review test document lint push pr ci"

# 0 when the steps table proves a terminal-success run validated and delivered
# nothing: every phase in NM_MANDATORY_STEPS is present and `skipped`. This is
# the vacuous-pass shape (2026-09-08 fm-nm-depot-livraison-non-modifiable): a
# branch whose commits are already contained in the rebase base loses its whole
# diff, and no-mistakes then logs `empty diff after rebase, skipping remaining
# steps` and still records the run `completed`. Nothing was reviewed, tested,
# documented, linted, pushed, or opened as a PR, so the word alone is not
# validation. Positive evidence is required in both directions: an absent table,
# or any mandatory row that is missing or not `skipped`, is not this shape and
# leaves the run's own reported result untouched.
nm_run_skipped_every_mandatory_step() {
local rows want
rows=$(nm_steps_rows)
[ -n "$rows" ] || return 1
for want in $NM_MANDATORY_STEPS; do
printf '%s\n' "$rows" \
| grep -qE "^[[:space:]]*$want,[[:space:]]*\"?skipped\"?[[:space:]]*," || return 1
done
return 0
}

# Reclassify a terminal SUCCESS run as failed when
# nm_run_skipped_every_mandatory_step matches, so a run that lost its change
# never reads as shippable. The branch still holds whatever the worker
# committed; what is refused is calling that outcome validated.
nm_reclassify_vacuous_success_as_failed() {
nm_run_skipped_every_mandatory_step || return 1
RUN_STATE=failed
RUN_DETAIL="not validated: run kept no change - review, test, document, lint, push, pr and ci were all skipped"
return 0
}

# 0 when an explicit probe proves the shared daemon down: `no-mistakes daemon
# status` is the canonical down-probe (the same one fm-brief.sh hands crews
# before a blocked append) and exits non-zero when the daemon is not running.
Expand Down Expand Up @@ -559,6 +607,7 @@ HAVE_RUN=0
# the TOON field parsing entirely for this crew.
RUN_SOURCE=full
COARSE_STATUS=""
COARSE_PR=""
# Scouts and secondmates never drive a no-mistakes validation of their own
# worktree, so skip the lookup for them and read state from pane/log directly.
if [ "$KIND" = ship ] && [ -n "$CREW_BRANCH" ] && command -v no-mistakes >/dev/null 2>&1; then
Expand All @@ -579,7 +628,11 @@ if [ "$KIND" = ship ] && [ -n "$CREW_BRANCH" ] && command -v no-mistakes >/dev/n
# `[ -n "$RUN_OUT" ]`: an empty/timed-out primary call means the CLI
# itself did not respond, so retrying it immediately with a second
# bounded call would just double the wait for no better answer.
COARSE_STATUS=$(fm_nm_runs_status_for_worktree "$WT" "$CREW_BRANCH" "$(nm_runs_list)")
coarse_row=$(fm_nm_runs_status_for_worktree "$WT" "$CREW_BRANCH" "$(nm_runs_list)")
COARSE_STATUS=${coarse_row%% *}
case "$coarse_row" in
*' '*) COARSE_PR=${coarse_row#* } ;;
esac
if [ -n "$COARSE_STATUS" ]; then
HAVE_RUN=1
# A branch-matching answer the strict rule rejected is this branch's
Expand Down Expand Up @@ -612,7 +665,22 @@ if [ "$HAVE_RUN" = 1 ]; then
# distinction, so a real gate is never silently missed.
case "$COARSE_STATUS" in
running) RUN_STATE=working; RUN_DETAIL="validating (background run)" ;;
completed) RUN_STATE="done"; RUN_DETAIL="run completed" ;;
completed)
# A terminal ledger word is not a verdict on its own here: the
# vacuous-pass shape the full path refuses
# (nm_run_skipped_every_mandatory_step) is recorded `completed` too,
# and this path has neither a steps table nor a run id to fetch one
# with. The row's PR URL is the ledger's own positive delivery
# evidence - a run that skipped push and pr has none - so a completed
# row that carries one is a real delivery and keeps its done verdict,
# while a bare completed row cannot be told from the vacuous shape
# and is reported unverified rather than validated.
if [ -n "$COARSE_PR" ]; then
RUN_STATE="done"; RUN_DETAIL="run completed: $COARSE_PR"
else
RUN_STATE=unknown
RUN_DETAIL="last ledger record completed with no PR; nothing proves a delivery phase ran - unverified"
fi ;;
failed)
# The ledger row is terminal but the coarse path has no steps table
# and no ci log, so the orphaned-monitor shape cannot be recognized
Expand All @@ -638,7 +706,10 @@ if [ "$HAVE_RUN" = 1 ]; then

if [ -n "$outcome" ]; then
case "$outcome" in
passed) RUN_STATE="done"; RUN_DETAIL="run passed: PR merged/closed" ;;
passed)
if nm_reclassify_vacuous_success_as_failed; then :; else
RUN_STATE="done"; RUN_DETAIL="run passed: PR merged/closed"
fi ;;
checks-passed) RUN_STATE="done"; RUN_DETAIL="checks green: PR ready for review" ;;
failed)
if nm_reclassify_failed_run_as_held_green; then :; else
Expand Down Expand Up @@ -666,7 +737,10 @@ if [ "$HAVE_RUN" = 1 ]; then
case "$status" in
ci) RUN_STATE=working; RUN_DETAIL="ci running" ;;
running|fixing) RUN_STATE=working; RUN_DETAIL="validating ($status)" ;;
completed) RUN_STATE="done"; RUN_DETAIL="run completed" ;;
completed)
if nm_reclassify_vacuous_success_as_failed; then :; else
RUN_STATE="done"; RUN_DETAIL="run completed"
fi ;;
failed)
if nm_reclassify_failed_run_as_held_green; then :; else
RUN_STATE=failed; RUN_DETAIL="run failed"
Expand Down
14 changes: 11 additions & 3 deletions bin/fm-nm-run-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,12 @@ fm_nm_run_is_pipeline_owned_active() { # <toon-output>
# --limit N` listing (plain text, no run id, no quoting, newest-first, columns
# "<status> <branch> <short-sha> <date> [<pr-url>]"; the `axi` surface has no
# runs-listing subcommand - verified against the installed CLI). Prints the
# status word of the branch's CURRENT run row, or nothing when the ledger
# cannot prove attribution. When optional expected head $4 is supplied, its
# branch's CURRENT run row as "<status>[ <pr-url>]" - the status word alone
# when the row carries no PR column - or nothing when the ledger cannot prove
# attribution. Callers that only want the word read the first field. The PR
# column is carried because it is the ledger's ONLY positive evidence that a
# row's run actually pushed a branch and opened a PR, which no status word can
# establish on its own. When optional expected head $4 is supplied, its
# abbreviated commit identity must match the newest row. The branch's NEWEST
# row alone decides; older rows are history and never answer for the present:
# - newest row's head resolves and matches the worktree (fm_nm_head_matches_worktree):
Expand Down Expand Up @@ -205,7 +209,11 @@ fm_nm_runs_status_for_worktree() { # <worktree> <branch> <runs-list-output> [ex
fi
if [ -n "$(fm_nm_resolve_commit "$wt" "$sha")" ]; then
if fm_nm_head_matches_worktree "$wt" "$sha"; then
printf '%s' "$st"
if [ -n "$pr" ]; then
printf '%s %s' "$st" "$pr"
else
printf '%s' "$st"
fi
fi
return 0
fi
Expand Down
37 changes: 37 additions & 0 deletions bin/fm-pr-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
# live only in a private sidecar and are never interpolated into shell source.
# A GitHub pull request URL and a GitLab merge request URL are both accepted,
# including a merge request on a self-hosted GitLab instance.
# A GitHub task carrying yolo=on intends Firstmate to land the PR itself, so the
# URL-derived repository must report live push permission before the PR can be
# recorded as ready. A read-only upstream is refused instead of presenting a
# green-looking PR that this home has no authority to land. Tasks with yolo off
# retain the contribution workflow in which an upstream maintainer lands work.
# Usage: fm-pr-check.sh <task-id> <pr-url>
set -eu

Expand Down Expand Up @@ -43,6 +48,38 @@ if [ ! -f "$META" ] || [ -L "$META" ] || [ "$(fm_pr_file_link_count "$META")" !=
exit 1
fi

github_verify_autonomous_target_writable() {
local yolo permission
[ "$PROVIDER" = github ] || return 0
yolo=$(grep '^yolo=' "$META" | tail -1 | cut -d= -f2- || true)
[ "$yolo" = on ] || return 0
command -v gh-axi >/dev/null 2>&1 || {
echo "error: verifying an autonomous GitHub delivery target requires gh-axi on PATH" >&2
return 1
}
if ! permission=$(gh-axi api "/repos/$FM_PR_OWNER/$FM_PR_REPO" \
--jq '.permissions.push' 2>/dev/null); then
printf 'error: could not verify live write permission for autonomous delivery target %s/%s\n' \
"$FM_PR_OWNER" "$FM_PR_REPO" >&2
return 1
fi
case "$permission" in
true) return 0 ;;
false)
printf 'error: refusing autonomous delivery to read-only GitHub repository %s/%s; choose a writable intended target before opening or accepting the PR\n' \
"$FM_PR_OWNER" "$FM_PR_REPO" >&2
return 1
;;
*)
printf 'error: could not verify live write permission for autonomous delivery target %s/%s\n' \
"$FM_PR_OWNER" "$FM_PR_REPO" >&2
return 1
;;
esac
}

github_verify_autonomous_target_writable || exit 1

# A prior exact merged result may have queued its durable wake immediately
# before interruption.
# Finish only its identity-bound receipt before publishing a replacement poll.
Expand Down
Loading
Loading