@feigi/fleet-ctl 3.76.9 → 3.77.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/scripts/fleet-event.mjs
CHANGED
|
@@ -134,6 +134,27 @@
|
|
|
134
134
|
// `closeout.mjs reopen --reason "<reason>"`, its output printed as it
|
|
135
135
|
// stands, then the tick. Refused once any `finish` step is ok, and when
|
|
136
136
|
// no drain marker is set; its exit 2 or 3 is passed through.
|
|
137
|
+
// member-died <member>
|
|
138
|
+
// Settles a member the controller has confirmed dead, then ticks. The
|
|
139
|
+
// outcome comes from the session directory's listing, never from an
|
|
140
|
+
// argument or a file's contents. The first marker that matches decides:
|
|
141
|
+
// <m>.jsonl.tombstone killed
|
|
142
|
+
// <m>.md, no tombstone, fix-pr or finisher-pr failed
|
|
143
|
+
// <m>.md, no tombstone, merge-bot killed
|
|
144
|
+
// <m>.md, no tombstone, impl refused: settle it by hand
|
|
145
|
+
// <m>.jsonl, no marker refused: running or parked
|
|
146
|
+
// no transcript, no marker killed once the tick's
|
|
147
|
+
// first-seen stamp in
|
|
148
|
+
// heartbeat.json is over
|
|
149
|
+
// five minutes old; refused
|
|
150
|
+
// when younger or unstamped
|
|
151
|
+
// Ahead of the markers: a review runner refuses, its death being
|
|
152
|
+
// `review-failed`; a member the ledger already holds settled is noted,
|
|
153
|
+
// and the tick runs; a report on disk refuses, naming the kind that
|
|
154
|
+
// settles it, or its violation when that kind could not settle from
|
|
155
|
+
// it. The session directory is this run's phase-0 receipt's
|
|
156
|
+
// `session`, else `--session <dir>`: the tick's lookup, so another
|
|
157
|
+
// run's receipt names none.
|
|
137
158
|
//
|
|
138
159
|
// A write this script cannot make, and does not refuse, is printed as a
|
|
139
160
|
// `NEED <what> …` line, at the left margin, for the controller to make by
|
|
@@ -141,7 +162,10 @@
|
|
|
141
162
|
//
|
|
142
163
|
// Exit: 0 every write made and the tick exited 0. 2 a usage error, refused
|
|
143
164
|
// before anything is written or ticked: an unknown kind or flag, a `--reason`
|
|
144
|
-
// that is empty or beside any kind but `drain` and `undrain`, a
|
|
165
|
+
// that is empty or beside any kind but `drain` and `undrain`, a `--session`
|
|
166
|
+
// beside any kind but `member-died`, any `member-died` refusal (the table's,
|
|
167
|
+
// a name that is no ledger member, no session directory, a session directory
|
|
168
|
+
// that cannot be listed, a heartbeat file that cannot be used), a member of
|
|
145
169
|
// the wrong family, a review path outside the PR's partition or a file that
|
|
146
170
|
// is no review result, a ledger or phase-0 receipt that cannot be read. A
|
|
147
171
|
// ledger write's own refusal — `settle`'s 3 (the report's claim and the world
|
|
@@ -155,17 +179,18 @@
|
|
|
155
179
|
// Zero deps: node builtins and sibling scripts only.
|
|
156
180
|
|
|
157
181
|
import { spawnSync } from "node:child_process";
|
|
158
|
-
import { readFileSync, realpathSync } from "node:fs";
|
|
182
|
+
import { readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
159
183
|
import { basename, dirname, join, normalize, resolve } from "node:path";
|
|
160
184
|
import { fileURLToPath } from "node:url";
|
|
161
185
|
import { makeDie, defineFlags, isDigits, writeAll } from "./arg.mjs";
|
|
162
186
|
import { FleetDirUnresolvable, fleetFile } from "./fleet-dir.mjs";
|
|
163
187
|
import { gitEnv } from "./git-env.mjs";
|
|
164
188
|
import { isCLI } from "./is-cli.mjs";
|
|
165
|
-
import { BRIEFS, REVIEW, REVIEWED, memberTokens, nextReviewerName, parseDispatchedEntry, parseMember, parseToken, readRetired, rowNums } from "./ledger-grammar.mjs";
|
|
166
|
-
import { dispositionsToken, foldLedger } from "./fleet-tick.mjs";
|
|
167
|
-
import { readRunReceipt, tickCaps } from "./phase0-receipt.mjs";
|
|
189
|
+
import { BRIEFS, REVIEW, REVIEWED, REVIEWER, memberTokens, nextReviewerName, parseDispatchedEntry, parseMember, parseToken, readRetired, rowNums } from "./ledger-grammar.mjs";
|
|
190
|
+
import { NO_TRANSCRIPT_AFTER_DEFAULT, controllerState, dispositionsToken, foldLedger } from "./fleet-tick.mjs";
|
|
191
|
+
import { phase0Gate, readRunReceipt, tickCaps } from "./phase0-receipt.mjs";
|
|
168
192
|
import { defaultScratch, readReportIn, reportsDir } from "./report-files.mjs";
|
|
193
|
+
import { readState, statePath } from "./fleet-state.mjs";
|
|
169
194
|
|
|
170
195
|
const NAME = "fleet-event";
|
|
171
196
|
const die = makeDie(NAME);
|
|
@@ -188,8 +213,9 @@ const KINDS = {
|
|
|
188
213
|
heartbeat: { args: [] },
|
|
189
214
|
drain: { args: [], flag: "--reason \"<reason>\"" },
|
|
190
215
|
undrain: { args: [], flag: "--reason \"<reason>\"" },
|
|
216
|
+
"member-died": { args: ["<member>"] },
|
|
191
217
|
};
|
|
192
|
-
const FLAGS = { "in-flight": "value", reason: "value" };
|
|
218
|
+
const FLAGS = { "in-flight": "value", reason: "value", session: "value" };
|
|
193
219
|
const USAGE = `usage: ${NAME}.mjs <kind> <args> [--in-flight <n>] — kinds: `
|
|
194
220
|
+ Object.entries(KINDS).map(([k, { args, flag }]) => [k, ...args, ...(flag ? [flag] : [])].join(" ")).join(", ");
|
|
195
221
|
|
|
@@ -299,15 +325,18 @@ class Event {
|
|
|
299
325
|
return readReportIn(reportsDir(this.scratch), member);
|
|
300
326
|
}
|
|
301
327
|
|
|
302
|
-
// `settle <member> --from-report` unless the ledger holds it settled
|
|
303
|
-
//
|
|
304
|
-
settle
|
|
328
|
+
// `settle <member> --from-report` unless the ledger holds it settled, or
|
|
329
|
+
// `settle <member> <decided>` when a marker decided the outcome — never one
|
|
330
|
+
// a file's contents named. Returns the outcome, or null when the settle
|
|
331
|
+
// refused.
|
|
332
|
+
settle(member, decided = null) {
|
|
305
333
|
const before = settledOutcome(this.ledger, member);
|
|
306
334
|
if (before !== null) {
|
|
307
335
|
this.note(`${member}=${before} already settled`);
|
|
308
336
|
return before;
|
|
309
337
|
}
|
|
310
|
-
const
|
|
338
|
+
const tail = decided === null ? ["--from-report", "--reports", reportsDir(this.scratch)] : [decided];
|
|
339
|
+
const r = node("ledger.mjs", ["settle", member, ...tail]);
|
|
311
340
|
if (r.status !== 0) {
|
|
312
341
|
this.refuse(`settle ${member}`, r);
|
|
313
342
|
return null;
|
|
@@ -727,6 +756,81 @@ function undrain(ev, reason) {
|
|
|
727
756
|
}
|
|
728
757
|
}
|
|
729
758
|
|
|
759
|
+
// ── member-died ─────────────────────────────────────────────────────────────
|
|
760
|
+
|
|
761
|
+
// fleet-tick's default `--no-transcript-after`: a member with no transcript
|
|
762
|
+
// whose first-seen stamp is older than this never started.
|
|
763
|
+
const NO_TRANSCRIPT_MS = NO_TRANSCRIPT_AFTER_DEFAULT * 60_000;
|
|
764
|
+
|
|
765
|
+
// The kind whose report settles a family — the KINDS entry naming that family
|
|
766
|
+
// — named in the refusal that finds one on disk.
|
|
767
|
+
export const reportKind = (family) => Object.keys(KINDS).find((k) => KINDS[k].family === family);
|
|
768
|
+
|
|
769
|
+
// This run's phase-0 receipt `session`, or null: the receipt phase0Gate()
|
|
770
|
+
// judges this run's against the controller record in heartbeat.json and that
|
|
771
|
+
// record's process, as fleet-tick judges it.
|
|
772
|
+
function runSession({ read, path }) {
|
|
773
|
+
const { controller } = readState(statePath(NAME), NAME);
|
|
774
|
+
return phase0Gate(read, { path, controllerAt: controller?.at, controller: controllerState(controller, NAME) }).session;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// The names in the session directory. A missing one reads as empty when the
|
|
778
|
+
// receipt named it, as the tick reads it, and refuses otherwise.
|
|
779
|
+
function sessionListing(session, receiptNamed) {
|
|
780
|
+
try {
|
|
781
|
+
return new Set(readdirSync(session));
|
|
782
|
+
} catch (e) {
|
|
783
|
+
if (e.code === "ENOENT" && receiptNamed) return new Set();
|
|
784
|
+
return die(`member-died: cannot list the session directory ${session} (${e.code ?? e.message}) — nothing written`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Settles a member the controller has confirmed dead, by the first marker
|
|
789
|
+
// that matches; see the header. The settle is a ledger write, so its
|
|
790
|
+
// refusal passes through to the exit as every other kind's does. Every other
|
|
791
|
+
// refusal is a die() before anything is written or ticked.
|
|
792
|
+
function memberDied(ev, member, receipt, flagSession) {
|
|
793
|
+
const review = REVIEWER.exec(member);
|
|
794
|
+
if (review !== null) die(`member-died ${member}: a review runner settles nothing; its death is review-failed ${review[1]} member:${member} — nothing written`);
|
|
795
|
+
const m = parseMember(member);
|
|
796
|
+
if (m === null) die(`member-died: '${member}' is not a ledger member — nothing written`);
|
|
797
|
+
const before = settledOutcome(ev.ledger, member);
|
|
798
|
+
if (before !== null) {
|
|
799
|
+
ev.note(`${member}=${before} already settled`);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const report = ev.report(member);
|
|
803
|
+
const kind = reportKind(m.family);
|
|
804
|
+
if (report.state === "invalid") die(`member-died ${member}: its report ${report.path} is on disk and ${kind} cannot settle from it (${report.violation}): fix the report and run ${kind} ${member}, or settle ${member} by hand — nothing written`);
|
|
805
|
+
if (report.state !== "absent") die(`member-died ${member}: its report ${report.path} is on disk; ${kind} ${member} settles it — nothing written`);
|
|
806
|
+
// The session directory the tick reads: this run's receipt's `session`,
|
|
807
|
+
// else `--session`. A `--session` the receipt's overrides is said, not
|
|
808
|
+
// dropped in silence.
|
|
809
|
+
const receiptSession = runSession(receipt);
|
|
810
|
+
if (receiptSession !== null && flagSession !== null && resolve(flagSession) !== resolve(receiptSession)) {
|
|
811
|
+
writeAll(2, `${NAME}: member-died ${member} reads ${receiptSession}, this run's phase-0 receipt's session, not --session ${flagSession}\n`);
|
|
812
|
+
}
|
|
813
|
+
const session = receiptSession ?? flagSession;
|
|
814
|
+
if (session === null) die(`member-died ${member} needs the session directory: this run's phase-0 receipt names none, so pass --session <dir> — nothing written`);
|
|
815
|
+
const listed = sessionListing(session, receiptSession !== null);
|
|
816
|
+
const md = `${member}.md`;
|
|
817
|
+
const jsonl = `${member}.jsonl`;
|
|
818
|
+
if (listed.has(`${jsonl}.tombstone`)) return ev.settle(member, "killed");
|
|
819
|
+
if (listed.has(md)) {
|
|
820
|
+
if (m.family === "impl") die(`member-died ${member}: ${md} is in ${session} with no tombstone, and an implementer's yield is its own outcome: read agent://${member} and settle it by hand — nothing written`);
|
|
821
|
+
return ev.settle(member, m.family === "merge-bot" ? "killed" : "failed");
|
|
822
|
+
}
|
|
823
|
+
if (listed.has(jsonl)) die(`member-died ${member}: ${member} has a transcript and no marker in ${session}, so it is running or parked; census it, kill it, then rerun — nothing written`);
|
|
824
|
+
const heartbeat = statePath("fleet-event");
|
|
825
|
+
const state = readState(heartbeat, "fleet-event");
|
|
826
|
+
if (state.degraded) die(`member-died ${member}: ${heartbeat} ${state.degraded} — nothing written`);
|
|
827
|
+
const stamp = state.seen[member];
|
|
828
|
+
if (typeof stamp !== "number") die(`member-died ${member}: no transcript and no first-seen stamp in ${heartbeat}, so the tick never saw it live — nothing written`);
|
|
829
|
+
const age = Date.now() - stamp;
|
|
830
|
+
if (age <= NO_TRANSCRIPT_MS) die(`member-died ${member}: first seen ${Math.floor(age / 60_000)} min ago, not yet past the ${NO_TRANSCRIPT_MS / 60_000}-minute no-transcript bound — nothing written`);
|
|
831
|
+
return ev.settle(member, "killed");
|
|
832
|
+
}
|
|
833
|
+
|
|
730
834
|
// ── main ────────────────────────────────────────────────────────────────────
|
|
731
835
|
|
|
732
836
|
function boundArg(F, name) {
|
|
@@ -764,6 +868,7 @@ function main() {
|
|
|
764
868
|
} else if (reason !== null) {
|
|
765
869
|
die(`--reason belongs to drain and undrain, not ${kind}`);
|
|
766
870
|
}
|
|
871
|
+
if (kind !== "member-died" && F.arg("session") !== null) die(`--session belongs to member-died, not ${kind}`);
|
|
767
872
|
let pr = null;
|
|
768
873
|
if (spec.family !== undefined) {
|
|
769
874
|
const m = parseMember(args[0]);
|
|
@@ -776,9 +881,11 @@ function main() {
|
|
|
776
881
|
|
|
777
882
|
let fleetDir;
|
|
778
883
|
let scratch;
|
|
884
|
+
let receipt;
|
|
779
885
|
try {
|
|
780
886
|
fleetDir = fleetFile(null);
|
|
781
|
-
|
|
887
|
+
receipt = readRunReceipt();
|
|
888
|
+
const { read } = receipt;
|
|
782
889
|
const named = read.state === "ok" ? read.receipt.scratch : undefined;
|
|
783
890
|
scratch = typeof named === "string" && named !== "" ? named : defaultScratch();
|
|
784
891
|
} catch (e) {
|
|
@@ -788,7 +895,6 @@ function main() {
|
|
|
788
895
|
const caps = tickCaps(fleetDir);
|
|
789
896
|
if (caps.error) die(`${caps.error} — nothing written`);
|
|
790
897
|
const tickArgs = ["--apply", ...(inFlight === null ? caps.args : ["--in-flight", inFlight]), ...(kind === "heartbeat" ? ["--fold-unchanged"] : [])];
|
|
791
|
-
|
|
792
898
|
const ev = new Event({ fleetDir, scratch, ledger: readLedger() });
|
|
793
899
|
if (kind === "impl-report") implReport(ev, args[0]);
|
|
794
900
|
else if (kind === "fix-report") fixReport(ev, args[0]);
|
|
@@ -800,6 +906,7 @@ function main() {
|
|
|
800
906
|
else if (kind === "conflict") conflict(ev, pr);
|
|
801
907
|
else if (kind === "drain") drain(ev, reason);
|
|
802
908
|
else if (kind === "undrain") undrain(ev, reason);
|
|
909
|
+
else if (kind === "member-died") memberDied(ev, args[0], receipt, F.arg("session"));
|
|
803
910
|
|
|
804
911
|
writeAll(1, `${[...ev.lines, `event: ${kind}: ${ev.summary.join("; ") || "nothing to record"}`].join("\n")}\n`);
|
|
805
912
|
const tick = spawnSync(process.execPath, [join(SCRIPTS, "fleet-tick.mjs"), ...tickArgs], { stdio: ["ignore", "inherit", "inherit"], env: gitEnv() });
|
package/scripts/fleet-tick.mjs
CHANGED
|
@@ -1295,6 +1295,10 @@ export function unrecordedLines({ stems, recorded }) {
|
|
|
1295
1295
|
});
|
|
1296
1296
|
}
|
|
1297
1297
|
|
|
1298
|
+
// The `--no-transcript-after` default, in minutes: its one definition, which
|
|
1299
|
+
// fleet-event.mjs's member-died reads as its own no-transcript bound.
|
|
1300
|
+
export const NO_TRANSCRIPT_AFTER_DEFAULT = 5;
|
|
1301
|
+
|
|
1298
1302
|
// The NO-TRANSCRIPT lines: one per live member token in `live` whose `seen`
|
|
1299
1303
|
// stamp is more than `after` minutes old and whose `<member>.jsonl` is not
|
|
1300
1304
|
// among `stems`, as `{kind, name, line, key}` — a ledger token whose `task` call
|
|
@@ -1481,7 +1485,7 @@ const OPTIONS = {
|
|
|
1481
1485
|
session: { type: "string", default: "" },
|
|
1482
1486
|
// Minutes a live member token may go with no transcript in the session
|
|
1483
1487
|
// directory before NO-TRANSCRIPT names it.
|
|
1484
|
-
"no-transcript-after": { type: "string", default:
|
|
1488
|
+
"no-transcript-after": { type: "string", default: String(NO_TRANSCRIPT_AFTER_DEFAULT) },
|
|
1485
1489
|
// Minutes a Pull's question may stand, its `pull.json` unwritten, before the
|
|
1486
1490
|
// PULL row naming it says `(stale)`.
|
|
1487
1491
|
"pull-stale-minutes": { type: "string", default: "120" },
|
|
@@ -1867,15 +1871,16 @@ function claimed() {
|
|
|
1867
1871
|
|
|
1868
1872
|
// What the controller record's process is now, for the phase-0 gate: `none`
|
|
1869
1873
|
// with no record, else judgeRecord()'s answer. A process table that cannot
|
|
1870
|
-
// be read is `unreadable`, said on stderr
|
|
1871
|
-
// is not one whose receipt
|
|
1872
|
-
|
|
1874
|
+
// be read is `unreadable`, said on stderr under the caller's `name`: a
|
|
1875
|
+
// controller nobody could judge is not one whose receipt may be taken as
|
|
1876
|
+
// this run's.
|
|
1877
|
+
export function controllerState(controller, name = NAME) {
|
|
1873
1878
|
if (controller === null) return "none";
|
|
1874
1879
|
try {
|
|
1875
1880
|
return judgeRecord(controller);
|
|
1876
1881
|
} catch (e) {
|
|
1877
1882
|
if (!(e instanceof ProcUnreadable)) throw e;
|
|
1878
|
-
console.error(`${
|
|
1883
|
+
console.error(`${name}: WARNING ${e.message} — the recorded controller pid ${controller.pid} cannot be judged, so the phase-0 receipt is held`);
|
|
1879
1884
|
return "unreadable";
|
|
1880
1885
|
}
|
|
1881
1886
|
}
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
// rows the next drain finds already there, so a second run changes nothing.
|
|
17
17
|
// A pending file that is empty, or blank lines only, is removed with its own
|
|
18
18
|
// line: an append created it and never wrote.
|
|
19
|
+
// The drain takes no lock by design: it assumes only the run's controller
|
|
20
|
+
// appends to the pending file and drains it, serially, so nothing appends
|
|
21
|
+
// between the read and either unlink. One controller per checkout is the
|
|
22
|
+
// rule, but phase 0 refuses a second one at its ledger rotation, which runs
|
|
23
|
+
// after its drain step, so a second controller's drain is not guarded.
|
|
19
24
|
//
|
|
20
25
|
// COST GUARD. `pr-cost.mjs --guard` rewrites `.fleet/cost-guard.json`, which
|
|
21
26
|
// the router reads and fails closed without. It needs all three metrics TSVs;
|
|
@@ -601,6 +601,10 @@ function main() {
|
|
|
601
601
|
issue: readJson(issuePath), sizing: readJson(F.arg("sizing")), guard: readGuard(readJson(guardPath)), table,
|
|
602
602
|
});
|
|
603
603
|
const pending = F.arg("pending") ?? join(dirname(guardPath), "ticket-features.pending.tsv");
|
|
604
|
+
// The header decision and the append are not atomic and take no lock. That is
|
|
605
|
+
// safe only while the run's controller is the only process that routes or
|
|
606
|
+
// drains on a checkout, doing both serially: one controller per checkout is
|
|
607
|
+
// the rule, and neither this append nor the drain checks it.
|
|
604
608
|
try {
|
|
605
609
|
mkdirSync(dirname(pending), { recursive: true });
|
|
606
610
|
const header = statSync(pending, { throwIfNoEntry: false })?.size > 0 ? "" : `${COLUMNS.join("\t")}\n`;
|
package/skills/run-team/SKILL.md
CHANGED
|
@@ -203,6 +203,7 @@ Run each as `~/.fleet/bin/fleet-run fleet-event.mjs <kind> <args>`, from the mai
|
|
|
203
203
|
| Heartbeat | `fleet-event.mjs heartbeat` |
|
|
204
204
|
| Drain | `fleet-event.mjs drain --reason "<reason>"` |
|
|
205
205
|
| Undrain | `fleet-event.mjs undrain --reason "<reason>"` |
|
|
206
|
+
| Member confirmed dead or never dispatched | `fleet-event.mjs member-died <member>` |
|
|
206
207
|
|
|
207
208
|
### Nothing to do: arm the beat
|
|
208
209
|
|
|
@@ -280,7 +281,7 @@ depth** guard table applied in code. Act on each line as it reads:
|
|
|
280
281
|
- `PHASE0-INCOMPLETE <step>…` — every dispatching row is held: run the `phase0.mjs` command the line names; advice lines hold nothing.
|
|
281
282
|
- `CHECK-LIVENESS <member> (…)` — advice, never an action: take the census the **Member unaccounted** row of **Failure handling** gives before you touch the token.
|
|
282
283
|
- `UNRECORDED <stem> (…)` — record it with `ledger.mjs dispatch`, or confirm it is a stranger's member.
|
|
283
|
-
- `NO-TRANSCRIPT <member> (…)` — make the `task` call, or
|
|
284
|
+
- `NO-TRANSCRIPT <member> (…)` — make the `task` call, or run the **Member confirmed dead or never dispatched** wake.
|
|
284
285
|
- `SETTLE <member> (report on disk …)` — a live token whose report is on disk: run that member's report wake from the wake table, which runs the `ledger.mjs settle <member> --from-report` the line prints plus the follow-on work its report calls for; `REPORT-MISMATCH`, `REPORT-UNREADABLE` and `REPORT-INVALID` name a report file that cannot be settled from, and the tick settles nothing itself.
|
|
285
286
|
|
|
286
287
|
## Phase 1 — Pull: judge, then claim or relabel
|
|
@@ -1296,11 +1297,7 @@ owes beyond its call:
|
|
|
1296
1297
|
nothing the tick did not name.
|
|
1297
1298
|
- **Finisher reports halted** → the per-cause rule (**Resolving a finisher
|
|
1298
1299
|
halt**, above).
|
|
1299
|
-
- **A finisher failed or was killed** → `
|
|
1300
|
-
finisher that crashed or gave up, `=killed` for one that was killed; no label,
|
|
1301
|
-
and the tick prints no dispatch for it. The cockpit flags the PR
|
|
1302
|
-
`finisher:failed` / `finisher:killed` at severity 4 and you resolve it by hand
|
|
1303
|
-
(**Resolving a finisher that died**, above), then run the tick.
|
|
1300
|
+
- **A finisher failed or was killed** → the **Member confirmed dead or never dispatched** wake (`member-died finisher-pr-<M>`), which settles it `failed` or `killed`; no label, and the tick prints no dispatch for it. The cockpit flags the PR `finisher:failed` / `finisher:killed` at severity 4 and you resolve it by hand (**Resolving a finisher that died**, above), then run the tick.
|
|
1304
1301
|
- **Implementer bails before implementing** → `fleet-event.mjs impl-report impl-<N>`; the causes and the issue action each takes are under **Relabel causes**.
|
|
1305
1302
|
- **A review result lands** → see the result file written (by you or by the
|
|
1306
1303
|
runner — **Reviewers** says which, per harness), then
|
|
@@ -2609,8 +2606,8 @@ Match the failure to its row in the table below and run that response; where the
|
|
|
2609
2606
|
| Merge bot's CI wait reaches its per-PR time bound with the run's `status` still not `completed` | The bot skips that PR and reports `ci-wait-timeout-#<pr>` with the run id and its status. PR stays queued, label untouched |
|
|
2610
2607
|
| Member silent or truncated | Send to ping or resume — same unit of work; see the state machine below |
|
|
2611
2608
|
| Member idle with work outstanding | Read the PR first, *then* ping. Idle ≠ done |
|
|
2612
|
-
| Member **killed** (spend limit, API error, crash) | Confirm it is dead first — a frozen transcript does not establish that; see below — then new member, new name, prompt carries inherited state |
|
|
2613
|
-
| Member **unaccounted** (a live token the tick flags `CHECK-LIVENESS`, or any live token nothing has reported on) | Registry census first: `read history://` once. A row other than `running` → `read agent://<member>` and the artifacts (PR head, worktree, labels), settle the real outcome with the existing vocabulary, and resume a `parked` member with `write agent://<member>` when its work is worth continuing. A `running` row → the mtime discriminator below, two polls: moved means BLOCKED and alive; frozen at both polls **and** no artifact movement → `write proc://<member>/kill`,
|
|
2609
|
+
| Member **killed** (spend limit, API error, crash) | Confirm it is dead first — a frozen transcript does not establish that; see below — then run the **Member confirmed dead or never dispatched** wake, then new member, new name, prompt carries inherited state |
|
|
2610
|
+
| Member **unaccounted** (a live token the tick flags `CHECK-LIVENESS`, or any live token nothing has reported on) | Registry census first: `read history://` once. A row other than `running` → `read agent://<member>` and the artifacts (PR head, worktree, labels), settle the real outcome with the existing vocabulary, and resume a `parked` member with `write agent://<member>` when its work is worth continuing. A `running` row → the mtime discriminator below, two polls: moved means BLOCKED and alive; frozen at both polls **and** no artifact movement → `write proc://<member>/kill`, the **Member confirmed dead or never dispatched** wake, fresh name. Three independent alive-signals — the first poll, the second poll, the artifacts — must all be silent before a kill; the tick settles and kills nothing |
|
|
2614
2611
|
|
|
2615
2612
|
### Recovery
|
|
2616
2613
|
|
|
@@ -2623,8 +2620,6 @@ spellings. Recovery is a fresh member, fresh name (`impl-<N>-b`,
|
|
|
2623
2620
|
`fix-pr-<M>-b`, and for a reviewer the next letter PR M's rows do not carry
|
|
2624
2621
|
yet) whose prompt states what it inherits.
|
|
2625
2622
|
|
|
2626
|
-
`hub cancel` leaves a peer hard-aborted and unmessageable, but a `failed` job's peer can stay `idle` and answer normally. No distinct truncated state exists.
|
|
2627
|
-
|
|
2628
2623
|
Reviewers that went idle on CI recover as a **finisher, not a re-review** once
|
|
2629
2624
|
commits are pushed. See references/member-lifecycle.md.
|
|
2630
2625
|
|
|
@@ -151,7 +151,9 @@ The full text of each row `fleet-tick.mjs` prints and what it asks of the contro
|
|
|
151
151
|
above the rows — a live ledger token first seen more than
|
|
152
152
|
`--no-transcript-after` minutes ago (default 5) has no transcript in the
|
|
153
153
|
session directory: a dispatch whose `task` call was never made. Make the
|
|
154
|
-
`task` call, or
|
|
154
|
+
`task` call, or run the **Member confirmed dead or never dispatched** wake
|
|
155
|
+
(`fleet-event.mjs member-died <member>`), which settles the token `killed`
|
|
156
|
+
once the first-seen stamp is past five minutes. Advice the same way, folded as
|
|
155
157
|
`…; NO-TRANSCRIPT <member> persists — …`. Both lines read the session
|
|
156
158
|
directory this run's phase-0 receipt names, else `--session <dir>`; with
|
|
157
159
|
neither the tick skips them with one stderr line. A heartbeat state file
|