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 @@ -92,6 +92,7 @@ state/ volatile runtime signals; gitignored
<id>.check-trust private content binding created by fm-check-register.sh for an intentional custom check
<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
.pr-check-quarantine/ private non-runnable storage for checks neutralized by the non-executing migration
.pr-check-migration.log private per-task outcomes distinguishing rebuilt or canonically registered replacement polls, quarantined unarmed polls, and incomplete migrations
.pr-check-migration-scan-v1 private marker proving the non-executing scan disabled every unsafe legacy check; .pr-check-migration-v1 separately records completed private repairs
Expand Down
9 changes: 7 additions & 2 deletions bin/fm-pr-check-migrate.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/usr/bin/env bash
# Non-executing migration for watcher PR checks created by older Firstmate
# versions. Legacy check files are never run, sourced, or parsed by Bash.
# Canonical polls are rebuilt from validated metadata, provenance-bound polls
# and registered custom checks remain armed, and every other task poll is
# Pending validated merged-poll retirements finish first. Canonical polls are
# then rebuilt from validated metadata, remaining provenance-bound polls and
# registered custom checks remain armed, and every other task poll is
# quarantined for private review. A current X-mode shim is preserved by exact
# content, while the recognized older byte-static shim is refreshed in place.
# Usage: fm-pr-check-migrate.sh [--checks-safe]
Expand Down Expand Up @@ -333,6 +334,10 @@ if [ ! -d "$STATE" ] || [ -L "$STATE" ]; then
fi
STATE_DEVICE=$(fm_pr_file_device "$STATE") || exit 1
[ -n "$STATE_DEVICE" ] || exit 1
if ! fm_pr_poll_retirement_recover_all "$STATE" "$TEMPLATE"; then
echo "PR_CHECK_MIGRATION: pending PR poll retirement could not be validated:$FM_PR_POLL_RETIREMENT_REJECTED" >&2
exit 1
fi
refresh_v1_x_shim() {
local shim="$STATE/x-watch.check.sh"
fmx_poll_shim_v1_valid "$shim" "$FM_HOME" "$FM_ROOT" "$STATE_DEVICE" || return 0
Expand Down
8 changes: 8 additions & 0 deletions bin/fm-pr-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ if [ ! -f "$META" ] || [ -L "$META" ] || [ "$(fm_pr_file_link_count "$META")" !=
exit 1
fi

# 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.
fm_pr_poll_retirement_recover_one "$STATE" "$ID" "$SCRIPT_DIR/fm-pr-poll.sh" || {
echo "error: pending PR poll retirement could not be validated" >&2
exit 1
}

# Refuse to arm a GitLab watch with no glab on PATH. The poll is silent on
# every error by design, so a missing CLI would be indistinguishable from a
# merge request that is never merged. Arming is the one point where that can be
Expand Down
355 changes: 355 additions & 0 deletions bin/fm-pr-lib.sh

Large diffs are not rendered by default.

17 changes: 14 additions & 3 deletions bin/fm-teardown.sh
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ validate_pr_poll_cleanup() {
return 1
fi
for artifact in "$state_dir/$id.check.sh" "$state_dir/$id.pr-poll" \
"$state_dir/$id.pr-poll-registration" "$state_dir/$id.check-trust"; do
"$state_dir/$id.pr-poll-registration" "$state_dir/$id.pr-poll-retirement" \
"$state_dir/$id.check-trust"; do
[ -e "$artifact" ] || [ -L "$artifact" ] || continue
has_artifact=1
done
Expand All @@ -220,7 +221,8 @@ validate_pr_poll_cleanup() {
[ -d "$state_dir" ] && [ ! -L "$state_dir" ] || return 1
state_device=$(fm_pr_file_device "$state_dir") || return 1
for artifact in "$state_dir/$id.check.sh" "$state_dir/$id.pr-poll" \
"$state_dir/$id.pr-poll-registration" "$state_dir/$id.check-trust"; do
"$state_dir/$id.pr-poll-registration" "$state_dir/$id.pr-poll-retirement" \
"$state_dir/$id.check-trust"; do
[ -e "$artifact" ] || [ -L "$artifact" ] || continue
if [ ! -f "$artifact" ] || [ -L "$artifact" ] \
|| [ "$(fm_pr_file_device "$artifact")" != "$state_device" ] \
Expand All @@ -229,6 +231,13 @@ validate_pr_poll_cleanup() {
return 1
fi
done
if [ -e "$state_dir/$id.pr-poll-retirement" ] \
|| [ -L "$state_dir/$id.pr-poll-retirement" ]; then
fm_pr_poll_retirement_state_valid "$state_dir" "$id" || {
echo "REFUSED: invalid PR-poll retirement receipt; preserving task state." >&2
return 1
}
fi
[ -e "$quarantine" ] || [ -L "$quarantine" ] || return 0
if [ ! -d "$state_dir" ] || [ -L "$state_dir" ] \
|| [ ! -d "$quarantine" ] || [ -L "$quarantine" ]; then
Expand All @@ -252,8 +261,10 @@ validate_pr_poll_cleanup() {
remove_pr_poll_artifacts() {
local state_dir=$1 id=$2 quarantine artifact
validate_pr_poll_cleanup "$state_dir" "$id" || return 1
fm_pr_poll_retirement_recover_one "$state_dir" "$id" "$SCRIPT_DIR/fm-pr-poll.sh" || return 1
rm -f "$state_dir/$id.check.sh" "$state_dir/$id.pr-poll" \
"$state_dir/$id.pr-poll-registration" "$state_dir/$id.check-trust" || return 1
"$state_dir/$id.pr-poll-registration" "$state_dir/$id.pr-poll-retirement" \
"$state_dir/$id.check-trust" || return 1
if fm_task_id_path_safe "$id"; then
quarantine="$state_dir/.pr-check-quarantine"
if [ -d "$quarantine" ] && [ ! -L "$quarantine" ]; then
Expand Down
35 changes: 29 additions & 6 deletions bin/fm-watch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
# check: <script>: <out> authenticated check output, always actionable
# check: rejected unauthenticated state checks: <paths>
# unsafe state checks were refused without execution
# check: rejected unauthenticated PR poll retirement receipts: <paths>
# invalid pending retirements were preserved without
# running a check or removing poll artifacts
# heartbeat fleet-scan backstop found an unsurfaced captain-relevant
# status, unless afk is active
# For normal supervision, resume the session-start primary-harness protocol
Expand Down Expand Up @@ -744,6 +747,16 @@ fm_pid_identity "$WATCHER_PID" > "$WATCH_LOCK/pid-identity" 2>/dev/null || true

[ -e "$STATE/.last-heartbeat" ] || touch "$STATE/.last-heartbeat"

# A merged poll may have queued its terminal wake and then lost the process
# between receipt publication and fixed-path removal.
# Finish only identity-bound retirement receipts before any check can run.
if ! fm_pr_poll_retirement_recover_all "$STATE" "$SCRIPT_DIR/fm-pr-poll.sh"; then
reason="check: rejected unauthenticated PR poll retirement receipts:$FM_PR_POLL_RETIREMENT_REJECTED"
fm_wake_append check pr-poll-retirement "$reason" || exit 1
touch "$STATE/.last-check"
wake "$reason"
fi

while :; do
# Self-eviction: if the singleton lock no longer names this process, a second
# watcher has taken over (e.g. a transient duplicate from a racy arm). Stand
Expand Down Expand Up @@ -776,6 +789,7 @@ while :; do
rejected_checks=
for c in "$STATE"/*.check.sh; do
[ -e "$c" ] || continue
is_pr_poll=0
if [ "$(basename "$c")" = x-watch.check.sh ]; then
if fmx_poll_shim_valid "$c" "$FM_HOME" "$FM_ROOT" \
&& [ -f "$FM_ROOT/bin/fm-x-poll.sh" ] && [ ! -L "$FM_ROOT/bin/fm-x-poll.sh" ]; then
Expand All @@ -787,12 +801,13 @@ while :; do
fi
else
id=$(basename "$c" .check.sh)
if fm_pr_poll_artifacts_valid "$STATE" "$id" "$SCRIPT_DIR/fm-pr-poll.sh"; then
provider=$FM_PR_DATA_PROVIDER
url=$FM_PR_DATA_URL
host=$FM_PR_DATA_HOST
path=$FM_PR_DATA_PATH
number=$FM_PR_DATA_NUMBER
if fm_pr_poll_snapshot_capture "$STATE" "$id" "$SCRIPT_DIR/fm-pr-poll.sh"; then
is_pr_poll=1
provider=$FM_PR_POLL_SNAPSHOT_PROVIDER
url=$FM_PR_POLL_SNAPSHOT_URL
host=$FM_PR_POLL_SNAPSHOT_HOST
path=$FM_PR_POLL_SNAPSHOT_PATH
number=$FM_PR_POLL_SNAPSHOT_NUMBER
run_check_capture "$SCRIPT_DIR/fm-pr-poll.sh" --validated \
"$provider" "$url" "$host" "$path" "$number" || exit 1
out=$FM_CHECK_RESULT
Expand All @@ -810,6 +825,14 @@ while :; do
if [ -n "$out" ]; then
reason="check: $c: $out"
fm_wake_append check "$c" "$reason" || exit 1
if [ "$is_pr_poll" -eq 1 ] && [ "$out" = merged ]; then
if fm_pr_poll_retirement_publish "$STATE" "$id" "$SCRIPT_DIR/fm-pr-poll.sh" "$out"; then
fm_pr_poll_retirement_recover_one "$STATE" "$id" "$SCRIPT_DIR/fm-pr-poll.sh" \
|| triage_log "merged PR poll retirement remains recoverable for $id"
else
triage_log "merged PR poll retirement deferred because its canonical snapshot changed for $id"
fi
fi
touch "$STATE/.last-check"
wake "$reason"
fi
Expand Down
4 changes: 4 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ A zero-token bash watcher (`bin/fm-watch.sh`) sleeps on the fleet, classifies de
Actionable wakes include captain-relevant status signals, no-verb signals whose crew is not provably working, authenticated check output such as PR merge polling or an X-mode mention, stale panes whose crew is not provably working whether their status log looks terminal or non-terminal, provably-working stale panes that persist past `FM_STALE_ESCALATE_SECS`, declared external waits that remain paused past `FM_PAUSE_RESURFACE_SECS`, and heartbeat backstop hits.
Repeated provably-working stale escalations on the same unchanged pane add an escalation count to the wake reason and, at `FM_WEDGE_DEMAND_INSPECT_COUNT`, a `demand-deep-inspection` marker.
Those actionable wakes are written to a durable local queue (`state/.wake-queue`) before detector state advances, so a missed process exit can be recovered by draining the queue.
When a canonical validated PR poll returns exactly `merged`, the watcher appends that durable notification before publishing a private receipt bound to the poll's registration, bytes, file identities, metadata, provider, URL, and task ID.
The receipt makes retirement safely retryable across restarts: fixed-path recovery revalidates the same evidence, removes the runnable check first, removes its registration and data sidecars, removes the receipt last, and preserves task metadata including `pr=` and `pr_head=`.
A concurrent replacement remains armed, every non-merged or invalid observation remains unchanged, and retirement never performs task or persistent-secondmate cleanup.
`bin/fm-pr-lib.sh` owns the receipt format and strict identity mechanics, while `bin/fm-watch.sh` owns queue-before-retirement ordering.
No-verb wakes, such as `working:` notes and bare turn-ended signals, are benign only when `bin/fm-crew-state.sh` reports positive evidence that the crew is still working: an actively running no-mistakes step attributed to that crew's current code or a backend busy signature.
A crew that declares `paused:` for a known external wait is separately absorbed while idle and re-surfaced only on the longer pause cadence, rather than being treated as a possible wedge.
For an ordinary crew that has stopped, the normal-mode watcher first surfaces one stale wake, then applies that same cadence to an unchanged `paused:` or durable `captain-held` endpoint only when the backend confidently reports its agent dead.
Expand Down
2 changes: 1 addition & 1 deletion docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize
| `fm-peek.sh` | Print a bounded tail of a crewmate endpoint |
| `fm-check-register.sh` | Bind an intentional custom watcher check to its current bytes |
| `fm-check-lib.sh` | Validate custom-check registrations and prepare private execution snapshots |
| `fm-pr-lib.sh` | Own canonical task and PR validation plus private atomic PR-poll and provenance publication |
| `fm-pr-lib.sh` | Own canonical task and PR validation plus private atomic PR-poll publication and identity-bound retirement |
| `fm-pr-poll.sh` | Provide the byte-static watcher program for validated PR/MR-poll sidecars |
| `fm-pr-check-migrate.sh` | Quarantine older task polls without execution and rebuild only canonical polls |
| `fm-pr-check.sh` | Record validated `pr=` and `pr_head=` values, then atomically arm a static merge poll |
Expand Down
Loading
Loading