@feigi/fleet-ctl 3.102.4 → 3.102.5
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/ledger-grammar.mjs +29 -20
- package/scripts/ledger.mjs +31 -21
- package/scripts/tier-check.mjs +15 -7
- package/skills/run-team/references/ledger.md +2 -0
package/package.json
CHANGED
|
@@ -120,34 +120,42 @@ export function memberRole(family) {
|
|
|
120
120
|
// the first 8 hex of the sha256 the brief's sidecar carries. A merge bot's
|
|
121
121
|
// entry then carries `queued=<pr>,<pr>…` — `queued=none` for an empty queue —
|
|
122
122
|
// the open `ready-to-merge` PRs `dispatch` read when it dispatched the bot,
|
|
123
|
-
// which its report is judged against at `settle --from-report`.
|
|
124
|
-
//
|
|
125
|
-
|
|
123
|
+
// which its report is judged against at `settle --from-report`. An
|
|
124
|
+
// implementer's entry ends with `agent=<definition>`, the definition
|
|
125
|
+
// `dispatch` printed for it, which tier-check judges it against. The value
|
|
126
|
+
// becomes a file name under `agents/`, so it is held to `[a-z0-9]` words
|
|
127
|
+
// joined by `-`, the shape of a `tier=` value. `settle` rewrites the token
|
|
128
|
+
// and keeps the words beside it.
|
|
129
|
+
const DISPATCHED_ENTRY = /^([^ ]*) brief=([0-9a-f]{8})(?: queued=(none|[1-9][0-9]*(?:,[1-9][0-9]*)*))?(?: agent=([a-z0-9]+(?:-[a-z0-9]+)*))?$/;
|
|
126
130
|
|
|
127
131
|
/**
|
|
128
|
-
* `{token, brief, queued}` for one `## Dispatched` entry: the member
|
|
129
|
-
* the brief's hash or null,
|
|
130
|
-
* for an entry without one
|
|
132
|
+
* `{token, brief, queued, agent}` for one `## Dispatched` entry: the member
|
|
133
|
+
* token, the brief's hash or null, the PR numbers of its `queued=` word or
|
|
134
|
+
* null for an entry without one, and the definition of its `agent=` word or
|
|
135
|
+
* null for an entry without one. An entry that is not a member token plus a
|
|
131
136
|
* well-formed `brief=` word, optionally followed by a well-formed `queued=`
|
|
132
|
-
* word, is returned whole as `token`, so a
|
|
133
|
-
* parseToken() refuses it exactly as it refuses any
|
|
134
|
-
* cannot parse.
|
|
137
|
+
* word and a well-formed `agent=` word, is returned whole as `token`, so a
|
|
138
|
+
* reader parsing it with parseToken() refuses it exactly as it refuses any
|
|
139
|
+
* other entry this grammar cannot parse.
|
|
135
140
|
*/
|
|
136
141
|
export function parseDispatchedEntry(entry) {
|
|
137
142
|
const m = DISPATCHED_ENTRY.exec(entry);
|
|
138
|
-
if (m === null || parseToken(m[1]) === null) return { token: entry, brief: null, queued: null };
|
|
143
|
+
if (m === null || parseToken(m[1]) === null) return { token: entry, brief: null, queued: null, agent: null };
|
|
139
144
|
const queued = m[3] === undefined ? null : m[3] === "none" ? [] : m[3].split(",").map(Number);
|
|
140
|
-
return { token: m[1], brief: m[2], queued };
|
|
145
|
+
return { token: m[1], brief: m[2], queued, agent: m[4] ?? null };
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
/**
|
|
144
149
|
* The `## Dispatched` entry for `token`, with `brief=<brief>` unless brief is
|
|
145
|
-
* null and, after it, `queued=…` unless queued is null
|
|
150
|
+
* null and, after it, `queued=…` unless queued is null and `agent=<agent>`
|
|
151
|
+
* unless agent is null.
|
|
146
152
|
*/
|
|
147
|
-
export function dispatchedEntry(token, brief, queued = null) {
|
|
153
|
+
export function dispatchedEntry(token, brief, queued = null, agent = null) {
|
|
148
154
|
if (brief === null) return token;
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
let entry = `${token} brief=${brief}`;
|
|
156
|
+
if (queued !== null) entry += ` queued=${queued.join(",") || "none"}`;
|
|
157
|
+
if (agent !== null) entry += ` agent=${agent}`;
|
|
158
|
+
return entry;
|
|
151
159
|
}
|
|
152
160
|
|
|
153
161
|
// `PR#<n>` as a word of a row's text, `PR #<n>` too.
|
|
@@ -794,7 +802,7 @@ export function drawCell({ session, ticket, policyCell, cells }) {
|
|
|
794
802
|
return { cell: E[k - 1], k, K };
|
|
795
803
|
}
|
|
796
804
|
|
|
797
|
-
// The definition an implementer
|
|
805
|
+
// The definition an implementer is dispatched under, off its ticket
|
|
798
806
|
// row's `tier=` token — none means `fleet-implementer-<POLICY_CELL>`,
|
|
799
807
|
// `tier=<x>` means `fleet-implementer-<x>`. The value becomes a file
|
|
800
808
|
// name under `agents/`, so it is held to `[a-z0-9]` words joined by `-`
|
|
@@ -804,10 +812,11 @@ export function drawCell({ session, ticket, policyCell, cells }) {
|
|
|
804
812
|
// `ledger.mjs dispatch`'s to refuse: it checks the file exists.
|
|
805
813
|
//
|
|
806
814
|
// Here, not in tier-check.mjs, because two readers must agree on it:
|
|
807
|
-
// `ledger.mjs dispatch` prints the definition before the call
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
815
|
+
// `ledger.mjs dispatch` prints the definition before the call and records
|
|
816
|
+
// it on the member's `## Dispatched` entry, and tier-check.mjs judges the
|
|
817
|
+
// member against that record — against this function's answer for the row
|
|
818
|
+
// only for an entry that holds none. Two copies could name different
|
|
819
|
+
// definitions for one row and fail a dispatch made exactly as printed.
|
|
811
820
|
const TIER_SUFFIX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
812
821
|
|
|
813
822
|
/** Every `tier=` value a row's text carries, in order, duplicates kept. */
|
package/scripts/ledger.mjs
CHANGED
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
// every liveness count from this file alone, rather than from row text a
|
|
15
15
|
// controller typed by hand: `dispatch` writes a member's live token onto its
|
|
16
16
|
// row and appends it to `## Dispatched` with `brief=<hash>`, the brief it
|
|
17
|
-
// refuses to dispatch without —
|
|
17
|
+
// refuses to dispatch without — for a merge bot, `queued=<pr>,…`, the
|
|
18
18
|
// open `ready-to-merge` PRs gh lists at dispatch, which its report must give
|
|
19
|
-
// a verdict each
|
|
19
|
+
// a verdict each, and for an implementer, `agent=<definition>`, the
|
|
20
|
+
// definition it prints, which tier-check judges the implementer against —
|
|
21
|
+
// when that brief's sidecar names an absolute
|
|
20
22
|
// scratch, it also clears the member's report file there, so a prior run's
|
|
21
23
|
// report is not read as this run's; `settle` rewrites that token to
|
|
22
24
|
// `<member>=<outcome>` in both places (an implementer's settle to `PR#M` also
|
|
@@ -487,7 +489,7 @@ function refuseStrayInId(value, what) {
|
|
|
487
489
|
let ledgerParsed = false;
|
|
488
490
|
|
|
489
491
|
function load() {
|
|
490
|
-
if (!existsSync(file)) return { rows: [], filed: [], ruled: [],
|
|
492
|
+
if (!existsSync(file)) return { rows: [], filed: [], ruled: [], ...dispatchedOf([]), drain: null, retired: [] };
|
|
491
493
|
let text;
|
|
492
494
|
try {
|
|
493
495
|
text = readFileSync(file, "utf8");
|
|
@@ -519,14 +521,15 @@ function load() {
|
|
|
519
521
|
}
|
|
520
522
|
|
|
521
523
|
// `## Dispatched` held as its tokens, which is what every reader of `read`
|
|
522
|
-
// parses, `briefs`, each member's `brief=` hash by name,
|
|
523
|
-
// merge bot's `queued=` PR numbers by name
|
|
524
|
-
//
|
|
525
|
-
//
|
|
524
|
+
// parses, `briefs`, each member's `brief=` hash by name, `queued`, each
|
|
525
|
+
// merge bot's `queued=` PR numbers by name, and `agents`, each implementer's
|
|
526
|
+
// `agent=` definition by name: one token per member name for
|
|
527
|
+
// the session, so a settle that rewrites the token keeps the name its brief,
|
|
528
|
+
// its queue and its definition are filed under. save() joins them back.
|
|
526
529
|
function dispatchedOf(entries) {
|
|
527
530
|
const parsed = entries.map(parseDispatchedEntry);
|
|
528
531
|
const byName = (field) => Object.fromEntries(parsed.filter((p) => p[field] !== null).map((p) => [parseToken(p.token).name, p[field]]));
|
|
529
|
-
return { dispatched: parsed.map((p) => p.token), briefs: byName("brief"), queued: byName("queued") };
|
|
532
|
+
return { dispatched: parsed.map((p) => p.token), briefs: byName("brief"), queued: byName("queued"), agents: byName("agent") };
|
|
530
533
|
}
|
|
531
534
|
|
|
532
535
|
// `## Drain` is written only once there is a marker to hold: an empty section
|
|
@@ -541,7 +544,7 @@ function save(d) {
|
|
|
541
544
|
`# Fleet run ledger\n\n${ROWS}\n\n` + list(d.rows) +
|
|
542
545
|
`\n\n${DISPATCHED}\n\n` + list(d.dispatched.map((t) => {
|
|
543
546
|
const name = parseToken(t)?.name;
|
|
544
|
-
return dispatchedEntry(t, d.briefs[name] ?? null, d.queued[name] ?? null);
|
|
547
|
+
return dispatchedEntry(t, d.briefs[name] ?? null, d.queued[name] ?? null, d.agents[name] ?? null);
|
|
545
548
|
})) +
|
|
546
549
|
`\n\n${FILED}\n\n` + list(d.filed) +
|
|
547
550
|
`\n\n${RULED}\n\n` + list(d.ruled) +
|
|
@@ -1632,7 +1635,7 @@ function runRotate() {
|
|
|
1632
1635
|
}
|
|
1633
1636
|
// Read before anything moves: a ledger whose live tokens cannot be read is
|
|
1634
1637
|
// refused with nothing moved, never archived with those tokens dropped.
|
|
1635
|
-
let carry = { rows: [], dispatched: [],
|
|
1638
|
+
let carry = { rows: [], dispatched: [], retired: [], skipped: [] };
|
|
1636
1639
|
if (prior === "ancestor" && exists) {
|
|
1637
1640
|
let text;
|
|
1638
1641
|
try {
|
|
@@ -1753,11 +1756,12 @@ function carryOf(text) {
|
|
|
1753
1756
|
return false;
|
|
1754
1757
|
}
|
|
1755
1758
|
});
|
|
1756
|
-
// An entry's `brief=` and `
|
|
1757
|
-
// beside the token it writes, so a carried member's record still
|
|
1758
|
-
// brief it was dispatched with —
|
|
1759
|
-
// judged against
|
|
1760
|
-
|
|
1759
|
+
// An entry's `brief=`, `queued=` and `agent=` travel with it: save() writes
|
|
1760
|
+
// them only beside the token it writes, so a carried member's record still
|
|
1761
|
+
// names the brief it was dispatched with — a carried merge bot the queue it
|
|
1762
|
+
// is judged against, a carried implementer the definition it is judged
|
|
1763
|
+
// against — and a dropped member's does not.
|
|
1764
|
+
const { dispatched: allDispatched, briefs: allBriefs, queued: allQueued, agents: allAgents } = dispatchedOf(ledgerSection(text, DISPATCHED));
|
|
1761
1765
|
const dispatched = allDispatched.filter((e) => {
|
|
1762
1766
|
const t = parseToken(e);
|
|
1763
1767
|
if (t !== null && t.error === null) return true;
|
|
@@ -1780,7 +1784,7 @@ function carryOf(text) {
|
|
|
1780
1784
|
const live = liveOnLedger(rows, dispatched);
|
|
1781
1785
|
const carried = { rows: live.rows.map((i) => rows[i]), dispatched: live.dispatched };
|
|
1782
1786
|
const retired = retiredOnLedger([...rows, ...skippedRows], allDispatched, archivedRetired, carried);
|
|
1783
|
-
return { ...carried, briefs: allBriefs, queued: allQueued, retired, skipped };
|
|
1787
|
+
return { ...carried, briefs: allBriefs, queued: allQueued, agents: allAgents, retired, skipped };
|
|
1784
1788
|
}
|
|
1785
1789
|
|
|
1786
1790
|
// ── dispatch / settle ────────────────────────────────────────────────────────
|
|
@@ -2220,11 +2224,13 @@ async function runDispatch() {
|
|
|
2220
2224
|
// The definition the `task` call names, printed so the controller reads it
|
|
2221
2225
|
// off this output rather than recalling it from prose a compaction drops.
|
|
2222
2226
|
// An implementer's comes off its row's `tier=`, the same function
|
|
2223
|
-
// tier-check.mjs
|
|
2224
|
-
//
|
|
2225
|
-
//
|
|
2226
|
-
//
|
|
2227
|
-
//
|
|
2227
|
+
// tier-check.mjs falls back to for an entry with no record, and `dispatch`
|
|
2228
|
+
// records what it prints on the member's `## Dispatched` entry, which
|
|
2229
|
+
// tier-check judges the member against — so a row that names no single
|
|
2230
|
+
// definition, or names one with no file under `agents/` (a well-formed
|
|
2231
|
+
// `tier=` whose definition has not shipped: a `task` call naming it cannot
|
|
2232
|
+
// resolve), is refused here, before the member is live, not discovered by
|
|
2233
|
+
// the check after the call.
|
|
2228
2234
|
//
|
|
2229
2235
|
// A fix-applier is `fleet-fix-applier` for every kind of work — review,
|
|
2230
2236
|
// conflict hold, dispositions retry — so its definition reads nothing off
|
|
@@ -2319,6 +2325,10 @@ async function runDispatch() {
|
|
|
2319
2325
|
// A merge bot's queue is recorded here, beside its brief, where nothing in
|
|
2320
2326
|
// its report can reach: `settle --from-report` judges the report against it.
|
|
2321
2327
|
if (QUEUED !== null) data.queued[member.name] = QUEUED;
|
|
2328
|
+
// An implementer's definition is recorded here too: tier-check judges the
|
|
2329
|
+
// member against it, and a `tier=` replaced after the member settled does
|
|
2330
|
+
// not change what it was dispatched under.
|
|
2331
|
+
if (member.family === "impl") data.agents[member.name] = agent;
|
|
2322
2332
|
save(data);
|
|
2323
2333
|
console.error(` dispatched ${member.name}`);
|
|
2324
2334
|
console.log(JSON.stringify({
|
package/scripts/tier-check.mjs
CHANGED
|
@@ -56,9 +56,11 @@
|
|
|
56
56
|
//
|
|
57
57
|
// An implementer (`impl-<N>`, ledger-grammar.mjs's name) is judged on the
|
|
58
58
|
// ledger's terms, not the caller's: the definition it should have
|
|
59
|
-
// run under is
|
|
60
|
-
// (ledger
|
|
61
|
-
//
|
|
59
|
+
// run under is the one its `## Dispatched` entry recorded at dispatch
|
|
60
|
+
// (`agent=`, read through `ledger.mjs read`), falling back for an entry
|
|
61
|
+
// without one to its ticket row's `tier=` token (ledger-grammar.mjs's
|
|
62
|
+
// `expectedDefinition` — the same function `ledger.mjs dispatch` prints the
|
|
63
|
+
// definition from), the agent type
|
|
62
64
|
// it was actually dispatched as is
|
|
63
65
|
// read off its own transcript and must BE that definition — a generic `task`
|
|
64
66
|
// dispatch fails even when its model happens to match — and the verdict
|
|
@@ -468,8 +470,8 @@ function main() {
|
|
|
468
470
|
}
|
|
469
471
|
|
|
470
472
|
// The ledger is read at most once for evaluation: every implementer's
|
|
471
|
-
// expected definition comes off its
|
|
472
|
-
//
|
|
473
|
+
// expected definition comes off its `## Dispatched` entry or its row, and
|
|
474
|
+
// nothing this loop does writes either.
|
|
473
475
|
let ledgerData = null;
|
|
474
476
|
let receiptSession;
|
|
475
477
|
const results = entries.map((raw) => {
|
|
@@ -479,7 +481,7 @@ function main() {
|
|
|
479
481
|
die(`batch entry missing member/agentFile: ${JSON.stringify(raw)}`);
|
|
480
482
|
}
|
|
481
483
|
if (impl && raw.agentFile) {
|
|
482
|
-
die(`${raw.member}: an impl- entry carries no agentFile — its definition is
|
|
484
|
+
die(`${raw.member}: an impl- entry carries no agentFile — its definition is the one its ## Dispatched entry recorded at dispatch, else the one its ledger row's tier= token names`);
|
|
483
485
|
}
|
|
484
486
|
const hasJobRecord = raw.resolvedModel && raw.resolvedThinkingLevel;
|
|
485
487
|
let frontmatter = null, transcriptText = null, ticket = null, definition = null;
|
|
@@ -487,7 +489,13 @@ function main() {
|
|
|
487
489
|
if (impl) {
|
|
488
490
|
ticket = parsed.number;
|
|
489
491
|
ledgerData ??= readLedger(ledgerFile);
|
|
490
|
-
|
|
492
|
+
// A `## Dispatched` entry for this member that the grammar cannot
|
|
493
|
+
// read, a malformed `agent=` among them, refuses: the definition it
|
|
494
|
+
// recorded is unreadable, and the row's `tier=` may have been
|
|
495
|
+
// replaced since.
|
|
496
|
+
const malformed = (ledgerData.dispatched ?? []).find((t) => t.split(/[\s=]/)[0] === raw.member && parseToken(t)?.error !== null);
|
|
497
|
+
if (malformed !== undefined) throw new Error(`## Dispatched entry '${malformed}' is malformed, so the definition it recorded cannot be read`);
|
|
498
|
+
definition = ledgerData.agents?.[raw.member] ?? expectedDefinition(rowText(ledgerData, ticket));
|
|
491
499
|
const definitionFile = join(repoRoot, "agents", `${definition}.agent.md`);
|
|
492
500
|
if (existsSync(definitionFile)) frontmatter = parseFrontmatter(readFileSync(definitionFile, "utf8"));
|
|
493
501
|
} else {
|
|
@@ -78,6 +78,8 @@ A member that dies between spawn and write is invisible — and members die in b
|
|
|
78
78
|
|
|
79
79
|
`dispatch merge-bot` also records the queue the bot is dispatched against: its `## Dispatched` entry carries `queued=<pr>,<pr>…` (`queued=none` for an empty queue), the open `ready-to-merge` PRs gh lists at that moment. A gh that cannot answer refuses the dispatch, nothing written. `settle merge-bot-<n> --from-report` judges the bot's report against that queue, not against anything the report names: a PR from it that is still open and carries the label, with no verdict in `merged`, `held`, `conflictHold` or `refused`, fails the settle at exit 3, naming the PR, and an entry without `queued=` cannot be judged (exit 2) — settle that bot by hand.
|
|
80
80
|
|
|
81
|
+
`dispatch impl-<N>` also records the definition it prints: the implementer's `## Dispatched` entry ends with `agent=<definition>` (`agent=fleet-implementer-<cell>`), and `ledger.mjs read` prints it under `agents`, by member name. tier-check judges the implementer against that record, falling back to the row's `tier=` for an entry without one, so a `tier=` replaced after the implementer settled does not change what it is judged against. `settle` and `rotate` keep the word with its entry. A value that is not `[a-z0-9]` words joined by `-` makes the entry malformed, and the ledger is refused.
|
|
82
|
+
|
|
81
83
|
## filed and ruled
|
|
82
84
|
|
|
83
85
|
`filed` is checked with `ledger.mjs check <subject>` before every `gh issue create`, so a finding already recorded this run is not filed twice. That is not a guarantee against duplicates at large; the tracker query, described under `check`'s exits and its `verdict` above, is what covers those.
|