@feigi/fleet-ctl 3.86.0 → 3.87.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/commands/review-and-fix.md +1 -1
- package/commands/run-merge-bot.md +2 -0
- package/package.json +1 -1
- package/scripts/compute-spend.mjs +2 -2
- package/scripts/fleet-heartbeat.mjs +2 -2
- package/scripts/fleet-tick.mjs +3 -2
- package/scripts/ledger.mjs +16 -14
- package/scripts/main-gain.mjs +104 -32
- package/scripts/merge-gate.mjs +2 -2
- 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 +1 -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
|
|
|
@@ -237,6 +237,8 @@ For each labeled PR clearing the hold rule, lowest first:
|
|
|
237
237
|
|
|
238
238
|
- **`head-moved-after-label`** — the PR head is neither `pre` nor `post`, nor a head whose net change the gate proved identical to `pre`'s (`rebaseCarry`, which names both). **Any third SHA is a push that landed while you waited on CI, and no CI check can see it**: `ci-state.mjs` selects the run whose `headSha` equals the *current* PR head (`scripts/ci-state.mjs`, the `r.headSha === prHead` filter), so a member's push plus its own green run satisfies every CI check while the audit behind the label belongs to a tree that is gone. A carry is proven only for the head `gh pr view` read; a different head at `ci-state`'s own read is still this row. The remedy is a fresh finisher against the new head, label untouched.
|
|
239
239
|
- **`main-gain-removed:<path>`** — merging this head would remove lines `main` gained after the PR's work began: lines a later merge landed, deleted by a branch commit that every rebase since has replayed without a conflict, so no conflict preview and no `origin/main...HEAD` read is sure to show them. `mainGain.hits[]` names each file, the landing key (`#<n>` for a merged PR, else the landing commit's 12-character SHA), the removed lines, and the `ack` line to copy. Skip the PR, leave the label alone, and report `main-gain-removed:<path>-#<pr>` with the hit list and the keys. Never restore the lines yourself — the removal may be deliberate. It clears when the PR stops removing them or its body carries `main-gain-removal: <path> <key> - <why>` for every hit; the gate reads the body each run, so editing it is enough. An acknowledged removal is listed in `mainGain.acknowledged[]` and the gate passes it — name each one, with its reason, in your report. Your report's `refused[]` entry for the PR carries the gate's `reason` as it printed it, `main-gain-removed:<path>`, and the gate's `--out` file above stays where the gate wrote it: the controller parks the PR off that entry, at the `head` and `bodyDigest` that file records the gate refused, and no merge bot is dispatched on the PR's account until its head or its body changes — though a bot dispatched for another queued PR may still evaluate it again.
|
|
240
|
+
|
|
241
|
+
Each hit line carries a `kind`: `modified` when the hunk that removed it also adds lines, none of which claimed it as an in-place edit, or `deleted` when the hunk only removes lines. A removed line edited in place that keeps every token of its text is not a hit; the gate lists it in `mainGain.edited[]` and passes it.
|
|
240
242
|
- **`main-gain-unanswerable`** — the main-gain check could not answer: a head object the main checkout does not hold after `git fetch origin`, a merge that does not resolve cleanly, a blame that failed. `mainGain.reason` says which. Stop and report `main-gain-unanswerable-#<pr>` with it, label untouched.
|
|
241
243
|
- **`behind:<n>`** — you are the only place currency is proven, and a sibling merge since your rebase expires it (the staleness below).
|
|
242
244
|
- **`instrument-set-changed` / `instruments-unanswerable`** — the instrument re-check the controller runs before its own gates (`run-team/SKILL.md`, **You read your instruments out of a tree every member can write to**), taken at this seat because nothing dispatches a merge bot to read that rule. Report `<reason>-#<pr>` with what the gate printed, leave the label alone, and do **not** re-run the gate.
|
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
|
|
@@ -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;
|
package/scripts/main-gain.mjs
CHANGED
|
@@ -29,12 +29,16 @@
|
|
|
29
29
|
// Removed lines are read off the merge the head would actually produce:
|
|
30
30
|
// `git merge-tree --write-tree <base> <head>`, diffed against `<base>` with
|
|
31
31
|
// rename detection on. That is right for a head that is behind `<base>` too,
|
|
32
|
-
// where a three-dot diff is not. Every removed line is a candidate,
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
32
|
+
// where a three-dot diff is not. Every removed line is a candidate, and every
|
|
33
|
+
// line of a deleted file is, except a move: a removed line whose exact text is
|
|
34
|
+
// added back in the same file. A candidate edited in place is not a hit: an
|
|
35
|
+
// added line of the same hunk that still holds every token of it claims it, and
|
|
36
|
+
// each added line claims at most one. A token is a run of letters, digits or
|
|
37
|
+
// underscores, or else one other character that is not whitespace, so
|
|
38
|
+
// appending a name to an import is an in-place edit and dropping one is not.
|
|
39
|
+
// Binary files cannot be read line by line and are listed in `unchecked[]`
|
|
40
|
+
// instead of blocking. A merge-tree that reports conflicts is a head that
|
|
41
|
+
// cannot merge as it stands, which is no answer about its content.
|
|
38
42
|
//
|
|
39
43
|
// Each hit carries a landing key: `#<n>` when the landing commit's subject is
|
|
40
44
|
// GitHub's merge subject for PR n, otherwise that commit's 12-character
|
|
@@ -43,10 +47,15 @@
|
|
|
43
47
|
// with an empty reason does not count. Acknowledged hits move from `hits[]`
|
|
44
48
|
// to `acknowledged[]` and both are reported.
|
|
45
49
|
//
|
|
46
|
-
// Stdout: one JSON line, `{head, base, since, hits, acknowledged,
|
|
47
|
-
// reason}`, `since` being R as an ISO timestamp. Each hit is `{path,
|
|
48
|
-
// landed, lines: [{line, text}], ack}`, `path` the file's name on
|
|
49
|
-
// `ack` the body line to copy, reason left for the author to
|
|
50
|
+
// Stdout: one JSON line, `{head, base, since, hits, acknowledged, edited,
|
|
51
|
+
// unchecked, reason}`, `since` being R as an ISO timestamp. Each hit is `{path,
|
|
52
|
+
// key, landed, lines: [{line, text, kind}], ack}`, `path` the file's name on
|
|
53
|
+
// `<base>` and `ack` the body line to copy, reason left for the author to
|
|
54
|
+
// write. `kind` is `modified` when the hunk that removed the line also adds
|
|
55
|
+
// lines, and `deleted` when it only removes them. `edited` lists the gained
|
|
56
|
+
// lines an in-place edit kept, as `{path, line, text, replacement}`, the
|
|
57
|
+
// `replacement` being the added line that holds their tokens. An edited line is
|
|
58
|
+
// not a hit and needs no acknowledgement.
|
|
50
59
|
//
|
|
51
60
|
// 0 clean, or every hit acknowledged
|
|
52
61
|
// 1 at least one unacknowledged hit reason main-gain-removed:<first path>
|
|
@@ -130,33 +139,72 @@ function changedPaths(tree) {
|
|
|
130
139
|
return out;
|
|
131
140
|
}
|
|
132
141
|
|
|
133
|
-
//
|
|
142
|
+
// The hunks of one path, `{removed: [{line, text}], added: [text]}` each, in
|
|
143
|
+
// diff order, or `binary`. Removed lines are numbered on `<base>`.
|
|
134
144
|
function fileDiff(tree, oldPath, newPath) {
|
|
135
145
|
const paths = oldPath === newPath ? [oldPath] : [oldPath, newPath];
|
|
136
146
|
const r = git(["diff-tree", "-r", "-M", "-p", "-U0", "--no-color", "--no-ext-diff", "--no-textconv", baseRev, tree, "--", ...paths]);
|
|
137
147
|
if (r === null) unanswerable(`diff-unreadable:${oldPath}`);
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
let inHunk = false;
|
|
148
|
+
const hunks = [];
|
|
149
|
+
let hunk = null;
|
|
141
150
|
let oldLine = 0;
|
|
142
151
|
for (const line of r.stdout.split("\n")) {
|
|
143
152
|
if (line.startsWith("diff --git ")) {
|
|
144
|
-
|
|
153
|
+
hunk = null;
|
|
145
154
|
continue;
|
|
146
155
|
}
|
|
147
156
|
const h = /^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/.exec(line);
|
|
148
157
|
if (h) {
|
|
149
|
-
|
|
158
|
+
hunk = { removed: [], added: [] };
|
|
159
|
+
hunks.push(hunk);
|
|
150
160
|
oldLine = Number(h[1]);
|
|
151
|
-
} else if (
|
|
161
|
+
} else if (hunk === null) {
|
|
152
162
|
if (line.startsWith("Binary files ")) return { binary: true };
|
|
153
163
|
} else if (line.startsWith("-")) {
|
|
154
|
-
removed.push({ line: oldLine++, text: line.slice(1) });
|
|
164
|
+
hunk.removed.push({ line: oldLine++, text: line.slice(1) });
|
|
155
165
|
} else if (line.startsWith("+")) {
|
|
156
|
-
added.push(line.slice(1));
|
|
166
|
+
hunk.added.push(line.slice(1));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { binary: false, hunks };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// The tokens an in-place edit keeps: a run of letters, digits or underscores is
|
|
173
|
+
// one token, and each other character that is not whitespace is one.
|
|
174
|
+
const TOKEN = /[\p{L}\p{N}_]+|[^\s\p{L}\p{N}_]/gu;
|
|
175
|
+
function tokensOf(text) {
|
|
176
|
+
const counts = new Map();
|
|
177
|
+
for (const t of text.match(TOKEN) ?? []) counts.set(t, (counts.get(t) ?? 0) + 1);
|
|
178
|
+
return counts;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Each unclaimed added line of one hunk claims at most one removed line of that
|
|
182
|
+
// hunk. Removed lines choose in turn, most tokens first so a short line does not
|
|
183
|
+
// take the added line a long one needs, and each takes the first unclaimed added
|
|
184
|
+
// line still holding every token of it. That is greedy: a removed line can stay
|
|
185
|
+
// a hit where another pairing would have matched every line, an error toward
|
|
186
|
+
// blocking. The rarest token of a removed line bounds the added lines tried for
|
|
187
|
+
// it.
|
|
188
|
+
function claimInPlace(removed, added) {
|
|
189
|
+
const index = new Map();
|
|
190
|
+
for (const a of added) {
|
|
191
|
+
if (a.claimed) continue;
|
|
192
|
+
a.tokens = tokensOf(a.text);
|
|
193
|
+
for (const t of a.tokens.keys()) {
|
|
194
|
+
if (!index.has(t)) index.set(t, []);
|
|
195
|
+
index.get(t).push(a);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const size = (counts) => [...counts.values()].reduce((s, n) => s + n, 0);
|
|
199
|
+
for (const c of [...removed].sort((x, y) => size(y.tokens) - size(x.tokens))) {
|
|
200
|
+
if (c.tokens.size === 0) continue;
|
|
201
|
+
const pool = [...c.tokens.keys()].map((t) => index.get(t) ?? []).reduce((best, l) => (l.length < best.length ? l : best));
|
|
202
|
+
const a = pool.find((x) => !x.claimed && [...c.tokens].every(([t, n]) => (x.tokens.get(t) ?? 0) >= n));
|
|
203
|
+
if (a) {
|
|
204
|
+
a.claimed = true;
|
|
205
|
+
c.replacement = a.text;
|
|
157
206
|
}
|
|
158
207
|
}
|
|
159
|
-
return { binary: false, removed, added };
|
|
160
208
|
}
|
|
161
209
|
|
|
162
210
|
// line number on `<base>` → {sha, time, summary} for the requested lines,
|
|
@@ -221,6 +269,7 @@ function check() {
|
|
|
221
269
|
|
|
222
270
|
const hits = [];
|
|
223
271
|
const unchecked = [];
|
|
272
|
+
const edited = [];
|
|
224
273
|
// No commits of its own: the head is already on `<base>` and merges nothing.
|
|
225
274
|
if (since !== null) {
|
|
226
275
|
for (const { oldMode, status, oldPath, newPath } of changedPaths(tree)) {
|
|
@@ -234,25 +283,47 @@ function check() {
|
|
|
234
283
|
unchecked.push(oldPath);
|
|
235
284
|
continue;
|
|
236
285
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
286
|
+
// A removed line whose exact text is added back in the same file is a
|
|
287
|
+
// move, and the added line is spent on it.
|
|
288
|
+
const addsByHunk = d.hunks.map(() => []);
|
|
289
|
+
const twins = new Map();
|
|
290
|
+
d.hunks.forEach((h, hunk) => {
|
|
291
|
+
for (const text of h.added) {
|
|
292
|
+
const a = { text, claimed: false, tokens: null };
|
|
293
|
+
addsByHunk[hunk].push(a);
|
|
294
|
+
if (!twins.has(text)) twins.set(text, []);
|
|
295
|
+
twins.get(text).push(a);
|
|
296
|
+
}
|
|
244
297
|
});
|
|
245
|
-
|
|
246
|
-
|
|
298
|
+
const candsByHunk = d.hunks.map(() => []);
|
|
299
|
+
d.hunks.forEach((h, hunk) => {
|
|
300
|
+
for (const { line, text } of h.removed) {
|
|
301
|
+
const spare = twins.get(text);
|
|
302
|
+
if (spare?.length) {
|
|
303
|
+
spare.pop().claimed = true;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const kind = h.added.length > 0 ? "modified" : "deleted";
|
|
307
|
+
candsByHunk[hunk].push({ line, text, kind, tokens: tokensOf(text), replacement: null });
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
d.hunks.forEach((_, hunk) => claimInPlace(candsByHunk[hunk], addsByHunk[hunk]));
|
|
311
|
+
const cands = candsByHunk.flat();
|
|
312
|
+
if (cands.length === 0) continue;
|
|
313
|
+
const blamed = blame(oldPath, cands);
|
|
247
314
|
const byKey = new Map();
|
|
248
|
-
for (const c of
|
|
315
|
+
for (const c of cands) {
|
|
249
316
|
const b = blamed.get(c.line);
|
|
250
317
|
if (b.time <= since) continue;
|
|
318
|
+
if (c.replacement !== null) {
|
|
319
|
+
edited.push({ path: oldPath, line: c.line, text: c.text, replacement: c.replacement });
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
251
322
|
const key = landingKey(b.sha, b.summary);
|
|
252
323
|
if (!byKey.has(key)) {
|
|
253
324
|
byKey.set(key, { path: oldPath, key, landed: b.sha, lines: [], ack: `main-gain-removal: ${oldPath} ${key} - <why>` });
|
|
254
325
|
}
|
|
255
|
-
byKey.get(key).lines.push(c);
|
|
326
|
+
byKey.get(key).lines.push({ line: c.line, text: c.text, kind: c.kind });
|
|
256
327
|
}
|
|
257
328
|
hits.push(...byKey.values());
|
|
258
329
|
}
|
|
@@ -270,6 +341,7 @@ function check() {
|
|
|
270
341
|
since: since === null ? null : new Date(since * 1000).toISOString(),
|
|
271
342
|
hits: open,
|
|
272
343
|
acknowledged,
|
|
344
|
+
edited,
|
|
273
345
|
unchecked,
|
|
274
346
|
reason: open.length ? `main-gain-removed:${open[0].path}` : null,
|
|
275
347
|
};
|
|
@@ -282,7 +354,7 @@ try {
|
|
|
282
354
|
result = check();
|
|
283
355
|
} catch (e) {
|
|
284
356
|
if (!(e instanceof Unanswerable)) die(`check failed: ${e.code ?? e.message} — could not evaluate`);
|
|
285
|
-
result = { since: null, hits: [], acknowledged: [], unchecked: [], reason: e.message, unknown: true };
|
|
357
|
+
result = { since: null, hits: [], acknowledged: [], edited: [], unchecked: [], reason: e.message, unknown: true };
|
|
286
358
|
}
|
|
287
359
|
const { unknown, ...payload } = result;
|
|
288
360
|
writeAll(1, `${JSON.stringify({ head, base, ...payload })}\n`);
|
package/scripts/merge-gate.mjs
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// `bodyDigest`), or null with `head` when gh could not be read, `rebaseCarry`
|
|
32
32
|
// naming the labelled head and the head accepted as carrying it (`{labelled,
|
|
33
33
|
// accepted}`), or null when no carry was proven, `mainGain` echoing
|
|
34
|
-
// main-gain.mjs's own payload — hits, acknowledged removals and unchecked
|
|
34
|
+
// main-gain.mjs's own payload — hits, acknowledged removals, edited lines and unchecked
|
|
35
35
|
// files — or null when it did not run or printed no JSON object; an unusable
|
|
36
36
|
// payload is still echoed. Stderr: the children's own diagnostics, passed
|
|
37
37
|
// straight through and never folded into stdout.
|
|
@@ -298,7 +298,7 @@ function readMainGain(root, prView) {
|
|
|
298
298
|
const payload = isObject(v) ? v : null;
|
|
299
299
|
const read = (usable) => ({ usable, exit: r.status, payload });
|
|
300
300
|
if (r.error || r.signal || payload === null || payload.head !== prView.headRefOid) return read(false);
|
|
301
|
-
if (![payload.hits, payload.acknowledged, payload.unchecked].every(Array.isArray)) return read(false);
|
|
301
|
+
if (![payload.hits, payload.acknowledged, payload.edited, payload.unchecked].every(Array.isArray)) return read(false);
|
|
302
302
|
if (!payload.hits.every((h) => isObject(h) && typeof h.path === "string" && h.path !== "")) return read(false);
|
|
303
303
|
return read((r.status === 0 && payload.hits.length === 0) || (r.status === 1 && payload.hits.length > 0));
|
|
304
304
|
}
|
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
|
@@ -831,7 +831,7 @@ and flag the PR for a human. A new review of the PR counts again from none.
|
|
|
831
831
|
|
|
832
832
|
| Failure | Response |
|
|
833
833
|
|---|---|
|
|
834
|
-
| Merge bot's gate finds the merge would remove lines `main` gained after the PR's work began (`main-gain-removed:<path>`, exit 1) | Relay `main-gain-removed:<path>-#<pr>` to a human with the hit list and the acknowledgement keys the bot printed. PR stays queued, label untouched, and no member is dispatched to restore the lines — the removal may be deliberate. `fleet-event.mjs merge-bot-report` parks it: a `parked=<head>:<body digest>:<reason>` token on its row, at the head and body digest the bot's gate reading `<scratch>/pr<pr>/merge-bot-<n>/ci.json` records it refused, and no bot is dispatched on its account — when nothing else queued is dispatchable the tick prints `HOLD (parked #<pr> <reason>)`, though a bot dispatched for another queued PR may still evaluate it again; a new head or an edited body lifts the park with no write. It clears when the PR stops removing them or its body carries `main-gain-removal: <path> <key> - <why>` for each hit |
|
|
834
|
+
| Merge bot's gate finds the merge would remove lines `main` gained after the PR's work began (`main-gain-removed:<path>`, exit 1) | Relay `main-gain-removed:<path>-#<pr>` to a human with the hit list and the acknowledgement keys the bot printed. PR stays queued, label untouched, and no member is dispatched to restore the lines — the removal may be deliberate. `fleet-event.mjs merge-bot-report` parks it: a `parked=<head>:<body digest>:<reason>` token on its row, at the head and body digest the bot's gate reading `<scratch>/pr<pr>/merge-bot-<n>/ci.json` records it refused, and no bot is dispatched on its account — when nothing else queued is dispatchable the tick prints `HOLD (parked #<pr> <reason>)`, though a bot dispatched for another queued PR may still evaluate it again; a new head or an edited body lifts the park with no write. A hit line's `kind` is `modified` when its hunk also adds lines, `deleted` when it only removes lines; a gained line edited in place keeping every token is listed in `mainGain.edited[]`, not a hit. It clears when the PR stops removing them or its body carries `main-gain-removal: <path> <key> - <why>` for each hit |
|
|
835
835
|
| Merge bot's gate cannot answer the main-gain check (`main-gain-unanswerable`, exit 2) | Report `main-gain-unanswerable-#<pr>` with what it printed, PR stays queued, label untouched |
|
|
836
836
|
|
|
837
837
|
## Failure handling
|