@feigi/fleet-ctl 3.19.8 → 3.21.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.
@@ -1,8 +1,7 @@
1
1
  import { test } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { existsSync, readFileSync } from "node:fs";
3
+ import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { CELL } from "./ledger-grammar.mjs";
6
5
  import { between as section } from "./prose-pin.mjs";
7
6
 
8
7
  // #864's finding is that tier is entangled with calendar date and therefore
@@ -126,14 +125,16 @@ test("phase 2 counts the rate off the ledger's impl- rows, and a replacement doe
126
125
  /records\s+`tier=<cell>`\s+in\s+the\s+row,\s+and\s+a\s+replacement\s+inherits\s+the\s+row's\s+tier/,
127
126
  "phase 2 no longer records the cell on the row, or no longer says a replacement inherits it rather than counting as a Pull",
128
127
  );
129
- // Until the router draws one, the prose has to name the cell the controller
130
- // writes, and that cell has to have a definition: `ledger.mjs dispatch`
131
- // refuses a `tier=` naming no `agents/` file, so a cell named here without
132
- // one stops every exploration Pull before its member goes live.
133
- const named = /Until\s+the\s+router\s+draws\s+the\s+cell[^.]*?`<cell>`\s+is\s+`([^`]+)`/.exec(slice)?.[1];
134
- assert.ok(named, "phase 2 no longer names the cell an exploration Pull writes before the router exists");
135
- assert.ok(CELL.test(named), `${named} is not a cell`);
136
- assert.ok(existsSync(join(REPO, "agents", `fleet-implementer-${named}.agent.md`)), `phase 2 names ${named}, which has no agents/fleet-implementer-${named}.agent.md`);
128
+ // The cell is the router's draw, never one this prose names: a cell
129
+ // written here is a second copy of the routing, free to disagree with the
130
+ // table the router reads.
131
+ assert.match(slice, /the\s+router\s+draws\s+its\s+cell/, "phase 2 no longer says the router draws the exploration cell");
132
+ assert.doesNotMatch(slice, /Until\s+the\s+router\s+draws/, "phase 2 names a stand-in exploration cell again");
133
+ assert.match(
134
+ slice,
135
+ /While\s+the\s+table's\s+`burn_in`\s+is\s+true\s+every\s+Pull\s+draws,\s+over\s+every\s+cell,\s+the\s+policy\s+cell\s+included/,
136
+ "phase 2 no longer says burn-in draws on every Pull, over every cell",
137
+ );
137
138
  // The roll: a Pull that lands on a chain head passes the exploration cell to
138
139
  // the next Pull rather than skipping it, which is the rule ADR 0013 §6 states
139
140
  // and the difficulty caveat below depends on.
@@ -144,6 +145,41 @@ test("phase 2 counts the rate off the ledger's impl- rows, and a replacement doe
144
145
  );
145
146
  });
146
147
 
148
+ // The Pull's own write: the router's line decides the row, and `tier=` is
149
+ // the draw's record — on a row exactly when the router drew. Written for a
150
+ // row the router did not draw for, `ledger.mjs dispatch` would print a cell
151
+ // nothing chose.
152
+ const pullStep7 = () => section(RUN_TEAM, "7. **Route, row, then dispatch**", "**The Pull table**", "run-team Pull step 7");
153
+
154
+ test("Pull step 7 routes through ticket-router.mjs and writes tier= exactly when it drew", () => {
155
+ const slice = pullStep7();
156
+ assert.match(slice, /ticket-router\.mjs\s+route\s+--session\s+<session>\s+--ticket\s+<N>\s+--arm\s+<A\|B>\s+--impl-row\s+<k>/, "step 7 no longer calls the router");
157
+ assert.match(slice, /--issue\s+<scratch>\/impl-<N>\/issue\.json/, "step 7 no longer hands the router the Pull's own read");
158
+ assert.match(slice, /`tier=<CELL>`\s+exactly\s+when\s+`DRAW`\s+is\s+not\s+`-`/, "step 7 no longer ties tier= to the draw");
159
+ assert.match(slice, /Router\s+exit\s+2\s+→\s+no\s+row\s+and\s+no\s+dispatch/, "step 7 dispatches on a router usage error");
160
+ assert.match(slice, /--impl-row\s+<k>\[\s+--chain-head\]/, "step 7 no longer passes --chain-head for a ticket another sequences after");
161
+ assert.match(slice, /--guard\s+\.fleet\/cost-guard\.json/, "step 7 no longer hands the router the cost guard");
162
+ assert.match(slice, /\[\s+·\s+route=<REASON>\]/, "step 7 no longer records the router's REASON on the row");
163
+ assert.match(slice, /release\s+the\s+claim\s+\(`release-ticket\.sh`/, "step 7 no longer releases the claim on a router exit 2");
164
+ // The Pull's one read is what the router reads: a second fetch would be a
165
+ // second network call per Pull.
166
+ const step3 = section(RUN_TEAM, "3. **Read the ticket in full, once**", "Record the ticket's real", "run-team Pull step 3");
167
+ assert.match(step3, /--json\s+title,body,comments,labels,createdAt`\s+written\s+to\s+`<scratch>\/impl-<N>\/issue\.json`/, "step 3 no longer writes the file the router reads");
168
+ });
169
+
170
+ const routerArgs = () =>
171
+ section(RUN_TEAM, "**The router's arguments, and nothing past its line.**", "**Count the rate against the ledger", "run-team router's arguments");
172
+
173
+ test("the router's arguments paragraph pins the roll, the arm parity and the single source of CELL", () => {
174
+ const slice = routerArgs();
175
+ assert.match(slice, /row\s+5k\s+and\s+every\s+row\s+after\s+it\s+carry\s+no\s+`tier=`/, "the --impl-row rolling definition is gone");
176
+ assert.match(slice, /`--chain-head`\s+marks\s+a\s+ticket\s+another\s+open\s+ticket\s+sequences\s+after/, "--chain-head is no longer defined");
177
+ assert.match(slice, /`A`\s+for\s+an\s+even\s+ticket\s+number\s+and\s+`B`\s+for\s+an\s+odd/, "the --arm parity rule changed");
178
+ assert.match(slice, /never\s+re-derive\s+`CELL`/, "the router's line is no longer the whole decision");
179
+ assert.match(slice, /`REASON=guard-missing`/, "the guard-missing route is no longer named");
180
+ assert.match(slice, /`REASON`\s+other\s+than\s+`ok`\s+goes\s+on\s+the\s+row\s+as\s+`route=<REASON>`/, "REASON no longer lands on the row as route=");
181
+ });
182
+
147
183
  // The difficulty caveat lives in the counter-evidence section, past this file's
148
184
  // dispatch slice, so it needs its own anchor pair.
149
185
  const counterEvidence = () =>
@@ -515,9 +515,10 @@ terminating in exactly one dispatch or an empty shortlist (ADR 0013 Decision 2):
515
515
  nothing else; work done directly against the shared checkout, by a
516
516
  human or another agent session, stakes no branch-shaped claim and trips
517
517
  none of them.
518
- 3. **Read the ticket in full, once** — `gh issue view <N> --json title,body,comments,labels
519
- --jq '.title, .body, ([.labels[].name]|join(",")), (.comments[]|.author.login + ": " + .body)'`.
520
- One read answers every question the Pull asks. Record the ticket's real
518
+ 3. **Read the ticket in full, once** — `gh issue view <N> --json title,body,comments,labels,createdAt`
519
+ written to `<scratch>/impl-<N>/issue.json`, then read off that file with `jq -r
520
+ '.title, .body, ([.labels[].name]|join(",")), (.comments[]|.author.login + ": " + .body)'`.
521
+ One read answers every question the Pull asks; step 7's router reads the same file. Record the ticket's real
521
522
  brief — the `## Agent Brief` comment where one exists, otherwise the issue
522
523
  body, which is where most tickets actually carry it (a controller probe found only 43/100 open
523
524
  `ready-for-agent` issues have a separate Agent Brief comment; the rest,
@@ -706,9 +707,16 @@ terminating in exactly one dispatch or an empty shortlist (ADR 0013 Decision 2):
706
707
  two independent tickets.
707
708
  6. **Claim** — `~/.fleet/bin/fleet-run claim-ticket.sh <N> impl-<N> implementer
708
709
  --apply` (below). Exit 2 → treat it as taken, next entry.
709
- 7. **Row, then dispatch** — `ledger.mjs row <N> "impl-<N>[ · tier=<cell>]"`,
710
- then phase 2 at once: claim what you are about to dispatch;
711
- dispatch what you have just claimed.
710
+ 7. **Route, row, then dispatch** — `~/.fleet/bin/fleet-run ticket-router.mjs route
711
+ --session <session> --ticket <N> --arm <A|B> --impl-row <k>[ --chain-head] --issue
712
+ <scratch>/impl-<N>/issue.json --guard .fleet/cost-guard.json` prints
713
+ `POLICY=… CELL=… DRAW=… STRATUM=… REASON=…` (phase 2's exploration rule
714
+ says what each argument is); then `ledger.mjs row <N> "impl-<N>[ · tier=<CELL>][ · route=<REASON>]"`,
715
+ `tier=<CELL>` exactly when `DRAW` is not `-`; then phase 2 at once: claim
716
+ what you are about to dispatch; dispatch what you have just claimed. Router
717
+ exit 2 → no row and no dispatch: release the claim (`release-ticket.sh`,
718
+ below) and emit an event, a probe that could not look — a table the router
719
+ refuses refuses every Pull.
712
720
 
713
721
  **The Pull table** — every verdict a Pull can reach, and the one thing each
714
722
  does (ADR 0013 Decision 2 and 4):
@@ -923,8 +931,8 @@ kernel and needs a `wait`, while a `task` job is process-level and its report
923
931
  auto-delivers on its own, like every other member's.
924
932
 
925
933
  **No per-call tier or effort anywhere on this path**: tier stays the agent
926
- definition's own frontmatter (ADR 0005), whichever of the two definitions the
927
- Pull names, and a member's Resolved tier stays readable from its transcript.
934
+ definition's own frontmatter (ADR 0005), whichever `fleet-implementer-<cell>`
935
+ definition the Pull names, and a member's Resolved tier stays readable from its transcript.
928
936
 
929
937
  **Claim what you are about to dispatch; dispatch what you have just claimed.**
930
938
  Phase 1 makes the claim, serially in the main checkout — its rule is measured —
@@ -1087,16 +1095,31 @@ dispatched — the fleet reads that config and never writes it (ADR 0003).
1087
1095
 
1088
1096
  **Every 5th Pull by ledger count goes at an exploration cell.** Count the `impl-`
1089
1097
  rows in `.fleet/ledger.md` at Pull time; the Pull that creates row 5, 10, 15 …
1090
- records `tier=<cell>` in the row, and a replacement inherits the row's tier —
1098
+ is an Exploration Pull, and the router draws its cell: `k = 1 +
1099
+ (sha256("<session>\t<ticket>")[0:8] mod K)` over `router-table.json`'s
1100
+ `cells` minus the policy cell, in token order. While the table's `burn_in` is
1101
+ true every Pull draws, over every cell, the policy cell included. An
1102
+ Exploration Pull records `tier=<cell>` in the row, and a replacement inherits the row's tier —
1091
1103
  written at the Pull's step 7, ahead of `ledger.mjs dispatch`, which reads it
1092
- and prints `fleet-implementer-<cell>` as the `agent` to dispatch. Until the
1093
- router draws the cell (spec 2026-09-28 § 2), `<cell>` is `task-high` — the
1094
- retired alternate tier's own `@task:high` route. The
1104
+ and prints `fleet-implementer-<cell>` as the `agent` to dispatch; a row with
1105
+ no `tier=` runs the policy cell. The
1095
1106
  assignment rolls to the next Pull when another open ticket sequences after
1096
1107
  it — a ticket the rest of the run
1097
- depends on. Do not tell the member it is a control: a member that
1108
+ depends on; `class` is not read. Do not tell the member it is a control: a member that
1098
1109
  knows it is being measured is not measuring the same thing.
1099
1110
 
1111
+ **The router's arguments, and nothing past its line.** `--impl-row` is the
1112
+ row count the Pull creates — or, while an assignment is rolling (the latest
1113
+ row 5k and every row after it carry no `tier=`), that row's number;
1114
+ `--chain-head` marks a ticket another open ticket sequences after, which is
1115
+ what rolls it. `--arm` is `A` for an even ticket number and `B` for an odd
1116
+ one; a B Pull routes exactly as an A one until the table opens arm B.
1117
+ `--session` is this run's own session directory. The printed line is the
1118
+ whole routing decision — never re-derive `CELL` from the table, the guard or
1119
+ this paragraph. A missing or unreadable `.fleet/cost-guard.json` routes the
1120
+ default cell only (`REASON=guard-missing`), burn-in included, and `REASON`
1121
+ other than `ok` goes on the row as `route=<REASON>`, nowhere else.
1122
+
1100
1123
  **Count the rate against the ledger, because a Pull is the dispatch unit.** One
1101
1124
  Pull is one member, so a rate counted per Pull is a rate counted per member —
1102
1125
  one implementer in five, however the Pulls fall across the run — and the
@@ -1353,31 +1376,35 @@ awk -F'\t' '!/^#/ && $4=="routine" {t[$5]++; if($6=="no") f[$5]++} \
1353
1376
  ```
1354
1377
 
1355
1378
  **These rows are the historical corpus, and nothing appended to them fixes the
1356
- confound.** What fixes it is the within-run pairing in the dispatch rule above:
1357
- from now on one implementer in every five runs at the alternate tier beside the
1358
- top-tier ones, against the same prompts on the same day, so the comparison stops depending on
1359
- which tier history happened to leave in which week. Read the pairs, not the
1360
- whole-file split, once there are enough of them.
1379
+ confound.** What fixes it is the exploration draw in the dispatch rule above:
1380
+ one implementer in every five — every one, during burn-in — runs at a drawn
1381
+ cell beside the policy cell's, against the same prompts on the same day, so
1382
+ the comparison stops depending on which tier history happened to leave in
1383
+ which week. Read each cell's comparisons, not the whole-file split, once there
1384
+ are enough of them.
1361
1385
 
1362
1386
  **So read what the guard actually established: routine tickets sometimes fail to
1363
1387
  close their own ticket. Not that `sonnet` caused it.** The revert is the
1364
1388
  pre-committed rule being honoured, not a measurement. Restoring a cheap tier is
1365
1389
  still the maintainer's call and still a change to the dispatch rule — but the
1366
1390
  deliberate control it used to require is no longer something anyone has to
1367
- authorize one ticket at a time: the alternate-tier dispatch above produces one
1368
- in every five Pulls by construction. **Do not read the pairs early.** Report the count —
1369
- the DELIBERATE count, from the query in `member-outcomes.tsv`'s header, which
1370
- prints the pair count and its distinct `run_date`s and counts only sessions
1371
- that ran both implementer definitions at different models — and stop until
1391
+ authorize one ticket at a time: the exploration draw above produces one in
1392
+ every five Pulls by construction. **Do not read a cell early.** A cell's
1393
+ comparison is one session holding an admissible row at that cell — its tier
1394
+ check passed and its resolved effort is the cell's level — and one at
1395
+ `slow-high` whose resolved model and effort differ. Count them per cell, never
1396
+ pooled across cells, and stop until
1372
1397
  there are at least ten of them across five or more distinct `run_date`s; below
1373
- that, a pair count is a number, not evidence, and the last guard fired with
1374
- n=1 on the control side. **Report whichever number that query prints, never a
1375
- number from any other one:** the query it replaces (#1066) counted every
1376
- session whose implementers merely differed, so it cleared this floor by an
1377
- order of magnitude while the controlled comparison did not exist yet.
1378
-
1379
- Why the agent body carries what it does — read this before editing either agent
1380
- file, and keep the two bodies byte-identical. Each rule in the body's
1398
+ that, a comparison count is a number, not evidence, and the last guard fired
1399
+ with n=1 on the control side. **Report the count the per-cell readout prints,
1400
+ never one from any other query:** until that readout exists there is no count
1401
+ to report, the pair query in `member-outcomes.tsv`'s header counts pre-cutover
1402
+ pairs only, which are no cell's comparisons, and the query before that one
1403
+ counted every session whose implementers merely differed, so it cleared this
1404
+ floor by an order of magnitude while the controlled comparison did not exist yet.
1405
+
1406
+ Why the agent body carries what it does — read this before editing any
1407
+ `fleet-implementer-<cell>` file, and keep every cell's body byte-identical. Each rule in the body's
1381
1408
  enumerate-and-declare block is load-bearing, for a different reason.
1382
1409
  **Enumerate-and-declare** answers a signature measured four times in one run:
1383
1410
  each implementer fixed exactly the cases its ticket named and left an adjacent
@@ -3667,7 +3694,7 @@ naming the token and writing nothing, because every reader would take it as a
3667
3694
  permanent settle.
3668
3695
 
3669
3696
  `→ PR#M` stays as the human-readable arrow; the tick reads only the `=`
3670
- tokens. `tier=<cell>` marks the every-5th-Pull member's cell (phase 2), and `excluded ·
3697
+ tokens. `tier=<cell>` marks an Exploration Pull's cell and `route=<REASON>` a router fallback (phase 2), and `excluded ·
3671
3698
  behind-pr:#M | behind-issue:#M` is an Exclusion (phase 1) — a ticket row like
3672
3699
  any other, never a section of its own.
3673
3700