@feigi/fleet-ctl 3.85.0 → 3.86.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/commands/review-and-fix.md +1 -1
- package/package.json +1 -1
- package/scripts/compute-spend.mjs +2 -2
- package/scripts/controller-alarm.mjs +8 -6
- package/scripts/fleet-heartbeat.mjs +2 -2
- package/scripts/fleet-tick.mjs +3 -2
- package/scripts/ledger.mjs +16 -14
- package/scripts/phase0-receipt.mjs +1 -1
- package/scripts/phase0.mjs +70 -24
- package/scripts/staleness.mjs +2 -2
- package/scripts/tier-check.mjs +14 -11
- package/scripts/tier-outcomes.mjs +2 -1
- package/skills/run-team/SKILL.md +2 -2
- package/skills/run-team/references/phase-0.md +5 -1
|
@@ -76,7 +76,7 @@ jq '.refuted' "$R" # refuted: your
|
|
|
76
76
|
|
|
77
77
|
`eval` loading `scripts/review-eval.mjs` through the Resolver (`fleet-run --path review-eval.mjs`) and calling `runReviewOnOmp({pr, branch, worktree, testCmd, scratch})` is the fleet's **default** review path. Its `scratch` is the scratch root, never `<scratch>/pr<N>`: the review creates that partition itself and refuses a `scratch` already ending in one, or one that is not an absolute path.
|
|
78
78
|
|
|
79
|
-
`$(~/.fleet/bin/fleet-run --root)/skills/run-team/SKILL.md`'s
|
|
79
|
+
`$(~/.fleet/bin/fleet-run --root)/skills/run-team/SKILL.md`'s **The reviewer** bullet owns its dispatch, and `references/reviewers.md` (**A review runs off your turn and relays nothing**) says what it does. It cuts the snapshot, sizes the fan-out, adversarially verifies every **critical/important** finding, and has `agent()` return **into the script**, so no report can go undelivered, the failure that cost one fleet five reports on one PR and four on another. It does **not** verify the `suggestion` band — that gets 0 refuters by policy, which is why step 2 checks each one itself — an in-scope one before applying it, an out-of-scope one above the claim bar before filing it. **You cannot run it** — the one member that runs it is the `review-pr-<pr#>` runner (`agents/fleet-review-runner.agent.md`), never a reader of this file.
|
|
80
80
|
|
|
81
81
|
It cannot ask the user anything and cannot wait on CI — rebasing, watching checks, binding and labelling stay here.
|
|
82
82
|
|
package/package.json
CHANGED
|
@@ -167,8 +167,8 @@ function roleFromNamePatterns(hay) {
|
|
|
167
167
|
// dispatch against an ALREADY-OPEN PR, not new ticket work — measured
|
|
168
168
|
// meta.json descriptions "Resolve conflict on PR <n>" and "Rebase and
|
|
169
169
|
// resolve conflicts for PR #<n>", the same rebase/conflict-resolver shape
|
|
170
|
-
// `run-team/
|
|
171
|
-
// worktree. It carries no ticket, no dispatch counter and no new-work verb —
|
|
170
|
+
// `run-team/references/isolation.md` calls "a rebase-resolver" in a prior-run
|
|
171
|
+
// worktree, here sent into an open PR's worktree. It carries no ticket, no dispatch counter and no new-work verb —
|
|
172
172
|
// only a PR number — so it is remediation on that PR's path to merge,
|
|
173
173
|
// exactly the category `fix-pr-<n>`'s applier already occupies here. It is
|
|
174
174
|
// NOT `merge-bot`: that bucket's number is a per-run dispatch counter, never
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
// THE PROOF IT IS ARMED. At `session_start`, for the main session only, and
|
|
46
46
|
// only AFTER the timer is set, it writes `~/.fleet/alarm/<pid>.json` holding
|
|
47
47
|
// `{ pid, lstart, fleet }`: this process's pid, its start time as `proc.mjs`
|
|
48
|
-
// reads it, and the `.fleet/` path it resolved.
|
|
49
|
-
//
|
|
48
|
+
// reads it, and the `.fleet/` path it resolved. Phase 0's `alarm` step reads
|
|
49
|
+
// the file and refuses unless it names this omp and this `.fleet/`. A
|
|
50
50
|
// session that set no timer writes no file, so the file proves the timer is
|
|
51
51
|
// armed against that repository's `.fleet/` and not only that the extension
|
|
52
52
|
// loaded. A start time that cannot be read writes no file, the safe direction:
|
|
@@ -59,13 +59,15 @@
|
|
|
59
59
|
|
|
60
60
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
61
61
|
import { homedir } from "node:os";
|
|
62
|
-
import { join } from "node:path";
|
|
62
|
+
import { dirname, join } from "node:path";
|
|
63
63
|
import { fleetFile, FleetDirUnresolvable } from "./fleet-dir.mjs";
|
|
64
64
|
import { readState, stateFileIn } from "./fleet-state.mjs";
|
|
65
65
|
import { processSource } from "./proc.mjs";
|
|
66
66
|
|
|
67
67
|
export const POLL_MS = 30_000;
|
|
68
68
|
export const ALARM_LINE = "[fleet-alarm] run ~/.fleet/bin/fleet-run fleet-event.mjs heartbeat";
|
|
69
|
+
// Where the extension running as `pid` records that its timer is armed; phase 0 reads it there.
|
|
70
|
+
export const alarmFile = (pid) => join(homedir(), ".fleet", "alarm", `${pid}.json`);
|
|
69
71
|
|
|
70
72
|
/**
|
|
71
73
|
* Whether the alarm fires now. `beat` is `{ at, interval, stopped }` (`at` in
|
|
@@ -132,7 +134,7 @@ function arm(pi, ctx) {
|
|
|
132
134
|
// An unreadable process table is an unreadable start time.
|
|
133
135
|
}
|
|
134
136
|
if (typeof lstart !== "string" || lstart === "") return;
|
|
135
|
-
const
|
|
136
|
-
mkdirSync(
|
|
137
|
-
writeFileSync(
|
|
137
|
+
const file = alarmFile(process.pid);
|
|
138
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
139
|
+
writeFileSync(file, `${JSON.stringify({ pid: process.pid, lstart, fleet: fleetDir })}\n`);
|
|
138
140
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// WHY A HOLD AND NOT A WATCHER — the ruling, measured before it was made:
|
|
14
14
|
//
|
|
15
15
|
// A backgrounded watcher does not wake an idle agent. Measured twice in one
|
|
16
|
-
// run (run-team/
|
|
16
|
+
// run (run-team/references/member-lifecycle.md, "A member runs its test suite in the foreground"):
|
|
17
17
|
// `fix-pr-1184` backgrounded its suite plus a Monitor and sat idle for
|
|
18
18
|
// roughly two hours; neither ever woke it. run-merge-bot.md's CI gate states
|
|
19
19
|
// the same rule as a rule — "whatever wakes you is external and may never
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// do.
|
|
29
29
|
//
|
|
30
30
|
// WHY IT TAKES NO CONTROLLER STATE — every flag below has a default. A monitor
|
|
31
|
-
// event is a wake-up, never a verdict (SKILL.md
|
|
31
|
+
// event is a wake-up, never a verdict (run-team/SKILL.md, "A monitor event is a wake-up, never a verdict"), so this
|
|
32
32
|
// script never reads or reports fleet state: counts captured before a
|
|
33
33
|
// twenty-minute hold and printed after it are a stale verdict, and a stale
|
|
34
34
|
// verdict is how a reconcile prints an ACTION nobody can take. The controller
|
package/scripts/fleet-tick.mjs
CHANGED
|
@@ -239,8 +239,9 @@ const mkRow = (role, actual, target, detail) => (action, { acts = false, extra =
|
|
|
239
239
|
// transcript; the session directory is the phase-0 receipt's, which
|
|
240
240
|
// tier-check.mjs reads itself when an entry omits it. Without a receipt of
|
|
241
241
|
// this run, `<session>` stays a placeholder the controller fills in, as
|
|
242
|
-
// `<file>` does — one batch file, one entry per held member
|
|
243
|
-
//
|
|
242
|
+
// `<file>` does — one batch file, one entry per held member, the shape under
|
|
243
|
+
// **The batch file is a JSON array, one entry per dispatched member** in
|
|
244
|
+
// run-team/references/tick-rows.md.
|
|
244
245
|
const tierCheckStep = (members, session) => "run ~/.fleet/bin/fleet-run tier-check.mjs --batch <file> with "
|
|
245
246
|
+ JSON.stringify(members.map((member) => (session ? { member } : { member, session: "<session>" })));
|
|
246
247
|
|
package/scripts/ledger.mjs
CHANGED
|
@@ -293,15 +293,16 @@ if (!cmd) die("usage: ledger.mjs [--file <path>] [--require-file] row|token|file
|
|
|
293
293
|
// the documented convention IS one quoted argument, and `check` is the only
|
|
294
294
|
// subcommand where it is: every caller that instructs running it spells it
|
|
295
295
|
// `ledger.mjs check "<subject>"` (the finisher's deferred-finding filing duty,
|
|
296
|
-
// review-and-fix's filing step), while run-team/SKILL.md
|
|
297
|
-
//
|
|
298
|
-
// `ruled <pr> <decision>` — a bare multi-word tail,
|
|
299
|
-
// usage strings for them. Applied there, the rule
|
|
300
|
-
//
|
|
296
|
+
// review-and-fix's filing step), while run-team/SKILL.md, as it stood when
|
|
297
|
+
// this was measured, spelled the other three `filed <issue> <subject>`,
|
|
298
|
+
// `row <ticket> <text>` and `ruled <pr> <decision>` — a bare multi-word tail,
|
|
299
|
+
// as did this file's own usage strings for them. Applied there, the rule
|
|
300
|
+
// refused what the docs prescribed: measured on the tree ahead of this
|
|
301
|
+
// narrowing, `filed 999 the
|
|
301
302
|
// --basee flag is unread`, `row 42 the --basee flag is unread` and `ruled 77
|
|
302
303
|
// the --basee flag is unread` each exited 2, where the tree before this guard
|
|
303
304
|
// existed answered all three at exit 0. The guard was ruled back to
|
|
304
|
-
// `check` rather than respelling three conventions SKILL.md
|
|
305
|
+
// `check` rather than respelling three conventions SKILL.md then carried, two
|
|
305
306
|
// other tickets holding that file open.
|
|
306
307
|
//
|
|
307
308
|
// Stated here and nowhere else. This paragraph is the one copy of why
|
|
@@ -329,9 +330,9 @@ if (!cmd) die("usage: ledger.mjs [--file <path>] [--require-file] row|token|file
|
|
|
329
330
|
// 2 now. Quoting the subject accepts it unchanged. The trade taken
|
|
330
331
|
// is that this refusal is loud and recoverable where the wrong-subject
|
|
331
332
|
// answer it replaces was silent. One residual of that trade is not this
|
|
332
|
-
// file's to close:
|
|
333
|
-
// unquoted
|
|
334
|
-
//
|
|
333
|
+
// file's to close: run-team/references/ledger.md's `filed and ruled` section
|
|
334
|
+
// spells `check <subject>` unquoted, where the instructions that tell a caller
|
|
335
|
+
// to RUN it quote the subject.
|
|
335
336
|
//
|
|
336
337
|
// Narrowed on `check` too, not closed: a lone stray with no subject beside it
|
|
337
338
|
// is a one-element tail, so it is still taken as the subject and answered at
|
|
@@ -652,15 +653,15 @@ async function acquireLock() {
|
|
|
652
653
|
// indistinguishable at this instant from a live reaper mid-takeover.
|
|
653
654
|
// Naming the pid alone points an operator at a process that no longer
|
|
654
655
|
// exists; naming `${REAP}` is what lets them find the file
|
|
655
|
-
//
|
|
656
|
-
// nothing is writing.
|
|
656
|
+
// run-team/references/ledger.md's Writer policy section tells them to
|
|
657
|
+
// remove by hand, once nothing is writing.
|
|
657
658
|
if (dead) {
|
|
658
659
|
die(`cannot lock ${file} — held by pid ${pid}, which is dead; ${REAP} is blocking its takeover — if nothing is currently writing, remove it by hand`);
|
|
659
660
|
}
|
|
660
661
|
// No pid at all is the other permanent wedge pid-lock.mjs's header describes: an
|
|
661
662
|
// empty or partial `${LOCK}` whose writer died before writing its pid,
|
|
662
663
|
// read as live forever by design. There is no process to point at, so
|
|
663
|
-
// name `${LOCK}` itself — the file the same
|
|
664
|
+
// name `${LOCK}` itself — the file the same Writer policy section says to remove.
|
|
664
665
|
if (pid === null) {
|
|
665
666
|
die(`cannot lock ${file} — ${LOCK} holds ${JSON.stringify(holder)}, not a pid, so it is never taken over — if nothing is currently writing, remove it by hand`);
|
|
666
667
|
}
|
|
@@ -2087,8 +2088,9 @@ function runDispatch() {
|
|
|
2087
2088
|
line = `#${member.number} ${member.name}`;
|
|
2088
2089
|
data.rows.push(line);
|
|
2089
2090
|
} else if (i !== -1) {
|
|
2090
|
-
// A row `row` already wrote the live token into (SKILL.md's
|
|
2091
|
-
//
|
|
2091
|
+
// A row `row` already wrote the live token into (run-team/SKILL.md's
|
|
2092
|
+
// `NEED resume` bullet records the row with `ledger.mjs row` before
|
|
2093
|
+
// `ledger.mjs dispatch`) keeps it as it is rather than gaining a second copy.
|
|
2092
2094
|
const row = data.rows[i];
|
|
2093
2095
|
line = carries(row, member.name, true) ? row : `${row} · ${member.name}`;
|
|
2094
2096
|
data.rows[i] = line;
|
|
@@ -18,7 +18,7 @@ const isRecord = (x) => x !== null && typeof x === "object" && !Array.isArray(x)
|
|
|
18
18
|
// a step not yet run being `pending`. A receipt missing one was not written
|
|
19
19
|
// by a finished phase 0.
|
|
20
20
|
export const STEP_NAMES = Object.freeze([
|
|
21
|
-
"branch", "fast-forward", "pin", "baseline", "preflight", "ruleset", "tier-roles", "metrics",
|
|
21
|
+
"alarm", "branch", "fast-forward", "pin", "baseline", "preflight", "ruleset", "tier-roles", "metrics",
|
|
22
22
|
"cockpit", "rotate", "reclaim", "recipe", "fold-in", "stranded", "shortlist",
|
|
23
23
|
]);
|
|
24
24
|
|
package/scripts/phase0.mjs
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
// reclaim straight after the rotation, the metrics refresh before the first
|
|
21
21
|
// Pull is routed:
|
|
22
22
|
//
|
|
23
|
+
// alarm `~/.fleet/alarm/<pid>.json` names this omp's pid and start time
|
|
24
|
+
// and this `.fleet/`. Missing, malformed or mismatched, or no omp
|
|
25
|
+
// above the script, refuses: restart omp.
|
|
23
26
|
// branch `git symbolic-ref -q HEAD` is refs/heads/main. A checkout left
|
|
24
27
|
// on a branch whose PR has merged would fast-forward that branch
|
|
25
28
|
// and the baseline would record it as the run's ref.
|
|
@@ -82,29 +85,31 @@
|
|
|
82
85
|
// is the In-flight bound this run used — `--in-flight`, or its default.
|
|
83
86
|
//
|
|
84
87
|
// RESUMABLE. A re-run reads the receipt. It continues it — skipping every `ok`
|
|
85
|
-
// step and re-running the rest — only when its `run` equals the
|
|
86
|
-
// controller record holds now AND its `session` equals this run's
|
|
87
|
-
// comparison alone does not separate two runs before the new
|
|
88
|
-
// the record still carries the previous run's `at` until
|
|
89
|
-
// writes the next, so the previous run's receipt still
|
|
90
|
-
// re-run that trusted `run` alone would skip `rotate`. The
|
|
91
|
-
// a new session.
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// `phase0.json
|
|
96
|
-
//
|
|
97
|
-
//
|
|
88
|
+
// step but `alarm` and re-running the rest — only when its `run` equals the
|
|
89
|
+
// `at` the controller record holds now AND its `session` equals this run's
|
|
90
|
+
// session. That comparison alone does not separate two runs before the new
|
|
91
|
+
// run's rotation: the record still carries the previous run's `at` until
|
|
92
|
+
// `ledger.mjs rotate` writes the next, so the previous run's receipt still
|
|
93
|
+
// matches it, and a re-run that trusted `run` alone would skip `rotate`. The
|
|
94
|
+
// session rule closes a new session. `alarm` is re-run on every invocation,
|
|
95
|
+
// `ok` or not: it proves something about the omp running this invocation, and
|
|
96
|
+
// a resumed receipt may have been written under another. A receipt of any
|
|
97
|
+
// other run (a `run` that is set and does not match, or a different session)
|
|
98
|
+
// is held as `phase0.previous.json`, where `closeout.mjs finish` also leaves a
|
|
99
|
+
// finished run's receipt, and the script starts at `alarm`; this run's own
|
|
100
|
+
// receipt is written at `phase0.json` after every step. A receipt whose
|
|
101
|
+
// rotation has not run is simply overwritten: every step before `rotate` is
|
|
102
|
+
// idempotent and is re-run on every invocation until the rotation has run.
|
|
98
103
|
//
|
|
99
104
|
// THE ROTATION FILES THE PREVIOUS RUN'S RECEIPT. The `rotate` step takes this
|
|
100
105
|
// run's partial receipt off disk, puts the held one back at `phase0.json` and
|
|
101
106
|
// runs `ledger.mjs rotate`, which archives it as `phase0.<UTC stamp>.json`
|
|
102
107
|
// beside `ledger.<UTC stamp>.md`, one stamp for both. A rotation that refused
|
|
103
108
|
// leaves it held for the next attempt; one that archived the ledger and not
|
|
104
|
-
// the receipt files it under a stamp of this script's own.
|
|
105
|
-
//
|
|
106
|
-
// null; a receipt whose `rotate` step is `ok` is then resumed on the
|
|
107
|
-
// alone.
|
|
109
|
+
// the receipt files it under a stamp of this script's own. A rotation that
|
|
110
|
+
// writes no controller record (the ledger finds no omp above itself) leaves
|
|
111
|
+
// `run` null; a receipt whose `rotate` step is `ok` is then resumed on the
|
|
112
|
+
// session alone.
|
|
108
113
|
//
|
|
109
114
|
// A step that fails stops the script; so does a pending one — nothing after
|
|
110
115
|
// the recipe may run before it is proven, and the shortlist waits for the
|
|
@@ -133,16 +138,19 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync,
|
|
|
133
138
|
import { dirname, join, resolve } from "node:path";
|
|
134
139
|
import { fileURLToPath } from "node:url";
|
|
135
140
|
import { makeDie, defineFlags, isDigits } from "./arg.mjs";
|
|
141
|
+
import { alarmFile } from "./controller-alarm.mjs";
|
|
136
142
|
import { FleetDirUnresolvable, fleetFile } from "./fleet-dir.mjs";
|
|
137
143
|
import { readState, stateFileIn } from "./fleet-state.mjs";
|
|
138
144
|
import { gitEnv } from "./git-env.mjs";
|
|
139
145
|
import { isCLI } from "./is-cli.mjs";
|
|
140
146
|
import { RECEIPT, RECEIPT_HELD, archiveStamp, receiptArchiveName, rowNums } from "./ledger-grammar.mjs";
|
|
147
|
+
import { ancestry, controllerOf, processSource, ProcUnreadable } from "./proc.mjs";
|
|
141
148
|
import { IN_FLIGHT_DEFAULT } from "./phase0-receipt.mjs";
|
|
142
149
|
|
|
143
150
|
const NAME = "phase0";
|
|
144
151
|
const SCRIPTS = dirname(fileURLToPath(import.meta.url));
|
|
145
152
|
const MAX_BUFFER = 64 * 1024 * 1024;
|
|
153
|
+
const isRecord = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
|
|
146
154
|
const NETWORK_TIMEOUT_MS = 120_000;
|
|
147
155
|
const PR_LIMIT = 1000;
|
|
148
156
|
// The tail of a child's output is the part that says why it stopped.
|
|
@@ -419,8 +427,46 @@ function shortlist({ cwd }) {
|
|
|
419
427
|
return ok("shortlist built");
|
|
420
428
|
}
|
|
421
429
|
|
|
430
|
+
// The controller alarm: this omp's own record that its timer is armed against
|
|
431
|
+
// this checkout's `.fleet/` (controller-alarm.mjs writes it at session_start).
|
|
432
|
+
// The record is read, not trusted: it must name this omp's pid and start time
|
|
433
|
+
// and this `.fleet/`.
|
|
434
|
+
function alarm({ fleetDir }) {
|
|
435
|
+
const tail = " — restart omp";
|
|
436
|
+
let controller;
|
|
437
|
+
try {
|
|
438
|
+
const source = processSource();
|
|
439
|
+
controller = controllerOf(source, ancestry(source));
|
|
440
|
+
} catch (e) {
|
|
441
|
+
if (!(e instanceof ProcUnreadable)) throw e;
|
|
442
|
+
return failed(`${e.message}${tail}`);
|
|
443
|
+
}
|
|
444
|
+
if (controller === null) return failed(`no omp process above phase 0: the controller alarm cannot be checked${tail}`);
|
|
445
|
+
const path = alarmFile(controller.pid);
|
|
446
|
+
let text;
|
|
447
|
+
try {
|
|
448
|
+
text = readFileSync(path, "utf8");
|
|
449
|
+
} catch (e) {
|
|
450
|
+
if (e.code === "ENOENT") return failed(`controller alarm is not armed: ${path} does not exist${tail}`);
|
|
451
|
+
return failed(`controller alarm file ${path} cannot be read (${e.code ?? e.message})${tail}`);
|
|
452
|
+
}
|
|
453
|
+
let rec;
|
|
454
|
+
try {
|
|
455
|
+
rec = JSON.parse(text);
|
|
456
|
+
} catch (e) {
|
|
457
|
+
return failed(`controller alarm file ${path} is not JSON (${e.message})${tail}`);
|
|
458
|
+
}
|
|
459
|
+
if (!isRecord(rec)) return failed(`controller alarm file ${path} is not an object${tail}`);
|
|
460
|
+
const missing = ["pid", "lstart", "fleet"].filter((k) => !Object.hasOwn(rec, k));
|
|
461
|
+
if (missing.length > 0) return failed(`controller alarm file ${path} lacks ${missing.join(", ")}${tail}`);
|
|
462
|
+
if (rec.pid !== controller.pid) return failed(`controller alarm file ${path} names pid ${rec.pid}, but this omp is pid ${controller.pid}${tail}`);
|
|
463
|
+
if (rec.lstart !== controller.lstart) return failed(`controller alarm file ${path} was written by a process started at "${rec.lstart}", but this omp started at "${controller.lstart}"${tail}`);
|
|
464
|
+
if (rec.fleet !== fleetDir) return failed(`controller alarm file ${path} names fleet "${rec.fleet}", not ${fleetDir}${tail}`);
|
|
465
|
+
return ok(`controller alarm armed: pid ${controller.pid}, ${fleetDir}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
422
468
|
const STEPS = {
|
|
423
|
-
"branch": branch, "fast-forward": fastForward, "pin": pin, "baseline": baseline, "preflight": preflight,
|
|
469
|
+
"alarm": alarm, "branch": branch, "fast-forward": fastForward, "pin": pin, "baseline": baseline, "preflight": preflight,
|
|
424
470
|
"ruleset": ruleset, "tier-roles": tierRoles, "metrics": metrics, "cockpit": cockpit, "rotate": rotate,
|
|
425
471
|
"reclaim": reclaim, "recipe": recipe, "fold-in": foldIn, "stranded": stranded, "shortlist": shortlist,
|
|
426
472
|
};
|
|
@@ -445,8 +491,7 @@ function readReceipt(path) {
|
|
|
445
491
|
} catch (e) {
|
|
446
492
|
throw new Error(`the receipt ${path} is not JSON (${e.message}) — move it aside and re-run`);
|
|
447
493
|
}
|
|
448
|
-
|
|
449
|
-
if (!(record(r) && record(r.steps) && Object.hasOwn(r, "run") && typeof r.session === "string")) {
|
|
494
|
+
if (!(isRecord(r) && isRecord(r.steps) && Object.hasOwn(r, "run") && typeof r.session === "string")) {
|
|
450
495
|
throw new Error(`the receipt ${path} has no steps, run and session — move it aside and re-run`);
|
|
451
496
|
}
|
|
452
497
|
return r;
|
|
@@ -548,9 +593,9 @@ function main() {
|
|
|
548
593
|
return;
|
|
549
594
|
}
|
|
550
595
|
// A receipt belongs to a rotated run when the rotation wrote a controller
|
|
551
|
-
// record it can be keyed on, or,
|
|
552
|
-
//
|
|
553
|
-
//
|
|
596
|
+
// record it can be keyed on, or, when it wrote none, when its rotation is
|
|
597
|
+
// recorded `ok`. Either way it is this run's only if the record now holds
|
|
598
|
+
// the `at` it names and the session is the same.
|
|
554
599
|
const rotated = previous !== null && (previous.run !== null || previous.steps.rotate?.status === "ok");
|
|
555
600
|
const resumed = rotated && previous.run === (record?.at ?? null) && previous.session === session;
|
|
556
601
|
if (rotated && !resumed) {
|
|
@@ -564,7 +609,8 @@ function main() {
|
|
|
564
609
|
|
|
565
610
|
const receipt = { run: resumed ? previous.run : null, session, scratch, inFlight, steps: {} };
|
|
566
611
|
for (const name of STEP_NAMES) {
|
|
567
|
-
|
|
612
|
+
// `alarm` checks the omp running this invocation, so its `ok` is never carried.
|
|
613
|
+
const was = resumed && name !== "alarm" ? previous.steps[name] : undefined;
|
|
568
614
|
receipt.steps[name] = was?.status === "ok" ? was : { status: "pending", at: now, output: "not run" };
|
|
569
615
|
}
|
|
570
616
|
|
package/scripts/staleness.mjs
CHANGED
|
@@ -125,8 +125,8 @@ if (!path) die("--path <path> is required");
|
|
|
125
125
|
// both would refuse, the more specific wording wins, and nothing above here
|
|
126
126
|
// has run a git call.
|
|
127
127
|
//
|
|
128
|
-
// One consequence the caller has to be told about, so run-team/
|
|
129
|
-
// it too: a needle that starts with `--`, given BARE, never reaches this
|
|
128
|
+
// One consequence the caller has to be told about, so run-team/references/pull.md
|
|
129
|
+
// says it too: a needle that starts with `--`, given BARE, never reaches this
|
|
130
130
|
// script's question — `arg()` refuses it as a missing value, at exit 2, which
|
|
131
131
|
// is the verdict that keeps the ticket in the queue. `--gone -- '<value>'`
|
|
132
132
|
// (separatedNeedles() above) is the opt-in past that, one flag at a time.
|
package/scripts/tier-check.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// what omp's own record says it RESOLVED, for every member dispatched by
|
|
6
6
|
// named definition. Not a lint — a refusal, run after every Pull's dispatch,
|
|
7
7
|
// whose verdict on an implementer lands on the ledger, where fleet-tick.mjs
|
|
8
|
-
// holds the next Pull until there is one (run-team/SKILL.md
|
|
8
|
+
// holds the next Pull until there is one (run-team/SKILL.md, **The tier check**).
|
|
9
9
|
//
|
|
10
10
|
// Declared is the definition's own frontmatter — `model: "@<role>:<level>"`,
|
|
11
11
|
// a fleet tier route, never a vendor id. Resolved is read back
|
|
@@ -48,10 +48,11 @@
|
|
|
48
48
|
// A batch file, not one member: `--batch` takes a JSON array, and the
|
|
49
49
|
// failure line — `member: declared <m>/<l> resolved <m>/<l>` — is printed
|
|
50
50
|
// once per member that mismatched, not once per invocation. Under Pull,
|
|
51
|
-
// run-team
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
51
|
+
// run-team runs this after every Pull's dispatch with a batch of one, the
|
|
52
|
+
// member just dispatched (run-team/SKILL.md, **The tier check**). The batch
|
|
53
|
+
// file's exact entry shape and how the controller obtains each field are under
|
|
54
|
+
// **The batch file is a JSON array, one entry per dispatched member** in
|
|
55
|
+
// run-team/references/tick-rows.md.
|
|
55
56
|
//
|
|
56
57
|
// An implementer (`impl-<N>`, ledger-grammar.mjs's name) is judged on the
|
|
57
58
|
// ledger's terms, not the caller's: the definition it should have
|
|
@@ -194,12 +195,14 @@ export function formatOmpExpectation(r) {
|
|
|
194
195
|
// held rather than replacing it, since `row` rewrites the whole line
|
|
195
196
|
// (ledger.mjs's `data.rows[i] = line`) and a bare mismatch note would drop
|
|
196
197
|
// `class=`, `KILLED` or `→ PR#` tokens a prior `row` call wrote for the same
|
|
197
|
-
// ticket. Idempotent on the identical note: the
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
198
|
+
// ticket. Idempotent on the identical note: the check can run again for a
|
|
199
|
+
// member it already judged, while what clears a mismatch is a new member
|
|
200
|
+
// (run-team/SKILL.md, **The tier check**: "Clear a mismatch by fixing the
|
|
201
|
+
// definition or the dispatch and dispatching the replacement at the right
|
|
202
|
+
// tier"), so a re-run that finds the SAME pair for the SAME member adds
|
|
203
|
+
// nothing and must not grow the row a second time — a DIFFERENT note (a
|
|
204
|
+
// changed pair, or the replacement's own line, which names the replacement)
|
|
205
|
+
// still appends, since that is new information the row does not carry yet.
|
|
203
206
|
export function appendedLedgerText(existingText, mismatchNote) {
|
|
204
207
|
const trimmed = String(existingText ?? "").trim();
|
|
205
208
|
if (!trimmed) return mismatchNote;
|
|
@@ -92,7 +92,8 @@ const LEDGER_SCRIPT = join(SCRIPT_DIR, "ledger.mjs");
|
|
|
92
92
|
// ---------------------------------------------------------------------------
|
|
93
93
|
|
|
94
94
|
// APPENDED TO, never inserted into: every awk read-out in the file's header
|
|
95
|
-
// and in run-team/
|
|
95
|
+
// and in run-team/references/pull.md (**Accumulate per PR, never conclude a
|
|
96
|
+
// tier result inside one run**) indexes by position.
|
|
96
97
|
export const COLUMNS = Object.freeze([
|
|
97
98
|
"run_date", "pr", "ticket", "class", "tier", "closed_own_ticket",
|
|
98
99
|
"minted_false_claim", "note", "sizing", "profile", "loc", "files",
|
package/skills/run-team/SKILL.md
CHANGED
|
@@ -50,8 +50,8 @@ phase, or in any later one, asks the maintainer which tickets to take.
|
|
|
50
50
|
when the repository has no workflow named CI. Its steps and receipt: See
|
|
51
51
|
references/phase-0.md. Dispatch nothing before it exits 0. Exit 4 is a
|
|
52
52
|
step that needs you: act on each `NEED` line below, then run it again. Once
|
|
53
|
-
`rotate` has run, a re-run skips every `ok` step and resumes
|
|
54
|
-
one stopped; before it, a re-run runs every step again.
|
|
53
|
+
`rotate` has run, a re-run skips every `ok` step but `alarm` and resumes
|
|
54
|
+
where the last one stopped; before it, a re-run runs every step again.
|
|
55
55
|
|
|
56
56
|
The `tier-roles` step runs `~/.fleet/bin/fleet-run tier-roles.mjs --check
|
|
57
57
|
--in-flight <B>`, `<B>` being the `--in-flight` given, else the default.
|
|
@@ -13,13 +13,17 @@ Exit 3 is a step that failed: its line carries the refusal, so fix the cause
|
|
|
13
13
|
and run it again.
|
|
14
14
|
|
|
15
15
|
`phase0.mjs` runs the start sequence in
|
|
16
|
-
its fixed order — branch, fast-forward, pin, baseline, preflight, ruleset,
|
|
16
|
+
its fixed order — alarm, branch, fast-forward, pin, baseline, preflight, ruleset,
|
|
17
17
|
tier-roles, metrics, cockpit, rotate, reclaim, recipe, fold-in, stranded,
|
|
18
18
|
shortlist — prints one `phase0: <step>: <status>` line per step with the
|
|
19
19
|
`NEED` lines last, and records each step in `.fleet/phase0.json`, which the
|
|
20
20
|
tick reads: it prints `PHASE0-INCOMPLETE` and holds every dispatching row
|
|
21
21
|
until the receipt is whole.
|
|
22
22
|
|
|
23
|
+
## Check the controller alarm
|
|
24
|
+
|
|
25
|
+
**Check the controller alarm first. Phase 0's first step refuses when this omp's timer is not armed.** The `alarm` step reads `~/.fleet/alarm/<pid>.json`, where `<pid>` is the omp process above the script, and checks that the file names that pid, that process's start time, and this checkout's `.fleet/`. A file that is missing, not JSON, or names something else, and no omp process above the script, each fail the step with exit 3 and a message ending `restart omp`. Restart omp, then run phase 0 again. The step runs on every run of phase 0, one its receipt records `ok` included, because it checks the omp running that run; nothing after it runs until it passes.
|
|
26
|
+
|
|
23
27
|
## Fast-forward the checkout
|
|
24
28
|
|
|
25
29
|
**Fast-forward the checkout before you trust these rules — you are reading
|