@feigi/fleet-ctl 3.19.8 → 3.20.0
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 +1 -1
- package/scripts/fleet-tick.test.mjs +1 -1
- package/scripts/ledger-dispatch.test.mjs +34 -9
- package/scripts/ledger-grammar.mjs +10 -4
- package/scripts/ledger.mjs +6 -1
- package/scripts/router-table.json +24 -0
- package/scripts/ticket-router.mjs +543 -0
- package/scripts/ticket-router.test.mjs +981 -0
- package/scripts/within-run-pair-prose.test.mjs +46 -10
- package/skills/run-team/SKILL.md +59 -32
package/package.json
CHANGED
|
@@ -1054,7 +1054,7 @@ test("deriveRun: the row text it does not own is accepted as it stands", () => {
|
|
|
1054
1054
|
// The accept side of every refusal below: freeform row text, the arrow, the
|
|
1055
1055
|
// board's own tokens and an empty ledger are the ordinary case.
|
|
1056
1056
|
assert.doesNotThrow(() => run({
|
|
1057
|
-
rows: ["#324 impl-324=PR#346 → PR#346 · fix-pr-346=no-op · ruled:6-applies · ci=123:1:success · ports=16324 · tier=
|
|
1057
|
+
rows: ["#324 impl-324=PR#346 → PR#346 · fix-pr-346=no-op · ruled:6-applies · ci=123:1:success · ports=16324 · tier=task-high"],
|
|
1058
1058
|
dispatched: ["impl-324=PR#346", "fix-pr-346=no-op"],
|
|
1059
1059
|
}, [pr(346)]));
|
|
1060
1060
|
const empty = run({});
|
|
@@ -173,28 +173,53 @@ test("dispatch prints the agent definition the call names: the row's tier for an
|
|
|
173
173
|
// A `tier=` of the right shape whose definition has no file is refused the
|
|
174
174
|
// same way: a `task` call naming it cannot resolve. Two of those: a cell
|
|
175
175
|
// outside the shipped grid (`smol-max` — haiku has no `max`), and the
|
|
176
|
-
// pre-cell `alt
|
|
176
|
+
// pre-cell `alt`, whose definition is deleted. `row` refuses a non-cell
|
|
177
|
+
// `tier=` before it is written, so the rows `row` would refuse are written
|
|
178
|
+
// as a ledger edited by hand — the case dispatch's own check still meets.
|
|
177
179
|
test("dispatch refuses an implementer whose row names no single definition, or one with no file, before marking it live", (t) => {
|
|
178
|
-
const
|
|
179
|
-
ok
|
|
180
|
+
const rows = ["#7 class=routine · tier=alt · tier=slow-high", "#8 class=routine · tier=../../etc", "#9 class=routine · tier=", "#11 class=routine · tier=alt"];
|
|
181
|
+
const { ok, read, refused } = fixture(t, `# Fleet run ledger\n\n## Rows\n\n${rows.map((r) => `- ${r}\n`).join("")}\n## Filed\n\n## Ruled\n\n`);
|
|
180
182
|
refused(["dispatch", "7", "impl-7"], /impl-7: row carries conflicting tier= tokens \(tier=alt, tier=slow-high\) — fix the row with `ledger\.mjs row` — not dispatching impl-7/);
|
|
181
|
-
ok("row", "8", "class=routine · tier=../../etc");
|
|
182
183
|
refused(["dispatch", "8", "impl-8"], /impl-8: tier=\.\.\/\.\.\/etc is not a definition suffix — expected \[a-z0-9\] words joined by '-' — fix the row with `ledger\.mjs row` — not dispatching impl-8/);
|
|
183
|
-
ok("row", "9", "class=routine · tier=");
|
|
184
184
|
refused(["dispatch", "9", "impl-9"], /impl-9: tier= is not a definition suffix/);
|
|
185
185
|
ok("row", "10", "class=routine · tier=smol-max");
|
|
186
186
|
refused(["dispatch", "10", "impl-10"], /impl-10: row #10's tier= names fleet-implementer-smol-max, which has no agents\/fleet-implementer-smol-max\.agent\.md — fix the row with `ledger\.mjs row` — not dispatching impl-10/);
|
|
187
|
-
ok("row", "11", "class=routine · tier=alt");
|
|
188
187
|
refused(["dispatch", "11", "impl-11"], /impl-11: row #11's tier= names fleet-implementer-alt, which has no agents\/fleet-implementer-alt\.agent\.md/);
|
|
188
|
+
ok("row", "12", "class=routine · tier=task-high · tier=slow-high");
|
|
189
|
+
refused(["dispatch", "12", "impl-12"], /impl-12: row carries conflicting tier= tokens \(tier=task-high, tier=slow-high\)/);
|
|
189
190
|
assert.deepEqual(read().dispatched, []);
|
|
190
191
|
});
|
|
191
192
|
|
|
193
|
+
// `row` holds `tier=` to the cell grammar, so a retired or misspelt tier is
|
|
194
|
+
// refused at the Pull's own write, before the claim is dispatched on — and a
|
|
195
|
+
// cell token is accepted whether or not its definition has shipped, which is
|
|
196
|
+
// `dispatch`'s to check. The value is held to the whole grammar, not a prefix
|
|
197
|
+
// of it: a real family with an unknown level, a cell with trailing text, or a
|
|
198
|
+
// case-variant of a cell is refused. Only a token that STARTS with `tier=` is
|
|
199
|
+
// a tier token, so one that merely contains it (`x-tier=alt`) is written as is.
|
|
200
|
+
test("row refuses a tier= that is not a cell token, and accepts every cell token", (t) => {
|
|
201
|
+
const { ok, read, refused } = fixture(t);
|
|
202
|
+
const bads = [
|
|
203
|
+
"tier=alt", "tier=", "tier=../../etc", "tier=Task-High", "tier=task-high · tier=alt", "tier=haiku-high",
|
|
204
|
+
"tier=slow-ultra", "tier=task-", "tier=smol-", "tier=slow", "tier=task-high-x", "tier=task-high-", "tier=task-highx",
|
|
205
|
+
"tier==task-high", "tier=task-high,", "tier=TASK-HIGH", "tier=Slow-High", "tier=slow-HIGH", "tier=xslow-high",
|
|
206
|
+
];
|
|
207
|
+
for (const bad of bads) {
|
|
208
|
+
refused(["row", "7", `impl-7 · ${bad}`], /row #7: 'tier=[^']*' is not tier=<cell>/);
|
|
209
|
+
}
|
|
210
|
+
assert.deepEqual(read().rows, []);
|
|
211
|
+
const goods = ["tier=task-high", "tier=slow-high", "tier=smol-max", "tier-ok=impl-7:fleet-implementer-task-high", "x-tier=alt", "x-tier=Slow-ultra · tier=task-high"];
|
|
212
|
+
for (const good of goods) {
|
|
213
|
+
assert.equal(ok("row", "7", `impl-7 · ${good}`).line, `#7 impl-7 · ${good}`);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
192
217
|
// The refusal is about the implementer's own definition: a member whose
|
|
193
218
|
// definition does not come off `tier=` must still be dispatchable on such a
|
|
194
219
|
// row, or one bad token would strand the PR's finisher and fix-applier.
|
|
195
220
|
test("a malformed tier= on a row does not refuse a member whose definition ignores it", (t) => {
|
|
196
221
|
const { ok } = fixture(t);
|
|
197
|
-
ok("row", "7", "impl-7=PR#70 · tier=
|
|
222
|
+
ok("row", "7", "impl-7=PR#70 · tier=task-high · tier=slow-high");
|
|
198
223
|
assert.equal(ok("dispatch", "70", "fix-pr-70").agent, null);
|
|
199
224
|
assert.equal(ok("dispatch", "7", "finisher-pr-70").agent, "fleet-finisher");
|
|
200
225
|
});
|
|
@@ -215,7 +240,7 @@ test("dispatch names a fix-applier on an unresolved conflict hold of its own PR
|
|
|
215
240
|
// `tier=` is not consulted — conflicting values neither change nor refuse it.
|
|
216
241
|
ok("row", "50", "impl-50=PR#51 · tier=task-high · conflict-hold:#51");
|
|
217
242
|
assert.equal(ok("dispatch", "51", "fix-pr-51").agent, "fleet-implementer-slow-high");
|
|
218
|
-
ok("row", "60", "impl-60=PR#61 · tier=
|
|
243
|
+
ok("row", "60", "impl-60=PR#61 · tier=task-high · tier=slow-high · conflict-hold:#61");
|
|
219
244
|
assert.equal(ok("dispatch", "61", "fix-pr-61").agent, "fleet-implementer-slow-high");
|
|
220
245
|
|
|
221
246
|
// The hold's other spelling the tick reads (fleet-tick.mjs's CONFLICT_HOLD).
|
|
@@ -387,7 +412,7 @@ test("row still writes well-formed member tokens and non-member keys", (t) => {
|
|
|
387
412
|
["7", "impl-7 · class=routine · ports=16007"],
|
|
388
413
|
["8", "impl-8=PR#9 · fix-pr-9=applied:73b356de · review=wf:r1=failed reviewed=abc1234:1/0/0 · ci=123:1:success"],
|
|
389
414
|
["10", "held-behind:#9 · merge-bot-1=done · review=member:review-pr-10"],
|
|
390
|
-
["11", "impl-11 · tier=
|
|
415
|
+
["11", "impl-11 · tier=task-high · tier-ok=impl-11:fleet-implementer-task-high"],
|
|
391
416
|
["12", "impl-12=bailed · tier-mismatch=impl-12:fleet-implementer · impl-12: dispatched task, expected fleet-implementer"],
|
|
392
417
|
];
|
|
393
418
|
for (const [ticket, text] of rows) ok("row", ticket, text);
|
|
@@ -99,8 +99,9 @@ export const CELL = /^(slow|task|smol)-(minimal|low|medium|high|xhigh|max)$/;
|
|
|
99
99
|
export const CELL_DEF = new RegExp(`^fleet-implementer-${CELL.source.slice(1)}`);
|
|
100
100
|
|
|
101
101
|
// The cell a row with no `tier=` runs at: `policy_cell`, `slow-high` on every
|
|
102
|
-
// Pull until the router
|
|
103
|
-
|
|
102
|
+
// Pull until the router's table picks one per stratum. Exported so
|
|
103
|
+
// ticket-router.mjs defaults to the same cell this file maps a bare row to.
|
|
104
|
+
export const POLICY_CELL = "slow-high";
|
|
104
105
|
|
|
105
106
|
// The Exploration Pull's draw (spec § 2): uniform over every cell but
|
|
106
107
|
// `policyCell`, keyed off the row's own `session` and `ticket`, so the draw
|
|
@@ -134,9 +135,14 @@ export function drawCell({ session, ticket, policyCell, cells }) {
|
|
|
134
135
|
// different definitions for one row and fail a dispatch made exactly as
|
|
135
136
|
// printed.
|
|
136
137
|
const TIER_SUFFIX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
138
|
+
|
|
139
|
+
/** Every `tier=` value a row's text carries, in order, duplicates kept. */
|
|
140
|
+
export function tierValues(rowText) {
|
|
141
|
+
return String(rowText ?? "").split(/\s+/).filter((t) => t.startsWith("tier=")).map((t) => t.slice("tier=".length));
|
|
142
|
+
}
|
|
143
|
+
|
|
137
144
|
export function expectedDefinition(rowText) {
|
|
138
|
-
const values = [...new Set(
|
|
139
|
-
.filter((t) => t.startsWith("tier=")).map((t) => t.slice("tier=".length)))];
|
|
145
|
+
const values = [...new Set(tierValues(rowText))];
|
|
140
146
|
if (values.length === 0) return `fleet-implementer-${POLICY_CELL}`;
|
|
141
147
|
if (values.length > 1) throw new Error(`row carries conflicting tier= tokens (${values.map((v) => `tier=${v}`).join(", ")}) — fix the row with \`ledger.mjs row\``);
|
|
142
148
|
if (!TIER_SUFFIX.test(values[0])) throw new Error(`tier=${values[0]} is not a definition suffix — expected [a-z0-9] words joined by '-' — fix the row with \`ledger.mjs row\``);
|
package/scripts/ledger.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { fileURLToPath } from "node:url";
|
|
|
35
35
|
import { spawnSync, execFileSync } from "node:child_process";
|
|
36
36
|
import { makeDie, isFlagLike, hasEqualsForm, isDigits } from "./arg.mjs";
|
|
37
37
|
import { gitEnv, workspaceDirFromGitCommonDir } from "./git-env.mjs";
|
|
38
|
-
import { parseMember, parseToken, memberTokens, nextMergeBot, agentDefinition } from "./ledger-grammar.mjs";
|
|
38
|
+
import { parseMember, parseToken, memberTokens, nextMergeBot, agentDefinition, CELL, tierValues } from "./ledger-grammar.mjs";
|
|
39
39
|
import { deriveRun, LedgerError, labelOffMember } from "./fleet-tick.mjs";
|
|
40
40
|
import { stateFileIn, readState, assessBeat, stallsAt } from "./fleet-state.mjs";
|
|
41
41
|
|
|
@@ -841,6 +841,11 @@ if (cmd === "read") {
|
|
|
841
841
|
// rewrite alike; a well-formed token, live or settled, still goes through.
|
|
842
842
|
const malformed = memberTokens(line).find((t) => t.error !== null);
|
|
843
843
|
if (malformed) die(`row ${key}: malformed member token '${malformed.name}=${malformed.outcome}' — ${malformed.error}`);
|
|
844
|
+
// `tier=` names the implementer cell an exploration Pull dispatched, so its
|
|
845
|
+
// value must be a cell token: a retired or misspelt tier written here would
|
|
846
|
+
// only be refused later, by `dispatch`, after the claim.
|
|
847
|
+
const badTier = tierValues(line).find((v) => !CELL.test(v));
|
|
848
|
+
if (badTier !== undefined) die(`row ${key}: 'tier=${badTier}' is not tier=<cell> — a cell is <slow|task|smol>-<level>`);
|
|
844
849
|
// `label-off=<attempt>` tells the tick a missing `ready-to-merge` is
|
|
845
850
|
// the controller's own removal, not a finisher's miss — so one naming no
|
|
846
851
|
// finisher-pr member this run has would mask a real miss behind a record of
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"window_start": null,
|
|
3
|
+
"fitted_through": null,
|
|
4
|
+
"n_rows": 0,
|
|
5
|
+
"stage": 1,
|
|
6
|
+
"cells": [
|
|
7
|
+
"slow-high",
|
|
8
|
+
"task-high",
|
|
9
|
+
"smol-high"
|
|
10
|
+
],
|
|
11
|
+
"burn_in": true,
|
|
12
|
+
"rows": {
|
|
13
|
+
"*": "slow-high"
|
|
14
|
+
},
|
|
15
|
+
"classifier": {
|
|
16
|
+
"b": null,
|
|
17
|
+
"tau": null
|
|
18
|
+
},
|
|
19
|
+
"estimates": {},
|
|
20
|
+
"guard": {
|
|
21
|
+
"tripped": [],
|
|
22
|
+
"n": {}
|
|
23
|
+
}
|
|
24
|
+
}
|