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
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ state/ volatile runtime signals; gitignored
<id>.pr-poll private validated data sidecar for the byte-static PR merge poll
<id>.pr-poll-registration private transactional provenance record binding the task, canonical metadata identity, sidecar, and static poll publication
<id>.pr-poll-retirement private identity-bound crash-recovery receipt for one exact validated merged result; removed after its poll artifacts retire
<id>.pr-poll-merge-notified canonical PR identity of the last merge outcome delivered for this task; bin/fm-pr-lib.sh owns the marker format and identity mechanics, while bin/fm-merge-outcome-lib.sh owns locked publication, duplicate suppression, and replacement; removed by teardown
x-watch.check.sh generated X-mode relay poll shim; present only when opted in (section 14)
pending-replies/ parent-owned secondmate pending-reply records (correlation id, delivery vs reply, recovery, escalation); fm-pending-reply-lib.sh
procevent/ registered process-to-event sources, one private record per canonical source id; written by bin/fm-procevent.sh or an adapter through the shared registration publisher, and their presence alone keeps supervision required (section 13)
Expand Down Expand Up @@ -326,7 +327,7 @@ Delivery mode and `yolo` are orthogonal.
Never merge a red PR under either setting; destructive, irreversible, and security-sensitive merges still escalate.
Without a current explicit captain instruction that states the concrete merge, that default stands, and standing `yolo` cannot authorize a red merge; section 1 owns when such an instruction overrides a Firstmate-written standing rule within its exact scope.
Load `ask-user-authority` before deciding any ask-user finding; the implementation worker never answers its own finding.
Use `bin/fm-pr-merge.sh` for every task PR merge so merge metadata is recorded, and use `bin/fm-merge-local.sh` for approved local-only landing; never call a lower-level merge command around their guards.
Use `bin/fm-pr-merge.sh` for every task PR merge so merge metadata is recorded and an unproved merge is refused instead of reported as landed, and use `bin/fm-merge-local.sh` for approved local-only landing; never call a lower-level merge command around their guards.
After an autonomous merge, give the captain a one-line full-URL or local-main outcome.

### Validate
Expand Down Expand Up @@ -354,7 +355,7 @@ For ordinary findings from any No-Mistakes tier, steer the original worker to re
When a finding invalidates a receipt or acceptance claim, use the receipt checker owner to record it before returning branch custody.
After the original worker's fix, return high-risk work to full validation with the updated receipts and delta context.

Judge validation by the current-code-matched run step through `bin/fm-crew-state.sh`, not by shell liveness or the last status event.
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.
A worker hand-editing, committing, aborting, or restarting during an active validation run duplicates pipeline ownership outside the supersession or ordinary-finding custody-return sequences 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
3 changes: 2 additions & 1 deletion bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,8 @@ Report only true captain-relevant outcomes or a declared external wait by append
\`echo "{state}: {one short line}" >> $STATUS_FILE\`
States: working, needs-decision, blocked, $PAUSED_VERB, done, failed.
Use \`$PAUSED_VERB: {why}\` (distinct from \`blocked:\`) only when your domain is deliberately idling on a known external wait you expect to clear on its own; use \`blocked:\` when you are stuck and need firstmate to act.
Use this only for material phase changes, a captain decision, a real blocker, a failure, or work ready for review.
Use this only for material phase changes, a captain decision, a real blocker, a failure, work ready for review, or work you landed.
Work you landed includes a merge you performed yourself under standing merge authority and one the captain merged on the forge: under that authority nothing is ever \"ready for review\", so a landed merge that goes unreported reaches the captain as silence.
This is also how you return the answer to a marked from-firstmate request above.
A marked request requires one correlated answer after the work; it does not require a separate receipt or start acknowledgement.
Never append \`working:\` merely to acknowledge receipt or announce that a marked request has started.
Expand Down
36 changes: 19 additions & 17 deletions bin/fm-crew-state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@
# still does not describe the crew's current state as it resumes, fixes, or
# re-validates. This helper never infers the current state from a tail of the log:
# it reads the authoritative source (a
# no-mistakes run-step attributed to this crew's branch and current code
# identity, with a completed receipt allowing a proven pipeline descendant,
# else the pane busy-signature) and reconciles the possibly-stale log against
# it.
# no-mistakes run-step attributed under bin/fm-nm-run-lib.sh's contract, with a
# completed receipt allowing a proven pipeline descendant, else the pane
# busy-signature) and reconciles the possibly-stale log against it.
#
# The determinism lives entirely here - only run-step / pane / log reads plus
# fixed mapping logic, no heuristics and no LLM. Output is one stable, parseable,
Expand All @@ -22,14 +21,8 @@
#
# Logic, in order:
# 1. Resolve worktree + backend target + kind from state/<id>.meta.
# 2. Matching no-mistakes run for this crew's branch AND current code identity,
# active or terminal (from `axi status`, or the coarse `no-mistakes runs`
# fallback)? Branch name alone is not enough: a historical run on a reused
# branch whose head was rewritten or diverged must not be attributed.
# A run matches when its head equals the worktree HEAD, or the worktree HEAD
# is an ancestor of the run head (pipeline fix commits advanced the run on
# the same line of history). Local work that advanced past the run head, or
# diverged from it, invalidates attribution.
# 2. Attribute an active or terminal no-mistakes run under the branch, head,
# pipeline-custody, and newest-first rules owned by bin/fm-nm-run-lib.sh.
# The run-step is AUTHORITATIVE: running/fixing -> working, ci -> working,
# awaiting_approval/fix_review -> parked (with gate findings), terminal
# passed/checks-passed -> done, failed/cancelled -> failed. EXCEPT: while
Expand Down Expand Up @@ -254,7 +247,7 @@ crew_busy_verdict() { # <target>

# --- no-mistakes run lookup (authoritative when a run matches this branch) --
# trim, strip_quotes, the bounded nm_run call, nm_field's TOON parse, and the
# branch+head attribution rule below are thin wrappers over the ONE owner in
# attribution helpers below are thin wrappers over the ONE owner in
# bin/fm-nm-run-lib.sh, shared with fm-teardown.sh's pre-teardown run abort.

trim() { fm_nm_trim "$@"; }
Expand Down Expand Up @@ -411,6 +404,10 @@ nm_runs_status_for_branch() { # <branch>
# Same code-identity rule as axi status: skip a same-branch row whose
# short-sha does not match this worktree (rewritten or advanced tip).
if ! nm_coarse_head_matches_worktree "$sha"; then
# An UNRESOLVABLE head is unknown attribution, not a proven
# mismatch. Stop instead of surfacing an older, superseded row;
# the caller's pane/log fallback can answer without misattribution.
fm_nm_head_resolvable "$WT" "$sha" || return 0
continue
fi
printf '%s' "$st"
Expand Down Expand Up @@ -453,12 +450,17 @@ if [ "$KIND" = ship ] && [ "$MODE" = no-mistakes ] && [ -n "$CREW_BRANCH" ] && c
RUN_OUT=$(nm_run axi status)
if [ -n "$RUN_OUT" ]; then
run_branch=$(strip_quotes "$(nm_field branch)")
if [ -n "$run_branch" ] && [ "$run_branch" = "$CREW_BRANCH" ] && nm_run_head_matches_worktree; then
# Head equality, or the pipeline-owned-active exemption: while the
# pipeline owns this branch, the daemon's own branch attribution is
# authoritative and the lane head need not be a git object here
# (fm_nm_run_is_pipeline_owned_active in bin/fm-nm-run-lib.sh).
if [ -n "$run_branch" ] && [ "$run_branch" = "$CREW_BRANCH" ] \
&& { nm_run_head_matches_worktree || fm_nm_run_is_pipeline_owned_active "$RUN_OUT"; }; then
HAVE_RUN=1
else
# The active-or-most-recent run is for another branch, or same branch with
# a rewritten/diverged head (the CLI is alive and answered; only the
# attribution missed) - try the coarse fallback.
# The active-or-most-recent run is for another branch, or its same-branch
# attribution failed (the CLI is alive and answered) - try the coarse
# fallback.
# Deliberately nested inside `[ -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
Expand Down
148 changes: 148 additions & 0 deletions bin/fm-merge-outcome-lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env bash
# Shared durable, supervisor-facing outcome publication for a confirmed merge.
#
# Both a merge performed by this home and a merge detected by its existing poll
# use this operation, so neither outcome depends on an agent remembering it.
# This operation publishes the poll's local actionable row; the watcher
# immediately delivers that row as observation handling, not a second outcome
# path.
#
# The destination is the home's role, never the caller's choice:
# - a secondmate home reports upward to its parent on the same reply channel
# a remote mate's parent-replies ingest and a local mate's parent status
# log already carry, in the same "<state> [key=<slug>]: <note>" shape the
# charter contract defines;
# - a main home reports to the captain through the durable wake queue.
# A poll observed in a secondmate home also receives a local durable wake after
# the upward write, so the mate can handle its own poll observation.
# No new state file and no new transport are involved.
#
# Normal operation deduplicates the task's latest canonical PR identity through
# the merge-notification marker owned by bin/fm-pr-lib.sh. Main-home wake keys
# also include that PR identity so distinct PRs for a reused task remain
# distinct in queue presentation. The outcome is published before the marker
# is committed, so a failed commit stays eligible for at-least-once retry and
# may rarely duplicate rather than leave a merge silent.
#
# The main-home row is an ordinary check-kind wake, which every supervision
# actor (including the OMP supervision branch's dispatch classifier) already
# treats as main-owned, so a merge outcome never routes to the branch.
#
# Sourced by bin/fm-pr-merge.sh, bin/fm-watch.sh, and tests. No side effects on
# source beyond its sourced libraries.

_FM_MERGE_OUTCOME_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=bin/fm-pr-lib.sh
. "$_FM_MERGE_OUTCOME_LIB_DIR/fm-pr-lib.sh"
# shellcheck source=bin/fm-secondmate-parent-lib.sh
. "$_FM_MERGE_OUTCOME_LIB_DIR/fm-secondmate-parent-lib.sh"

# The secondmate identity of the home reporting, or non-zero when this home is
# a main home (1) or carries an unusable identity marker (2). The marker's
# contract is the seeded .fm-secondmate-home identity (bin/fm-home-seed.sh).
fm_merge_outcome_home_id() { # <home>
local home=$1 marker id
marker="$home/.fm-secondmate-home"
if [ ! -e "$marker" ] && [ ! -L "$marker" ]; then
return 1
fi
[ -f "$marker" ] && [ ! -L "$marker" ] || return 2
[ "$(wc -c < "$marker")" -eq "$(LC_ALL=C tr -d '\0' < "$marker" | wc -c)" ] || return 2
id=$(cat "$marker" 2>/dev/null) || return 2
fm_pr_task_id_valid "$id" || return 2
printf '%s\n' "$id"
}

# Append <line> to <path> unless that exact line is already there, so a repeat
# report of the same merge cannot duplicate it.
fm_merge_outcome_append_once() { # <path> <line>
local path=$1 line=$2
[ ! -L "$path" ] || return 1
mkdir -p "$(dirname "$path")" || return 1
if grep -Fqx -- "$line" "$path" 2>/dev/null; then
return 0
fi
printf '%s\n' "$line" >> "$path"
}

# shellcheck disable=SC2034 # Public result consumed by sourcing callers.
FM_MERGE_OUTCOME_ALREADY_RECORDED=false

# fm_merge_outcome_report <home> <state> <task-id> <pr-url> <origin>
#
# <origin> says who observed the merge, because that decides whether the
# existing poll path also needs a local wake:
# self - this home performed the merge.
# poll - this home's merge poll detected the merge, so the canonical outcome
# also wakes this home after any upward hop needed by a secondmate.
#
# Returns 0 when the outcome is recorded (or already was), 2 on an invalid
# request, 3 when this home's own role or parent binding cannot be read well
# enough to say where the outcome belongs, and 1 on any other failure to
# record. A caller that has already merged must report a non-zero return rather
# than treat it as success: the merge landed and the record did not.
fm_merge_outcome_report() { # <home> <state> <task-id> <pr-url> <origin>
local home=$1 state=$2 id=$3 url=$4 origin=$5
local self='' self_rc=0 destination='' line lock status=0
local provider host path number
# The wake helpers read these as globals; scoping them here points one call at
# this state directory without touching the caller's own queue settings.
# shellcheck disable=SC2034 # Sourced wake helpers consume these scoped globals.
local STATE=$state FM_WAKE_QUEUE="$state/.wake-queue" FM_WAKE_QUEUE_LOCK="$state/.wake-queue.lock"
FM_MERGE_OUTCOME_ALREADY_RECORDED=false
case "$origin" in self|poll) ;; *) return 2 ;; esac
fm_pr_task_id_valid "$id" || return 2
fm_pr_url_parse "$url" || return 2
provider=$FM_PR_PROVIDER
host=$FM_PR_HOST
path=$FM_PR_PATH
number=$FM_PR_NUMBER
[ -d "$state" ] && [ ! -L "$state" ] || return 1

if self=$(fm_merge_outcome_home_id "$home"); then
fm_secondmate_parent_record_parse "$home/.fm-secondmate-parent" || return 3
case "$FM_SECONDMATE_PARENT_ROUTE" in
local)
[ -n "$FM_SECONDMATE_PARENT_HOME" ] || return 3
destination="$FM_SECONDMATE_PARENT_HOME/state/$self.status"
;;
remote) destination="$state/parent-replies.status" ;;
*) return 3 ;;
esac
line="done [key=merged-$id]: merged $id $FM_PR_URL"
else
self_rc=$?
[ "$self_rc" -eq 1 ] || return 3
fi

# Load the wake queue owner only when this process has not already: sourcing
# it again would reset the process-level recovery-marker and lock state a
# running watcher (bin/fm-watch.sh) carries across its own marker transitions.
if ! command -v fm_wake_append >/dev/null 2>&1; then
# shellcheck source=bin/fm-wake-lib.sh
. "$_FM_MERGE_OUTCOME_LIB_DIR/fm-wake-lib.sh"
fi
lock="$state/$id.pr-poll-merge-notified.lock"
fm_lock_acquire_wait "$lock" || return 1
if fm_pr_poll_merge_already_notified "$state" "$id" \
"$provider" "$host" "$path" "$number"; then
# shellcheck disable=SC2034 # Public result consumed by sourcing callers.
FM_MERGE_OUTCOME_ALREADY_RECORDED=true
fm_lock_release "$lock"
return 0
fi

if [ -n "$destination" ]; then
fm_merge_outcome_append_once "$destination" "$line" || status=1
fi
if [ "$status" -eq 0 ] && { [ "$origin" = poll ] || [ -z "$destination" ]; }; then
fm_wake_append check "merged-$id-$FM_PR_URL" \
"check: merge landed: $id $FM_PR_URL" || status=1
fi
if [ "$status" -eq 0 ]; then
fm_pr_poll_merge_mark_notified "$state" "$id" \
"$provider" "$host" "$path" "$number" || status=1
fi
fm_lock_release "$lock"
return "$status"
}
40 changes: 36 additions & 4 deletions bin/fm-nm-run-lib.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#!/usr/bin/env bash
# Shared no-mistakes axi run attribution primitives.
#
# ONE owner for the branch+code-identity matching rule that decides whether a
# no-mistakes run belongs to a given worktree, used by fm-crew-state.sh
# (read-only current-state reporting) and fm-teardown.sh (pre-teardown run
# abort, see its "Fix 1" header comment). Getting this wrong in either
# ONE owner for the no-mistakes run-attribution primitives used by
# fm-crew-state.sh (read-only current-state reporting), fm-teardown.sh
# (pre-teardown run abort, see its "Fix 1" header comment), and
# fm-receipt-check.sh (bound-run completion). Teardown uses only strict
# branch-and-head identity; crew-state additionally permits the active
# pipeline-owned exemption defined below. Getting this wrong in either
# direction is unsafe: a false negative hides a genuinely parked run, and a
# false positive lets teardown act on a run it does not own.
#
Expand Down Expand Up @@ -65,6 +67,8 @@ fm_nm_field() { # <toon-output> <key>
# the same history advanced the run tip past local HEAD)
# - run head is a strict ancestor of worktree HEAD, or diverged: no match
# (local work advanced outside the run, or the branch tip was rewritten)
# fm_nm_run_is_pipeline_owned_active below carries the one exemption: a live
# run whose pipeline currently owns the branch binds without head equality.
fm_nm_head_matches_worktree() { # <worktree> <run_head>
local wt=$1 run_head=$2 local_full run_full
[ -n "$run_head" ] || return 1
Expand Down Expand Up @@ -128,6 +132,34 @@ fm_nm_run_is_active() { # <toon-output>
case "$status" in completed|failed|cancelled) return 1 ;; esac
}

# 0 if head $2 resolves to a commit object in worktree $1 at all. This
# distinguishes a PROVEN mismatch (resolvable but not current: a historical or
# diverged head fm_nm_head_matches_worktree correctly rejects) from UNKNOWN
# attribution (unresolvable: e.g. a pipeline-owned lane head that never
# reached this worktree). A caller scanning run rows newest-first must stop on
# unknown attribution rather than surface an older, superseded run.
fm_nm_head_resolvable() { # <worktree> <head>
[ -n "$2" ] || return 1
git -C "$1" rev-parse --verify --quiet "$2^{commit}" >/dev/null 2>&1
}

# The one exemption to the head rule above: while the pipeline OWNS the branch
# (branch_sync.state=pipeline_owned), the daemon's own branch attribution IS
# the attribution for an ACTIVE run, and head equality must not be required -
# the pipeline's lane head is routinely not a git object in the task worktree
# (rebase and fix commits that were never pushed back), so the head rule
# rejects exactly the run that is most current. The exemption never applies to
# a terminal run: a terminal run has released the branch, and binding one by
# branch name alone is the historical reused-branch misattribution the head
# rule exists to prevent. fm_nm_branch_sync_state above reads the scalar
# directly under the top-level `branch_sync:` block; it is empty when the block
# is absent (no run on the current branch, another branch's run, or a CLI
# without branch sync).
fm_nm_run_is_pipeline_owned_active() { # <toon-output>
[ "$(fm_nm_branch_sync_state "$1")" = pipeline_owned ] || return 1
fm_nm_run_is_active "$1"
}

# During no-mistakes' ci monitor, top-level status and outcome stay running after
# checks turn green until the PR merges, while the append-only ci log records the
# transition. The most recent recognized log marker is therefore authoritative:
Expand Down
Loading
Loading