Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
b96dba1
fix(bearings): restore decision options and add close controls (#2707)
kunchenguid Aug 21, 2026
a0cec26
ci: require no-mistakes pipeline step attestation (#2710)
kunchenguid Aug 21, 2026
99b21d8
feat: collapse decisions into tasks held for the captain (#2728)
kunchenguid Aug 21, 2026
3f03533
fix: bound recovery announcements and preserve supervision (#2733)
kunchenguid Aug 21, 2026
d3342dc
fix(bin): surface captain-call record divergence (#2744)
mremond Aug 21, 2026
4d2cb0c
fix(bin): re-arm after an abandoned auto-arm claim and defer a wedge …
Inthuson Aug 21, 2026
3d125ad
fix(bin): give a captain hold the same bounded pause cadence as a dec…
Inthuson Aug 21, 2026
738460d
fix(bin): make lint prerequisites and harness tests reliable (#2758)
kunchenguid Aug 21, 2026
59e7393
feat(bin): report watched tooling updates that are available or insta…
Inthuson Aug 21, 2026
52d20f1
fix: decouple ask-user decisions from yolo (#2764)
kunchenguid Aug 22, 2026
fbe37e9
feat(bin): add a spoken interface that answers from records and hands…
Inthuson Aug 22, 2026
dc0172c
fix(bin): preserve Relay follow-up loops until explicit disposition (…
kunchenguid Aug 22, 2026
5b6d0fb
feat(bin): merge GitLab merge requests through the guarded PR merge p…
Inthuson Aug 22, 2026
1231b6a
fix(bin): record a lost relay connection instead of an unanswered tur…
Inthuson Aug 22, 2026
8714c9a
fix(composer): stop a blocked pi pane from proving an empty composer …
karotkriss Aug 23, 2026
801c083
fix(bin): require project clone roots during fleet sync (#2849)
karotkriss Aug 23, 2026
505c819
fix(bin): retry transient Lavish poll interruptions (#2846)
karotkriss Aug 23, 2026
86dd2f6
fix(brief): stop the documented {TASK} fill from corrupting the Herdr…
karotkriss Aug 23, 2026
266fdb9
fix(bin): resolve the busy-state lock mtime with the platform's own s…
karotkriss Aug 23, 2026
f170ced
fix(stow): add opt-in pass horizon for memory decay (#2850)
karotkriss Aug 23, 2026
2f250c7
test(watcher): stop fixture confirmation budgets racing real child st…
karotkriss Aug 23, 2026
197afbb
fix(bin): deterministically order remote tool paths (#2870)
karotkriss Aug 23, 2026
52f62ab
fix(bin): prevent routed secondmate work from stranding (#2848)
kunchenguid Aug 23, 2026
822a990
fix: make macOS inbox test path portable (#2857)
kunchenguid Aug 23, 2026
e46df1a
feat(bin): deliver local steers through durable task inboxes (#2856)
kunchenguid Aug 23, 2026
8d8362c
feat(bin): add fast local lint mode (#2891)
kunchenguid Aug 23, 2026
ddf74ef
feat(bin): deliver remote steers through durable inboxes (#2901)
kunchenguid Aug 23, 2026
7b88520
feat: add persistent Pi supervision branch (#2858)
kunchenguid Aug 23, 2026
fb2ce5b
fix(bin): parallelize startup network sweeps (#2927)
kunchenguid Aug 24, 2026
8b21c99
test: handle absent watcher wake queues (#2845)
karotkriss Aug 24, 2026
52ff62e
style(pi): distinguish routine and captain supervision merge notes by…
kunchenguid Aug 24, 2026
d55e00a
docs(pi): add the approved multi-brain architecture poster (#2938)
kunchenguid Aug 24, 2026
8fa0505
feat(pi): default branch supervision and route heartbeats (#2939)
kunchenguid Aug 24, 2026
038d0f7
fix(bin): bound remote job worker supervisor restarts (#2942)
stanzhang Aug 24, 2026
85d6c72
fix: safely split supervision wake handling by actor (#2953)
kunchenguid Aug 25, 2026
6a2cd6c
fix(pi): hide branch outcomes tool rows in Calm (#3024)
kunchenguid Aug 25, 2026
3e5577b
fix: bind no-mistakes attestations to PR head (#3027)
kunchenguid Aug 25, 2026
9a01dea
feat(pi): add persistent supervision branch model selection (#3028)
kunchenguid Aug 25, 2026
f7a387f
feat(pi): let /supervision-model pick branch reasoning effort (#3079)
kunchenguid Aug 26, 2026
07bf0c8
fix: keep routine supervision noise out of captain chat (#3093)
kunchenguid Aug 26, 2026
9ce69ac
fix(bin): stop a correlation token from hiding and stranding decision…
mremond Aug 26, 2026
b0ca8f5
fix(bin): Cursor-Park unter Pi-Host ohne Cursor-Identität stilllegen …
thelad-dev Aug 26, 2026
5aed873
fix(pi): make supervision model picker searchable and scrollable (#3099)
kunchenguid Aug 26, 2026
662a8c7
fix(bin): durably report merged pull requests (#3104)
kunchenguid Aug 26, 2026
60bedde
fix(bearings): preserve projections through inventory mismatches (#3129)
kunchenguid Aug 26, 2026
22fa6ed
fix(bin): reconcile markerless remote secondmates safely (#3140)
kunchenguid Aug 27, 2026
99c1a0d
fix(bin): hand a busy declared pause to the away-mode daemon once, un…
3264studios Aug 27, 2026
524994c
fix(bin): name the stale submodule pin behind a pooled slot refusal (…
mkurt Aug 27, 2026
d63b0e2
fix(pi): prevent stale captain outcome re-emissions (#3154)
kunchenguid Aug 27, 2026
5953e9b
fix(bin): keep a declared wait on the pause cadence under a busy pane…
3264studios Aug 27, 2026
10b93b2
fix(bin): recover Claude auto-arm from hung claims (#3156)
kunchenguid Aug 27, 2026
7ee0c19
fix(bin): verify the real GitHub merge outcome instead of reporting a…
wjkawecki-jt Aug 27, 2026
4f89f5b
fix(pi): prevent duplicate captain outcome reports (#3184)
kunchenguid Aug 27, 2026
bca584a
fix(bin): prioritize active pipeline-owned crew runs (#3194)
kunchenguid Aug 27, 2026
c651b59
fix(pi): surface requested outcomes without replaying fleet events (#…
kunchenguid Aug 28, 2026
1fd7ea2
feat(bin): add concurrent bounded remote transport lanes (#3210)
kunchenguid Aug 28, 2026
6c98653
chore: restore fork sync spine
withally Aug 28, 2026
4dc0c3e
chore: re-apply upstream-sync companion edits
withally Aug 28, 2026
e3f36a7
fix(watch): hold the declared-wait cadence across pane churn (#67)
withally Aug 22, 2026
bc1dd44
feat(bin): add durable host resource telemetry recorder (#70)
withally Aug 24, 2026
bf54c71
fix(lock): bound stale lock recovery with a non-recursive steal mutex…
withally Aug 24, 2026
13cbd78
fix(bin): keep remote home seeding portable on Bash 3.2 and fail clos…
withally Aug 24, 2026
aaa43be
feat(bin): add fseventsd footprint early warning to the fleet watcher…
withally Aug 24, 2026
e5b7c39
fix(spawn): set PI_CLEAR_ON_SHRINK=1 for Firstmate-launched Pi sessio…
withally Aug 25, 2026
caa0aca
feat: add urgent upstream tripwire (#79)
withally Aug 28, 2026
fa78627
fix: unblock Herdr Claude away-mode injections (#80)
withally Aug 28, 2026
0bed202
ci: wait for no-mistakes attestation convergence (#81)
withally Aug 28, 2026
0ee9399
test: materialize skill reference fixture
withally Aug 28, 2026
c918928
docs: record the 2026-08-29 catch-up
withally Aug 29, 2026
54438b6
chore: reconcile snapshot with fork main
withally Aug 29, 2026
4de2276
test(wake-drain): synchronize pre-commit interruption
withally Aug 29, 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
8 changes: 5 additions & 3 deletions .agents/skills/afk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ The daemon still clears its buffer only on the backend's `empty` success verdict

The daemon wraps `fm-watch.sh`, runs the watcher as a child, presents every durable wake after each actionable watcher close, classifies each presented record in bash, and acknowledges the presented generation only after routing completes.
It self-handles the routine majority without consuming a firstmate turn.
Captain-relevant events, plus a bounded recheck of a declared wait that remains idle, escalate to firstmate's context as one pre-read, single-line, batched digest.
Captain-relevant events, plus a bounded recheck of a declared wait that is still declared, escalate to firstmate's context as one pre-read, single-line, batched digest.
The classification predicates (the captain-relevant verb set, declared-wait vocabulary, signal/stale tests, and fleet-scan) live in the shared `bin/fm-classify-lib.sh`, the same library the always-on watcher uses for its own triage when afk is off, so the two modes apply one identical policy.
While `state/.afk` exists the daemon owns the watcher, so the watcher reverts to one-shot and lets the daemon do the triage - the two never run their triage at the same time.

Expand All @@ -147,8 +147,10 @@ Classify each wake this way:
- `signal` with a terminal captain verb (`done:`, `needs-decision:`, `blocked:`, or `failed:`) -> escalate.
A nonterminal progress verb remains nonterminal even when its prose contains a legacy free-text token such as `PR ready`, `checks green`, `ready in branch`, or `merged`; only a bare legacy line with such a token escalates.
Other signals with no captain-relevant status -> self-handle.
- `signal` or `stale` for a declared wait, either a `paused:` external wait or a verified `captain-held` transfer -> self-handle and track the pause rather than a wedge.
If it remains declared and idle past `FM_PAUSE_RESURFACE_SECS` (default 3600s), housekeeping sends one recheck and resets the pause window.
- `signal` or `stale` for a declared wait, either a `paused:` external wait or a verified `captain-held` transfer -> self-handle and track the pause rather than a wedge, whether its pane reads idle or busy.
That outranks an enriched possible-wedge reason, so a declared wait never escalates on the `FM_STALE_ESCALATE_SECS` cadence.
If it is still declared past `FM_PAUSE_RESURFACE_SECS` (default 3600s), housekeeping sends one recheck and resets the pause window.
The window ages against the crew's own latest status line, so only a status append that stops declaring the wait ends this routing and restores wedge detection.
That recheck names which human the wait is on: the external dependency for `paused:`, and the captain themself for a `captain-held` transfer, who can answer the held decision or release the hold.
- `check` -> always escalate. Check scripts print only when firstmate should wake.
- `stale` with a terminal status or bare legacy captain-relevant line -> escalate.
Expand Down
136 changes: 107 additions & 29 deletions .pi/extensions/fm-branch-supervision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
// real tools and reports through the fm_branch_report custom tool, which
// writes the durable outcome store FIRST (bin/fm-branch-outcome.sh) and then
// merges an append-only note to main's tail. Main's captain/assistant dialog
// is mirrored into the branch as read-only fm-main-mirror context at main's
// turn_end. Pi-only by construction: this file lives in .pi/extensions, so no
// is mirrored into the branch as read-only fm-main-mirror context from Pi's
// before_agent_start prompt and at main's turn_end. Pi-only by construction: this
// file lives in .pi/extensions, so no
// other harness ever loads it. Supervision is default-on for every task once
// this Pi session owns the fleet lock: no captain grant file is required.
// Away mode (or a broken branch) keeps today's wake-to-main behavior
Expand Down Expand Up @@ -95,7 +96,10 @@ import {
FOLLOW_MAIN_VALUE,
type BranchPickerItem,
} from "./lib/fm-branch-model-picker.ts";
import { encodeFirstmateOperationalInput } from "./lib/fm-operational-input.ts";
import {
classifyFirstmateOperationalText,
encodeFirstmateOperationalInput,
} from "./lib/fm-operational-input.ts";

const extensionFile = fileURLToPath(import.meta.url);
const extensionDir = dirname(extensionFile);
Expand Down Expand Up @@ -129,11 +133,18 @@ const MIRROR_MESSAGE_CAP = 4000;
const MERGE_NOTE_BOAT = "⛵";
// Carried inside the captain note's own text because that text is the only
// part of a custom message Pi gives the model (see mergeIntoMain).
//
// The note still needs to identify itself so main cannot mistake an incoming
// outcome for its own earlier answer and silently lose the outcome. Event
// ownership forbids a second fleet operation, while the captain-facing verdict
// requires a visible response and leaves its wording to main.
const CAPTAIN_OUTCOME_INSTRUCTION =
"This is a supervision outcome delivered automatically by the supervision branch. " +
"It was not typed by the captain and it is not your own earlier output. " +
"Relay only this outcome to the captain now, in one short message, in captain outcome language. " +
"Do not restate or repeat any earlier answer.";
"It was not typed by the captain. " +
"The fleet event is already handled: do not re-drain, re-run, or acknowledge it. " +
"This outcome is captain-facing: give the captain a visible response now. " +
"Use your judgment over the wording and how to incorporate it, not whether to surface it. " +
"An outcome that directly answers an explicit captain request is captain-facing, regardless of whether it is healthy, routine, measured, actionable, or requires a decision.";
type MirrorItem = { tag: "captain" | "main"; text: string };
type MirrorCursor = { file: string; index: number };
type Verdict = "routine" | "captain";
Expand Down Expand Up @@ -294,15 +305,17 @@ function textOfContent(content: unknown): string {
// Operational injections (watcher wakes, away-supervisor escalations, launch
// briefs) are fleet machinery, not captain dialog; the report's volume
// analysis counts them apart from dialog, and mirroring them would feed the
// branch its own supervision traffic back. Current injections start with the
// U+2063 operational prefix; the plain legacy form starts with FIRSTMATE.
// branch its own supervision traffic back.
function isOperationalUserText(text: string): boolean {
return text.startsWith("⁣") || /^FIRSTMATE[ _]/.test(text);
return classifyFirstmateOperationalText(text) !== undefined;
}

function capMirrorText(text: string): string {
if (text.length <= MIRROR_MESSAGE_CAP) return text;
return `${text.slice(0, MIRROR_MESSAGE_CAP)}\n[mirror truncated at ${MIRROR_MESSAGE_CAP} characters]`;
const headLength = Math.ceil(MIRROR_MESSAGE_CAP / 2);
const tailLength = MIRROR_MESSAGE_CAP - headLength;
const omitted = text.length - MIRROR_MESSAGE_CAP;
return `${text.slice(0, headLength)}\n[mirror truncated: ${omitted} characters omitted]\n${text.slice(-tailLength)}`;
}

function readMirrorCursor(): MirrorCursor {
Expand Down Expand Up @@ -336,23 +349,52 @@ type ReadonlyEntries = {
type MirrorCollectionState = {
collectAnchor: MirrorCursor | null;
pendingCursor: MirrorCursor | null;
// Pi emits before_agent_start before it appends that turn's user message to
// SessionManager. The prompt is mirrored from the event immediately, then
// this marker suppresses the same persisted entry when turn_end collects it.
stagedCaptain: { file: string; index: number; text: string } | null;
};

function collectMainDialog(sessionManager: ReadonlyEntries, collection: MirrorCollectionState): MirrorItem[] {
const file = sessionManager.getSessionFile() ?? "";
const entries = sessionManager.getEntries();
const anchor = collection.collectAnchor ?? readMirrorCursor();
const start = anchor.file === file ? Math.min(anchor.index, entries.length) : 0;
let currentCaptainIndex = -1;
for (let index = entries.length - 1; index >= start; index -= 1) {
const entry = entries[index];
if (entry.type !== "message") continue;
const message = (entry as { message?: { role?: string; content?: unknown } }).message;
if (message?.role !== "user") continue;
const text = textOfContent(message.content).trim();
if (!text || isOperationalUserText(text)) continue;
currentCaptainIndex = index;
break;
}
const items: MirrorItem[] = [];
for (const entry of entries.slice(start)) {
for (let index = start; index < entries.length; index += 1) {
const entry = entries[index];
if (entry.type !== "message") continue;
const message = (entry as { message?: { role?: string; content?: unknown } }).message;
if (!message) continue;
if (message.role !== "user" && message.role !== "assistant") continue;
const text = textOfContent(message.content).trim();
if (!text) continue;
if (message.role === "user" && isOperationalUserText(text)) continue;
items.push({ tag: message.role === "user" ? "captain" : "main", text: capMirrorText(text) });
const staged = collection.stagedCaptain;
if (
message.role === "user" &&
staged?.file === file &&
staged.index === index &&
staged.text === text
) {
collection.stagedCaptain = null;
continue;
}
items.push({
tag: message.role === "user" ? "captain" : "main",
text: index === currentCaptainIndex ? text : capMirrorText(text),
});
}
collection.collectAnchor = { file, index: entries.length };
collection.pendingCursor = collection.collectAnchor;
Expand All @@ -375,7 +417,12 @@ export default function (pi: ExtensionAPI) {
// serially by design).
let branchChain: Promise<void> = Promise.resolve();
const pendingMirror: MirrorItem[] = [];
const mirrorCollection: MirrorCollectionState = { collectAnchor: null, pendingCursor: null };
const mirrorCollection: MirrorCollectionState = {
collectAnchor: null,
pendingCursor: null,
stagedCaptain: null,
};
let currentMainSession: ReadonlyEntries | null = null;
// One revision for BOTH selections: a model or effort change invalidates an
// in-flight branch build exactly the same way.
let branchSelectionRevision = 0;
Expand Down Expand Up @@ -562,10 +609,11 @@ export default function (pi: ExtensionAPI) {
// therefore has to carry its own identity inside `content`, or main receives
// an unattributed user message written in main's own captain-facing voice
// and cannot tell an incoming outcome from its own earlier answer. When that
// happens main re-emits its previous answer instead of relaying the outcome,
// and the outcome is lost. The typed operational envelope is what makes the
// note self-describing; it stays invisible to the captain because the note
// is never rendered.
// happens main can lose the outcome while deciding how to handle it. The
// typed operational envelope is what makes the note self-describing; it stays
// invisible to the captain because the note is never rendered. The
// instruction preserves the event-ownership boundary while requiring the
// captain-facing response and leaving its wording to main.
//
// Encoding shells out, so it can fail on a broken checkout. This file's
// failure direction applies: an outcome that cannot be typed is still
Expand Down Expand Up @@ -621,7 +669,8 @@ export default function (pi: ExtensionAPI) {
parameters: Type.Object({
task: Type.String({ description: "The task id the event belongs to (or 'fleet' for fleet-wide events)" }),
verdict: Type.Union([Type.Literal("routine"), Type.Literal("captain")], {
description: "captain only for what a human must see; routine otherwise",
description:
"Use captain unconditionally for an outcome that directly answers an explicit captain request, regardless of whether it is healthy, routine, measured, actionable, or requires a decision. Also use captain for work ready for review, captain-only decisions, blockers or failures after recovery is exhausted, needed credentials, and destructive, irreversible, or security-sensitive actions; use routine otherwise.",
}),
summary: Type.String({
description:
Expand Down Expand Up @@ -930,6 +979,16 @@ ${context.command}
});
}

function collectCurrentMainDialog(): boolean {
if (!currentMainSession) return true;
try {
pendingMirror.push(...collectMainDialog(currentMainSession, mirrorCollection));
return true;
} catch {
return false;
}
}

function enqueueMirrorFlush(): void {
if (!branch || pendingMirror.length === 0) return;
const flushGeneration = generation;
Expand All @@ -955,10 +1014,29 @@ ${context.command}
if (!actingAsOwner()) return; // cold start pre-lock, secondary session, or shutdown
if (afkActive()) return; // the away daemon owns supervision while afk
if (branchBroken) return; // fail back to today's wake-to-main path
if (!collectCurrentMainDialog()) return;
offer.accept();
enqueueWake(offer.message, generation);
});

pi.on?.("before_agent_start", (event, ctx) => {
rememberMainModel(ctx);
currentMainSession = ctx?.sessionManager ?? null;
if (!actingAsOwner() || !currentMainSession || !collectCurrentMainDialog()) return;

// This event is Pi's authoritative complete current prompt. At this point
// SessionManager still contains only the preceding dialog, so relying on
// getEntries() here loses the captain request that the next wake may answer.
// Stage it verbatim and remember the future persisted index for turn_end's
// duplicate suppression. Operational extension injections are not dialog.
const prompt = event.prompt.trim();
if (!prompt || isOperationalUserText(prompt)) return;
const file = currentMainSession.getSessionFile() ?? "";
const index = mirrorCollection.collectAnchor?.index ?? currentMainSession.getEntries().length;
pendingMirror.push({ tag: "captain", text: prompt });
mirrorCollection.stagedCaptain = { file, index, text: prompt };
});

pi.on?.("agent_start", () => {
mainStreaming = true;
});
Expand All @@ -969,18 +1047,16 @@ ${context.command}
mainStreaming = false;
});

// Mirror at main's turn_end: collect the new captain/assistant dialog into
// the volatile queue, then deliver it through the serialized chain so it
// lands before any later wake. The durable cursor advances only in
// before_agent_start stages Pi's authoritative in-flight prompt before
// SessionManager persists it. The dispatch handler then collects any newly
// persisted dialog immediately before accepting a wake, so all context joins
// the serialized chain before that wake's branch prompt. turn_end remains
// the idle-path mirror flush. The durable cursor advances only in
// flushMirror after the complete pending batch reaches the branch.
pi.on?.("turn_end", (_event, ctx) => {
rememberMainModel(ctx);
if (!actingAsOwner()) return;
try {
pendingMirror.push(...collectMainDialog(ctx.sessionManager, mirrorCollection));
} catch {
return;
}
currentMainSession = ctx.sessionManager;
if (!actingAsOwner() || !collectCurrentMainDialog()) return;
enqueueMirrorFlush();
});

Expand All @@ -993,6 +1069,7 @@ ${context.command}
// recorded pointer. Terminal quit simply never fires another session_start.
pi.on?.("session_start", (_event, ctx) => {
rememberMainModel(ctx);
currentMainSession = ctx?.sessionManager ?? null;
shuttingDown = false;
branchBroken = "";
generation += 1;
Expand Down Expand Up @@ -1030,8 +1107,10 @@ ${context.command}
shuttingDown = true;
generation += 1;
pendingMirror.length = 0;
currentMainSession = null;
mirrorCollection.collectAnchor = null;
mirrorCollection.pendingCursor = null;
mirrorCollection.stagedCaptain = null;
if (branch) {
try {
branch.dispose();
Expand Down Expand Up @@ -1368,8 +1447,7 @@ ${context.command}
.map((item) => normalizeOutcomesToolOutput(item.text))
.join("\n");
const shellState = context.state as OutcomesToolShellState;
const styledOutput = output.split("\n").map((line) => theme.fg("toolOutput", line)).join("\n");
shellState.result = output ? new Text(styledOutput, 0, 0) : new Container();
shellState.result = output ? new Text(theme.fg("toolOutput", output), 0, 0) : new Container();
refreshOutcomesToolShell(shellState, theme, context);
return new Container();
},
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,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 All @@ -359,7 +359,7 @@ Send the same worker one exact decision naming the decision key, step, action, a
Require the matching `resolved` event, forbid `--yes`, and require the worker to process every synchronous return until completion or a genuinely new escalation.
Resume fleet supervision immediately after the decision lands.

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 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
2 changes: 1 addition & 1 deletion bin/fm-backlog-handoff.sh
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ remote_deliver_outbox() { # <secondmate-id> <outbox-path>
mv -f -- "$counter_tmp" "$counter" \
|| { rm -f -- "$snapshot" "$counter_tmp"; return 1; }
remote_rel="state/handoff/$id.outbox.md"
if ! "$SCRIPT_DIR/fm-on.sh" "$id" fm-remote-file.sh put "$remote_rel" 1048576 \
if ! "$SCRIPT_DIR/fm-on.sh" --stdin "$id" fm-remote-file.sh put "$remote_rel" 1048576 \
"$bytes" "$hash" "$generation" < "$snapshot"; then
rm -f -- "$snapshot"
echo "error: handoff transfer to $id was unavailable or completion is unknown; outbox preserved at $outbox" >&2
Expand Down
Loading
Loading