@feigi/fleet-ctl 3.94.0 → 3.95.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/agents/fleet-implementer-slow-high.agent.md +5 -1
- package/agents/fleet-implementer-slow-medium.agent.md +5 -1
- package/agents/fleet-implementer-smol-high.agent.md +5 -1
- package/agents/fleet-implementer-task-high.agent.md +5 -1
- package/agents/fleet-implementer-task-max.agent.md +5 -1
- package/package.json +1 -1
- package/scripts/fleet-event.mjs +1 -0
- package/scripts/ledger.mjs +5 -0
- package/scripts/member-outcomes.mjs +2 -0
- package/scripts/pr-cost.mjs +22 -66
- package/scripts/pull-attribution.mjs +70 -10
- package/scripts/report-files.mjs +1 -1
- package/scripts/report-schema.mjs +28 -10
- package/scripts/router-table.json +8 -0
- package/scripts/ticket-router.mjs +122 -32
- package/skills/run-team/SKILL.md +2 -4
- package/skills/run-team/references/failure-handling.md +7 -0
|
@@ -240,6 +240,10 @@ including if the `Sizing:` line was written before the run and corrected after;
|
|
|
240
240
|
`sizing.verdict` and `sizing.signal` are that line's verdict and signal.
|
|
241
241
|
Nothing in the PR body separates a measured verdict from one typed in early;
|
|
242
242
|
that field is the only thing that does, and an unreported ordering costs the
|
|
243
|
-
covariate. A bail is `outcome` `bailed`, its cause in `bailCause
|
|
243
|
+
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`: the
|
|
244
|
+
pre-code outcome, before you touch code. A block or ambiguity you find after
|
|
245
|
+
implementation began — the work cannot finish without a ruling or human hands
|
|
246
|
+
you do not have — is `outcome` `blocked` instead, with `pr`, `head` and `bailCause` `null` and
|
|
247
|
+
the reason in `bailDetail`; report it rather than ending with no report. Then send
|
|
244
248
|
the one line your brief gives, `<your name>: <outcome> <path>`, and exit. Never
|
|
245
249
|
apply `ready-to-merge`, never merge.
|
|
@@ -240,6 +240,10 @@ including if the `Sizing:` line was written before the run and corrected after;
|
|
|
240
240
|
`sizing.verdict` and `sizing.signal` are that line's verdict and signal.
|
|
241
241
|
Nothing in the PR body separates a measured verdict from one typed in early;
|
|
242
242
|
that field is the only thing that does, and an unreported ordering costs the
|
|
243
|
-
covariate. A bail is `outcome` `bailed`, its cause in `bailCause
|
|
243
|
+
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`: the
|
|
244
|
+
pre-code outcome, before you touch code. A block or ambiguity you find after
|
|
245
|
+
implementation began — the work cannot finish without a ruling or human hands
|
|
246
|
+
you do not have — is `outcome` `blocked` instead, with `pr`, `head` and `bailCause` `null` and
|
|
247
|
+
the reason in `bailDetail`; report it rather than ending with no report. Then send
|
|
244
248
|
the one line your brief gives, `<your name>: <outcome> <path>`, and exit. Never
|
|
245
249
|
apply `ready-to-merge`, never merge.
|
|
@@ -240,6 +240,10 @@ including if the `Sizing:` line was written before the run and corrected after;
|
|
|
240
240
|
`sizing.verdict` and `sizing.signal` are that line's verdict and signal.
|
|
241
241
|
Nothing in the PR body separates a measured verdict from one typed in early;
|
|
242
242
|
that field is the only thing that does, and an unreported ordering costs the
|
|
243
|
-
covariate. A bail is `outcome` `bailed`, its cause in `bailCause
|
|
243
|
+
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`: the
|
|
244
|
+
pre-code outcome, before you touch code. A block or ambiguity you find after
|
|
245
|
+
implementation began — the work cannot finish without a ruling or human hands
|
|
246
|
+
you do not have — is `outcome` `blocked` instead, with `pr`, `head` and `bailCause` `null` and
|
|
247
|
+
the reason in `bailDetail`; report it rather than ending with no report. Then send
|
|
244
248
|
the one line your brief gives, `<your name>: <outcome> <path>`, and exit. Never
|
|
245
249
|
apply `ready-to-merge`, never merge.
|
|
@@ -240,6 +240,10 @@ including if the `Sizing:` line was written before the run and corrected after;
|
|
|
240
240
|
`sizing.verdict` and `sizing.signal` are that line's verdict and signal.
|
|
241
241
|
Nothing in the PR body separates a measured verdict from one typed in early;
|
|
242
242
|
that field is the only thing that does, and an unreported ordering costs the
|
|
243
|
-
covariate. A bail is `outcome` `bailed`, its cause in `bailCause
|
|
243
|
+
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`: the
|
|
244
|
+
pre-code outcome, before you touch code. A block or ambiguity you find after
|
|
245
|
+
implementation began — the work cannot finish without a ruling or human hands
|
|
246
|
+
you do not have — is `outcome` `blocked` instead, with `pr`, `head` and `bailCause` `null` and
|
|
247
|
+
the reason in `bailDetail`; report it rather than ending with no report. Then send
|
|
244
248
|
the one line your brief gives, `<your name>: <outcome> <path>`, and exit. Never
|
|
245
249
|
apply `ready-to-merge`, never merge.
|
|
@@ -240,6 +240,10 @@ including if the `Sizing:` line was written before the run and corrected after;
|
|
|
240
240
|
`sizing.verdict` and `sizing.signal` are that line's verdict and signal.
|
|
241
241
|
Nothing in the PR body separates a measured verdict from one typed in early;
|
|
242
242
|
that field is the only thing that does, and an unreported ordering costs the
|
|
243
|
-
covariate. A bail is `outcome` `bailed`, its cause in `bailCause
|
|
243
|
+
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`: the
|
|
244
|
+
pre-code outcome, before you touch code. A block or ambiguity you find after
|
|
245
|
+
implementation began — the work cannot finish without a ruling or human hands
|
|
246
|
+
you do not have — is `outcome` `blocked` instead, with `pr`, `head` and `bailCause` `null` and
|
|
247
|
+
the reason in `bailDetail`; report it rather than ending with no report. Then send
|
|
244
248
|
the one line your brief gives, `<your name>: <outcome> <path>`, and exit. Never
|
|
245
249
|
apply `ready-to-merge`, never merge.
|
package/package.json
CHANGED
package/scripts/fleet-event.mjs
CHANGED
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
// opens with no sha, is a NEED and N is left untouched, claim and all.
|
|
51
51
|
// The PR a `covered` detail names is never written to.
|
|
52
52
|
// A `PR#M` outcome also sets the claim branch's upstream when it has none: `reap.sh` keeps a branch with no upstream, and only an upstream's `[gone]` state makes it reapable. It writes `branch.<b>.remote` and `branch.<b>.merge` only while the merge key is unset, and prints `NEED upstream <b> — <why>` when a write fails.
|
|
53
|
+
// A `blocked` outcome takes no action beyond the settle: no label, comment, release or upstream, so the ticket keeps `in-progress`.
|
|
53
54
|
// fix-report <fix-pr-M[-x]>
|
|
54
55
|
// `settle --from-report`, which records the filings and rulings the
|
|
55
56
|
// report carries. Settled `applied:`/`no-op`, on a PR whose rows carry
|
package/scripts/ledger.mjs
CHANGED
|
@@ -2508,6 +2508,9 @@ function recordFiled(id, subject) {
|
|
|
2508
2508
|
// own repository name N;
|
|
2509
2509
|
// the Check-step record (below) holds for <head>
|
|
2510
2510
|
// impl-N=bailed bailCause is not null; the receipt names it
|
|
2511
|
+
// impl-N=blocked nothing to check: report-schema.mjs already
|
|
2512
|
+
// refuses a block that names a bailCause, a pr or
|
|
2513
|
+
// a head, or gives no reason in bailDetail
|
|
2511
2514
|
// fix-pr-M=applied:<head> origin's refs/heads/<branch> is <head>, fetched
|
|
2512
2515
|
// by verify-sha.sh; the Check-step record (below)
|
|
2513
2516
|
// holds for <head>
|
|
@@ -2670,6 +2673,8 @@ async function runSettleFromReport() {
|
|
|
2670
2673
|
const { view: pr, numbers } = prViewClosing(report.pr, "state,closingIssuesReferences,url");
|
|
2671
2674
|
check(`PR #${report.pr} reads OPEN`, pr.state === "OPEN");
|
|
2672
2675
|
check(`PR #${report.pr} closes #${live.number}`, numbers.includes(live.number));
|
|
2676
|
+
} else if (report.outcome === "blocked") {
|
|
2677
|
+
outcome = "blocked";
|
|
2673
2678
|
} else {
|
|
2674
2679
|
outcome = "bailed";
|
|
2675
2680
|
check(`bailCause: ${report.bailCause}`, report.bailCause !== null);
|
|
@@ -146,6 +146,7 @@ export function withReports(rows, scratch) {
|
|
|
146
146
|
// opened one (more than one leaves the pr column blank), or
|
|
147
147
|
// the last report claims a PR an earlier same-ticket
|
|
148
148
|
// implementer opened in this session;
|
|
149
|
+
// blocked the last report blocks mid-implementation;
|
|
149
150
|
// bail:<cause> the last report bails (`fixed` is checked against main);
|
|
150
151
|
// provider a model fell back, or the last turn errored;
|
|
151
152
|
// budget a budget notice reached the member;
|
|
@@ -162,6 +163,7 @@ export function endingOf({ member, pr, signals = null, earlierPrs = new Set(), v
|
|
|
162
163
|
if (signals.openedPrs.length > 0) return "pr";
|
|
163
164
|
const last = lastReport(signals.reportWrites, member.split("/").pop());
|
|
164
165
|
if (last?.outcome === "pr" && earlierPrs.has(String(last.pr))) return "pr";
|
|
166
|
+
if (last?.outcome === "blocked") return "blocked";
|
|
165
167
|
// lastReport passed the impl schema, whose bailCause is a BAILS key or null.
|
|
166
168
|
if (last?.bailCause) return bailEnding(last, verifyMain);
|
|
167
169
|
if (signals.providerFallback || signals.lastStopReason === "error") return "provider";
|
package/scripts/pr-cost.mjs
CHANGED
|
@@ -65,7 +65,7 @@ import { gitEnv } from "./git-env.mjs";
|
|
|
65
65
|
import { fleetFile, FleetDirUnresolvable } from "./fleet-dir.mjs";
|
|
66
66
|
import { isCLI } from "./is-cli.mjs";
|
|
67
67
|
import { parseTsv as parseMemberTsv } from "./member-outcomes.mjs";
|
|
68
|
-
import { AttributionError, attribute, parseFeatures, tally } from "./pull-attribution.mjs";
|
|
68
|
+
import { AttributionError, attribute, bootUnit, bootstrapDiffCI, parseFeatures, tally, underFloor } from "./pull-attribution.mjs";
|
|
69
69
|
import { RulingError, parseTierOutcomes } from "./tier-outcomes.mjs";
|
|
70
70
|
|
|
71
71
|
const NAME = "pr-cost";
|
|
@@ -78,9 +78,6 @@ export const BASELINE_CELL = "slow-high";
|
|
|
78
78
|
// Stage 1 is the model axis at fixed `high`.
|
|
79
79
|
export const STAGE_1 = ["slow-high", "task-high", "smol-high"];
|
|
80
80
|
export const MIN_N = 20;
|
|
81
|
-
// A cell trips when its fail rate is at least this many points above the
|
|
82
|
-
// baseline's, or its mean $ is at least the baseline's.
|
|
83
|
-
export const TRIP_POINTS = 15;
|
|
84
81
|
export const EXIT = { ok: 0, tripped: 3, none: 4 };
|
|
85
82
|
export const GH_PR_LIMIT = 1000;
|
|
86
83
|
|
|
@@ -94,77 +91,36 @@ function median(xs) {
|
|
|
94
91
|
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
95
92
|
}
|
|
96
93
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return () => {
|
|
101
|
-
a = (a + 0x6d2b79f5) >>> 0;
|
|
102
|
-
let t = a;
|
|
103
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
104
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
105
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Each unit is one booked group: `{ usd, merged }`. mean_usd is Σusd / Σmerged.
|
|
110
|
-
export function bootstrapDiffCI(cellUnits, baseUnits, { iterations = 2000, seed = 1 } = {}) {
|
|
111
|
-
if (!cellUnits.length || !baseUnits.length) return null;
|
|
112
|
-
const rand = mulberry32(seed);
|
|
113
|
-
const resample = (units) => {
|
|
114
|
-
let u = 0, m = 0;
|
|
115
|
-
for (let i = 0; i < units.length; i++) {
|
|
116
|
-
const x = units[Math.floor(rand() * units.length)];
|
|
117
|
-
u += x.usd; m += x.merged;
|
|
118
|
-
}
|
|
119
|
-
return m ? u / m : null;
|
|
120
|
-
};
|
|
121
|
-
const diffs = [];
|
|
122
|
-
for (let i = 0; i < iterations; i++) {
|
|
123
|
-
const a = resample(cellUnits), b = resample(baseUnits);
|
|
124
|
-
if (a !== null && b !== null) diffs.push(a - b);
|
|
125
|
-
}
|
|
126
|
-
if (diffs.length < iterations / 2) return null;
|
|
127
|
-
diffs.sort((x, y) => x - y);
|
|
128
|
-
return [round(diffs[Math.floor(0.025 * (diffs.length - 1))], 2), round(diffs[Math.ceil(0.975 * (diffs.length - 1))], 2)];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// The trip rule, in integer counts so a 3-of-20 difference is exactly 15
|
|
132
|
-
// points rather than 14.999… in floating point. `n` is a cell's claims and
|
|
133
|
-
// `n_pass` the claims among them that held.
|
|
94
|
+
// The trip rule: the quality floor (pull-attribution.mjs's `underFloor`, in
|
|
95
|
+
// integer counts), or a mean $ at least the baseline's. Each side is
|
|
96
|
+
// `{ claims, failed, mean_usd }`.
|
|
134
97
|
export function trips(cell, base) {
|
|
135
|
-
|
|
136
|
-
const failRise = (failC * base.n - failB * cell.n) * 100 >= TRIP_POINTS * cell.n * base.n;
|
|
137
|
-
return failRise || cell.mean_usd >= base.mean_usd;
|
|
98
|
+
return underFloor(cell, base) || cell.mean_usd >= base.mean_usd;
|
|
138
99
|
}
|
|
139
100
|
|
|
140
101
|
export function computeReport({ members, tiers, features, prs, routerTable = null, pricing = null, windowStart = WINDOW_START }) {
|
|
141
102
|
const units = attribute({ features, members, tiers, prs, windowStart });
|
|
142
|
-
const counted = units.filter((u) => u.status === "counted");
|
|
143
103
|
const tallies = new Map(tally(units).map((t) => [t.cell, t]));
|
|
144
|
-
const
|
|
145
|
-
const bootUnits = (us) => us.map((u) => ({ usd: u.spend, merged: u.merged ? 1 : 0 }));
|
|
104
|
+
const bootUnits = (g) => g.pulls.map(bootUnit);
|
|
146
105
|
const meanUsd = (t) => (t.merged ? t.spend / t.merged : null);
|
|
147
106
|
const base = tallies.get(BASELINE_CELL) ?? null;
|
|
148
107
|
// One generation's figures. The interval is the newest generation's against
|
|
149
108
|
// the baseline's newest; an older generation is printed, never compared.
|
|
150
|
-
const figuresOf = (cell, g, newest) => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
unpriced: g.unpriced,
|
|
166
|
-
};
|
|
167
|
-
};
|
|
109
|
+
const figuresOf = (cell, g, newest) => ({
|
|
110
|
+
cell,
|
|
111
|
+
generation: g.generation,
|
|
112
|
+
n_pulls: g.n,
|
|
113
|
+
n_merged: g.merged,
|
|
114
|
+
n_claims: g.claims,
|
|
115
|
+
n_failed: g.failed,
|
|
116
|
+
fail_rate: g.claims ? round(g.failed / g.claims) : null,
|
|
117
|
+
mean_usd: round(meanUsd(g), 2),
|
|
118
|
+
median_usd: round(median(g.pulls.filter((u) => u.merged).map((u) => u.spend)), 2),
|
|
119
|
+
router_usd: round(g.pulls.reduce((s, u) => s + routerUsd(u.pull), 0), 4),
|
|
120
|
+
usd_diff_ci95: !newest || cell === BASELINE_CELL || !base ? null : bootstrapDiffCI(bootUnits(g), bootUnits(base)),
|
|
121
|
+
left_out: g.left_out,
|
|
122
|
+
unpriced: g.unpriced,
|
|
123
|
+
});
|
|
168
124
|
const figures = [...tallies.values()].map((t) => ({
|
|
169
125
|
...figuresOf(t.cell, t, true),
|
|
170
126
|
excluded: t.excluded,
|
|
@@ -177,7 +133,7 @@ export function computeReport({ members, tiers, features, prs, routerTable = nul
|
|
|
177
133
|
// The $ leg takes the unrounded means: the cent-rounded figures are for
|
|
178
134
|
// display, and a trip is sticky, so a cell cheaper by under half a cent
|
|
179
135
|
// must not trip on rounding alone. The fail leg reads the claims.
|
|
180
|
-
const quality = (t) => ({
|
|
136
|
+
const quality = (t) => ({ claims: t.claims, failed: t.failed, mean_usd: meanUsd(t) });
|
|
181
137
|
const tripped = [];
|
|
182
138
|
if (baseline.n >= MIN_N) {
|
|
183
139
|
for (const t of tallies.values()) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// The one booking of member rows, merges and claims to Pulls, read by both the
|
|
2
2
|
// cost guard (pr-cost.mjs) and the router's fit (ticket-router.mjs), so the two
|
|
3
3
|
// print the same figures for a cell. Neither CLI imports the other; both import
|
|
4
|
-
// this
|
|
4
|
+
// this, and with it the two judgements they share: the quality floor (the
|
|
5
|
+
// guard's fail leg) and the bootstrap range of a $ per merged PR difference.
|
|
5
6
|
//
|
|
6
7
|
// THE UNIT is the Pull, a ticket-features.tsv row, booked AS ROUTED to its
|
|
7
8
|
// `chosen_cell`: its merge, its Ruling and all its spend. A ticket Pulled twice
|
|
@@ -38,8 +39,8 @@
|
|
|
38
39
|
// counted every other Pull: the only ones in a cell's figures.
|
|
39
40
|
//
|
|
40
41
|
// ENDINGS come from member-outcomes' `ending` column. The cell's: `hollow`,
|
|
41
|
-
// `budget`, `bail:fixed-refuted`, `unknown`. The ticket's: every
|
|
42
|
-
// `bail:<cause>`. Neither's: `provider`, `crash`, `abort`.
|
|
42
|
+
// `budget`, `bail:fixed-refuted`, `unknown`. The ticket's: `blocked` and every
|
|
43
|
+
// other `bail:<cause>`. Neither's: `provider`, `crash`, `abort`.
|
|
43
44
|
//
|
|
44
45
|
// CLAIMS. A merged Pull is one claim, false when its Ruling fails the floor
|
|
45
46
|
// (`minted_false_claim=yes` or `closed_own_ticket=no`). Each `bail:fixed` or
|
|
@@ -59,6 +60,10 @@
|
|
|
59
60
|
// Generations are told apart by model alone, so a cell that returns to an
|
|
60
61
|
// earlier model rejoins that generation's Pulls. Only the newest generation's
|
|
61
62
|
// figures are judged; older ones are reported beside them, never pooled.
|
|
63
|
+
//
|
|
64
|
+
// THE FLOOR. A cell is under the floor against a baseline when its fail rate
|
|
65
|
+
// is TRIP_POINTS or more points above the baseline's, compared in integer
|
|
66
|
+
// counts. The guard holds it pooled; the fit holds it per row.
|
|
62
67
|
|
|
63
68
|
import { isDigits } from "./arg.mjs";
|
|
64
69
|
import { CELL } from "./ledger-grammar.mjs";
|
|
@@ -73,6 +78,57 @@ export const FEATURE_COLUMNS = Object.freeze([
|
|
|
73
78
|
"paths", "test_paths", "xrefs", "kind",
|
|
74
79
|
]);
|
|
75
80
|
|
|
81
|
+
// The quality floor's margin, in points of fail rate.
|
|
82
|
+
export const TRIP_POINTS = 15;
|
|
83
|
+
|
|
84
|
+
// The floor, in integer counts so a 3-of-20 difference is exactly 15 points
|
|
85
|
+
// rather than 14.999… in floating point. Each side is `{ claims, failed }`.
|
|
86
|
+
export function underFloor(cell, base) {
|
|
87
|
+
return (cell.failed * base.claims - base.failed * cell.claims) * 100 >= TRIP_POINTS * cell.claims * base.claims;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const round2 = (x) => (Number.isFinite(x) ? Number(x.toFixed(2)) : null);
|
|
91
|
+
|
|
92
|
+
// Deterministic, so a re-run reads the same range.
|
|
93
|
+
function mulberry32(seed) {
|
|
94
|
+
let a = seed >>> 0;
|
|
95
|
+
return () => {
|
|
96
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
97
|
+
let t = a;
|
|
98
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
99
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
100
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The bootstrap 95% range of (cell − base) $ per merged PR, its ends rounded
|
|
105
|
+
// to cents. Each unit is one counted Pull: `{ usd, merged }`, merged 0 or 1.
|
|
106
|
+
// $ per merged PR is Σusd / Σmerged. null when either side has no unit, or
|
|
107
|
+
// when under half the resamples have a merged PR on both sides.
|
|
108
|
+
export function bootstrapDiffCI(cellUnits, baseUnits, { iterations = 2000, seed = 1 } = {}) {
|
|
109
|
+
if (!cellUnits.length || !baseUnits.length) return null;
|
|
110
|
+
const rand = mulberry32(seed);
|
|
111
|
+
const resample = (units) => {
|
|
112
|
+
let u = 0, m = 0;
|
|
113
|
+
for (let i = 0; i < units.length; i++) {
|
|
114
|
+
const x = units[Math.floor(rand() * units.length)];
|
|
115
|
+
u += x.usd; m += x.merged;
|
|
116
|
+
}
|
|
117
|
+
return m ? u / m : null;
|
|
118
|
+
};
|
|
119
|
+
const diffs = [];
|
|
120
|
+
for (let i = 0; i < iterations; i++) {
|
|
121
|
+
const a = resample(cellUnits), b = resample(baseUnits);
|
|
122
|
+
if (a !== null && b !== null) diffs.push(a - b);
|
|
123
|
+
}
|
|
124
|
+
if (diffs.length < iterations / 2) return null;
|
|
125
|
+
diffs.sort((x, y) => x - y);
|
|
126
|
+
return [round2(diffs[Math.floor(0.025 * (diffs.length - 1))]), round2(diffs[Math.ceil(0.975 * (diffs.length - 1))])];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// A counted Pull as a bootstrap unit.
|
|
130
|
+
export const bootUnit = (u) => ({ usd: u.spend, merged: u.merged ? 1 : 0 });
|
|
131
|
+
|
|
76
132
|
// ticket-features.tsv: one bare header line naming the columns, then rows.
|
|
77
133
|
// Read by header name; a row whose width differs from the header's is refused.
|
|
78
134
|
export function parseFeatures(text) {
|
|
@@ -125,7 +181,7 @@ function datedRows(features) {
|
|
|
125
181
|
|
|
126
182
|
const CELL_ENDINGS = new Set(["hollow", "budget", "bail:fixed-refuted", "unknown"]);
|
|
127
183
|
const NEITHER_ENDINGS = new Set(["provider", "crash", "abort"]);
|
|
128
|
-
const ticketsEnding = (e) => e.startsWith("bail:") && !CELL_ENDINGS.has(e);
|
|
184
|
+
const ticketsEnding = (e) => e === "blocked" || (e.startsWith("bail:") && !CELL_ENDINGS.has(e));
|
|
129
185
|
const failsFloor = (ruling) => ruling.minted_false_claim === "yes" || ruling.closed_own_ticket === "no";
|
|
130
186
|
// An omp session id opens with its ISO start time, so two of them sort in time order.
|
|
131
187
|
const STARTED = /^\d{4}-\d{2}-\d{2}T/;
|
|
@@ -309,10 +365,11 @@ export function generations(units) {
|
|
|
309
365
|
|
|
310
366
|
/**
|
|
311
367
|
* Per cell, in cell-name order: `{ cell, generation, n, merged, claims, failed,
|
|
312
|
-
* spend, excluded, left_out, unpriced, pending, older }`. `generation`
|
|
313
|
-
* cell's newest (null when no Pull of it has one), and the figures
|
|
314
|
-
* are that generation's alone: `n`, `merged`, `claims`, `failed` and
|
|
315
|
-
* over its counted Pulls,
|
|
368
|
+
* spend, pulls, excluded, left_out, unpriced, pending, older }`. `generation`
|
|
369
|
+
* is the cell's newest (null when no Pull of it has one), and the figures
|
|
370
|
+
* beside it are that generation's alone: `n`, `merged`, `claims`, `failed` and
|
|
371
|
+
* `spend` over its counted Pulls, `pulls` those counted units themselves in
|
|
372
|
+
* the order of `units`, then its left-out, unpriced and pending Pull counts.
|
|
316
373
|
* `excluded` counts the cell's excluded Pulls, which have no generation.
|
|
317
374
|
* `older` is an array, newest first, of every other generation's figures in
|
|
318
375
|
* the same shape, each carrying its `generation`. `order` is `generations()`
|
|
@@ -321,7 +378,10 @@ export function generations(units) {
|
|
|
321
378
|
* never moves a cell's newest generation.
|
|
322
379
|
*/
|
|
323
380
|
export function tally(units, order = generations(units)) {
|
|
324
|
-
|
|
381
|
+
// `n` is read off `pulls`, so the count and the list cannot drift apart.
|
|
382
|
+
const figures = (generation) => ({
|
|
383
|
+
generation, get n() { return this.pulls.length; }, merged: 0, claims: 0, failed: 0, spend: 0, pulls: [], left_out: 0, unpriced: 0, pending: 0,
|
|
384
|
+
});
|
|
325
385
|
const cells = new Map();
|
|
326
386
|
for (const u of units) {
|
|
327
387
|
const c = cells.get(u.cell) ?? cells.set(u.cell, { excluded: 0, gens: new Map() }).get(u.cell);
|
|
@@ -332,11 +392,11 @@ export function tally(units, order = generations(units)) {
|
|
|
332
392
|
const g = u.generation ?? null;
|
|
333
393
|
const f = c.gens.get(g) ?? c.gens.set(g, figures(g)).get(g);
|
|
334
394
|
if (u.status === "counted") {
|
|
335
|
-
f.n++;
|
|
336
395
|
if (u.merged) f.merged++;
|
|
337
396
|
f.claims += u.claims;
|
|
338
397
|
f.failed += u.falseClaims;
|
|
339
398
|
f.spend += u.spend;
|
|
399
|
+
f.pulls.push(u);
|
|
340
400
|
} else f[u.status.replace("-", "_")]++;
|
|
341
401
|
}
|
|
342
402
|
return [...cells].sort(([a], [b]) => cmp(a, b)).map(([cell, c]) => {
|
package/scripts/report-files.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// (flat: no per-run directory), `<member>` being its ledger name (`impl-412`,
|
|
7
7
|
// `impl-412-b`, `fix-pr-88`, `finisher-pr-88`, `merge-bot-3`). The file is
|
|
8
8
|
// checked against the schema of the member's family, report-schema.mjs's
|
|
9
|
-
// SCHEMAS, and the
|
|
9
|
+
// SCHEMAS, and the rules reportViolation() adds to them. The review runner and
|
|
10
10
|
// every member outside those four families have no schema, so no file to
|
|
11
11
|
// look up.
|
|
12
12
|
//
|
|
@@ -18,9 +18,14 @@
|
|
|
18
18
|
// 0 print: the schema is on stdout. validate: `valid` on stdout
|
|
19
19
|
// 1 validate: `invalid: <violation>` on stdout, naming the FIRST violation;
|
|
20
20
|
// a file that is not JSON is invalid too, as `$: not JSON (...)`, and so
|
|
21
|
-
// is one that repeats a key, as `$.merged: duplicate key "merged"`,
|
|
22
|
-
//
|
|
23
|
-
// `$.refused[0].pr: PR #<pr> already has a verdict at $.merged[0].pr
|
|
21
|
+
// is one that repeats a key, as `$.merged: duplicate key "merged"`, a
|
|
22
|
+
// merge-bot report giving one PR two verdicts, as
|
|
23
|
+
// `$.refused[0].pr: PR #<pr> already has a verdict at $.merged[0].pr`,
|
|
24
|
+
// and an impl report whose `blocked` outcome names a `bailCause`, as
|
|
25
|
+
// `$.bailCause: "<cause>" is not null, with outcome "blocked" — a block
|
|
26
|
+
// is no bail`, gives no reason in `bailDetail`, or names a `pr` or a
|
|
27
|
+
// `head`, as `$.pr: <pr> is not null, with outcome "blocked" — a block
|
|
28
|
+
// reports no PR`
|
|
24
29
|
// 2 usage: no or unknown subcommand, a wrong argument count, an unknown
|
|
25
30
|
// family, any flag; or <file> could not be read; or print could not
|
|
26
31
|
// write the schema to stdout. Refused on stderr, nothing on stdout
|
|
@@ -112,7 +117,9 @@ const issues = listOf({ issue: id, subject: str });
|
|
|
112
117
|
export const SCHEMAS = {
|
|
113
118
|
impl: report({
|
|
114
119
|
member: str,
|
|
115
|
-
outcome
|
|
120
|
+
// `bailed` is the pre-code outcome; `blocked` a block or ambiguity found
|
|
121
|
+
// after implementation began, its reason in `bailDetail`.
|
|
122
|
+
outcome: { type: "string", enum: ["pr", "bailed", "blocked"] },
|
|
116
123
|
pr: { ...id, type: ["integer", "null"] },
|
|
117
124
|
head: { type: ["string", "null"], pattern: HEX },
|
|
118
125
|
branch: str,
|
|
@@ -174,14 +181,25 @@ export function mergeBotVerdicts(report) {
|
|
|
174
181
|
];
|
|
175
182
|
}
|
|
176
183
|
|
|
177
|
-
// The first violation of `family`'s schema by `value`, or null;
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
184
|
+
// The first violation of `family`'s schema by `value`, or null; then the rules
|
|
185
|
+
// no schema keyword states. For an impl report: a `blocked` outcome names no
|
|
186
|
+
// `bailCause`, since a block is no bail, gives its reason in `bailDetail`,
|
|
187
|
+
// text that is not only whitespace, and names no `pr` or `head`, since a block
|
|
188
|
+
// reports no PR — a PR it opened is outcome `pr`, which settle checks. For a
|
|
189
|
+
// merge-bot report: each PR gets one verdict — merged, held, conflict-held or
|
|
190
|
+
// refused — the last it reached in the pass, so a PR named twice, in one list
|
|
191
|
+
// or in two, is a violation at its second entry.
|
|
182
192
|
export function reportViolation(family, value) {
|
|
183
193
|
const found = validate(SCHEMAS[family], value);
|
|
184
|
-
if (found !== null
|
|
194
|
+
if (found !== null) return found;
|
|
195
|
+
if (family === "impl" && value.outcome === "blocked") {
|
|
196
|
+
if (value.bailCause !== null) return `$.bailCause: ${show(value.bailCause)} is not null, with outcome "blocked" — a block is no bail`;
|
|
197
|
+
if (!/\S/.test(value.bailDetail ?? "")) return `$.bailDetail: ${show(value.bailDetail)} is not a reason, with outcome "blocked"`;
|
|
198
|
+
for (const field of ["pr", "head"]) {
|
|
199
|
+
if (value[field] !== null) return `$.${field}: ${show(value[field])} is not null, with outcome "blocked" — a block reports no PR`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (family !== "merge-bot") return null;
|
|
185
203
|
const first = new Map();
|
|
186
204
|
for (const { pr, path } of mergeBotVerdicts(value)) {
|
|
187
205
|
if (first.has(pr)) return `${path}: PR #${pr} already has a verdict at ${first.get(pr)}`;
|
|
@@ -20,8 +20,10 @@
|
|
|
20
20
|
// --guard <cost-guard.json> --commit <rev> [--due]
|
|
21
21
|
// Re-fits the table from the three TSVs as committed at `--commit` (read
|
|
22
22
|
// with `git show`, never from the working tree) and the guard, and
|
|
23
|
-
// rewrites `--table`, recording the commit's sha as `fitted_commit
|
|
24
|
-
//
|
|
23
|
+
// rewrites `--table`, recording the commit's sha as `fitted_commit`, then
|
|
24
|
+
// prints the fit and one line per row: its cell and the reason, then each
|
|
25
|
+
// candidate's verdict and range. The TSV flags name paths inside the
|
|
26
|
+
// repository that holds them. With
|
|
25
27
|
// `--due`, writes nothing and prints `DUE=yes|no MERGED=<n>/<N>`: whether
|
|
26
28
|
// N merged PRs have landed since `fitted_commit`, counting the features
|
|
27
29
|
// rows at `--commit` that the fitted commit's features file does not hold.
|
|
@@ -47,7 +49,9 @@ import { makeDie, defineFlags } from "./arg.mjs";
|
|
|
47
49
|
import { CELL, POLICY_CELL, drawCell, parseMember } from "./ledger-grammar.mjs";
|
|
48
50
|
import { sessionFromReceipt } from "./phase0-receipt.mjs";
|
|
49
51
|
import { parseTsv as parseMemberTsv } from "./member-outcomes.mjs";
|
|
50
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
AttributionError, FEATURE_COLUMNS, TRIP_POINTS, attribute, bootUnit, bootstrapDiffCI, generations, parseFeatures, tally, underFloor,
|
|
54
|
+
} from "./pull-attribution.mjs";
|
|
51
55
|
import { RulingError, parseTierOutcomes } from "./tier-outcomes.mjs";
|
|
52
56
|
|
|
53
57
|
const NAME = "ticket-router";
|
|
@@ -61,7 +65,7 @@ const HEAVY_BRIEF_CHARS = 3483;
|
|
|
61
65
|
const HEAVY_CRITERIA = 5;
|
|
62
66
|
// Exploration cadence after burn-in: the Pull that creates `impl-` row 5k.
|
|
63
67
|
const EXPLORE_EVERY = 5;
|
|
64
|
-
// The fit's adoption floor (
|
|
68
|
+
// The fit's adoption floor (merged PRs per side), margin, and cadence.
|
|
65
69
|
const MIN_N = 20;
|
|
66
70
|
const ADOPT_RATIO = 0.75;
|
|
67
71
|
export const FIT_EVERY_MERGED = 50;
|
|
@@ -346,15 +350,19 @@ function fitPulls({ features, members, verdicts, window }) {
|
|
|
346
350
|
// Pulls, `merged`, $ per merged PR, false claims over claims, fix rounds per
|
|
347
351
|
// merged PR, and its left-out and unpriced Pull counts. Beside them the cell's
|
|
348
352
|
// excluded Pull count, `generation` naming the newest generation's model, and
|
|
349
|
-
// `older`, each older generation's model to its figures, newest first.
|
|
353
|
+
// `older`, each older generation's model to its figures, newest first. Beside
|
|
354
|
+
// `estimates`, `pulls`: per stratum and `*`, per cell, the newest generation's
|
|
355
|
+
// counted Pulls those figures are over.
|
|
350
356
|
function estimate(units, order) {
|
|
351
|
-
const
|
|
357
|
+
const estimates = {};
|
|
358
|
+
const pulls = {};
|
|
352
359
|
for (const s of units.length ? [...new Set([...units.map((u) => u.stratum), "*"])].sort() : []) {
|
|
353
360
|
const mine = s === "*" ? units : units.filter((u) => u.stratum === s);
|
|
354
|
-
|
|
361
|
+
estimates[s] = {};
|
|
362
|
+
pulls[s] = {};
|
|
355
363
|
for (const t of tally(mine, order)) {
|
|
356
364
|
const figures = (g) => {
|
|
357
|
-
const fix =
|
|
365
|
+
const fix = g.pulls.filter((u) => u.merged)
|
|
358
366
|
.reduce((n, u) => n + u.rows.filter((r) => parseMember(r.member)?.family === "fix-pr").length, 0);
|
|
359
367
|
return {
|
|
360
368
|
n: g.n,
|
|
@@ -369,52 +377,123 @@ function estimate(units, order) {
|
|
|
369
377
|
unpriced: g.unpriced,
|
|
370
378
|
};
|
|
371
379
|
};
|
|
372
|
-
|
|
380
|
+
estimates[s][t.cell] = {
|
|
373
381
|
generation: t.generation,
|
|
374
382
|
...figures(t),
|
|
375
383
|
excluded: t.excluded,
|
|
376
384
|
older: Object.fromEntries(t.older.map((o) => [o.generation, figures(o)])),
|
|
377
385
|
};
|
|
386
|
+
pulls[s][t.cell] = t.pulls;
|
|
378
387
|
}
|
|
379
388
|
}
|
|
380
|
-
return
|
|
389
|
+
return { estimates, pulls };
|
|
381
390
|
}
|
|
382
391
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
392
|
+
/**
|
|
393
|
+
* One row's cell — the `*` row's or a stratum's — and why. The quality floor
|
|
394
|
+
* is held twice: pooled by the guard, whose trip takes a cell out of every row
|
|
395
|
+
* and out of the exploration draw, and per row by the fit, which refuses a
|
|
396
|
+
* cell under the floor that row alone. Every figure read is the cell's newest
|
|
397
|
+
* generation's, the default cell's included. `est` and `pulls` are the row's
|
|
398
|
+
* `estimate()` figures and counted Pulls.
|
|
399
|
+
*
|
|
400
|
+
* A candidate is a cell of `cells` other than the default cell. Its `verdict`,
|
|
401
|
+
* the first that applies: `tripped` by the guard; `too-few`, under MIN_N
|
|
402
|
+
* merged PRs in the row; `under-floor` against the default cell there;
|
|
403
|
+
* `too-dear`, above ADOPT_RATIO of the default cell's $ per merged PR;
|
|
404
|
+
* `not-reliably-cheaper`, its `range` does not lie wholly below zero (its
|
|
405
|
+
* upper end, as `bootstrapDiffCI` returns it, is not below zero) or there is
|
|
406
|
+
* none; else it `qualifies`. A candidate past `too-few` records whether it is
|
|
407
|
+
* `under_floor`, the floor's `bar` (the default cell's fail rate plus
|
|
408
|
+
* TRIP_POINTS, rounded as a fail rate is, for reading only: the floor compares
|
|
409
|
+
* counts) and its `range`, the bootstrap 95% range of (candidate − default
|
|
410
|
+
* cell) $ per merged PR over those Pulls; the others record null for each.
|
|
411
|
+
*
|
|
412
|
+
* Several qualifiers: each pair records the range of (the cheaper by $ per
|
|
413
|
+
* merged PR, else by name − the other) in `pairs`, and a qualifier another
|
|
414
|
+
* is reliably cheaper than is dropped. Among the rest the lower fail rate,
|
|
415
|
+
* compared in counts, wins, then the lower $ per merged PR, then the name.
|
|
416
|
+
*
|
|
417
|
+
* `reason`, why the row holds `cell`: `unjudged` (the default cell, or every
|
|
418
|
+
* untripped candidate, is under MIN_N merged PRs; `cell` is null and a
|
|
419
|
+
* stratum follows `*`), `no-qualifier` (the default cell), or, for the
|
|
420
|
+
* winner, `sole-qualifier`, `reliably-cheaper` (every other qualifier was
|
|
421
|
+
* dropped), or what put it ahead of the next of the rest: `lower-fail-rate`,
|
|
422
|
+
* `lower-usd` or `name`.
|
|
423
|
+
*/
|
|
424
|
+
function judgeRow(est = {}, pulls, cells, tripped) {
|
|
425
|
+
const record = { cell: null, reason: "unjudged", cells: {}, pairs: [] };
|
|
426
|
+
const base = est[POLICY_CELL];
|
|
427
|
+
if (!base || base.merged < MIN_N) return record;
|
|
428
|
+
// JSON writes −0 as 0, so a recorded end is 0 either way; `--check` compares the two.
|
|
429
|
+
const range = (a, b) => bootstrapDiffCI(pulls[a].map(bootUnit), pulls[b].map(bootUnit))?.map((x) => x + 0) ?? null;
|
|
430
|
+
const reliablyBelow = (r) => r !== null && r[1] < 0;
|
|
431
|
+
const bar = round4(base.failed / base.claims + TRIP_POINTS / 100);
|
|
432
|
+
const qualifiers = [];
|
|
433
|
+
let judged = false;
|
|
434
|
+
for (const c of cells) {
|
|
435
|
+
if (c === POLICY_CELL) continue;
|
|
436
|
+
const e = est[c];
|
|
437
|
+
if (tripped.has(c) || !e || e.merged < MIN_N) {
|
|
438
|
+
record.cells[c] = { verdict: tripped.has(c) ? "tripped" : "too-few", under_floor: null, bar: null, range: null };
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
judged = true;
|
|
442
|
+
const under = underFloor(e, base);
|
|
443
|
+
const r = range(c, POLICY_CELL);
|
|
444
|
+
const verdict = under ? "under-floor"
|
|
445
|
+
: e.usd_per_merged > ADOPT_RATIO * base.usd_per_merged ? "too-dear"
|
|
446
|
+
: reliablyBelow(r) ? "qualifies" : "not-reliably-cheaper";
|
|
447
|
+
record.cells[c] = { verdict, under_floor: under, bar, range: r };
|
|
448
|
+
if (verdict === "qualifies") qualifiers.push(c);
|
|
449
|
+
}
|
|
450
|
+
if (!judged) return record;
|
|
451
|
+
if (qualifiers.length === 0) return { ...record, cell: POLICY_CELL, reason: "no-qualifier" };
|
|
452
|
+
|
|
453
|
+
const usd = (c) => est[c].usd_per_merged;
|
|
454
|
+
const byName = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
455
|
+
const failCmp = (a, b) => est[a].failed * est[b].claims - est[b].failed * est[a].claims;
|
|
456
|
+
const named = [...qualifiers].sort(byName);
|
|
457
|
+
const dropped = new Set();
|
|
458
|
+
for (let i = 0; i < named.length; i++) {
|
|
459
|
+
for (let j = i + 1; j < named.length; j++) {
|
|
460
|
+
const [cheaper, other] = usd(named[j]) < usd(named[i]) ? [named[j], named[i]] : [named[i], named[j]];
|
|
461
|
+
const r = range(cheaper, other);
|
|
462
|
+
record.pairs.push({ cheaper, other, range: r });
|
|
463
|
+
if (reliablyBelow(r)) dropped.add(other);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// The cheapest qualifier, by $ then name, is never dropped, so the rest is never empty.
|
|
467
|
+
const [win, next] = named.filter((c) => !dropped.has(c)).sort((a, b) => failCmp(a, b) || usd(a) - usd(b) || byName(a, b));
|
|
468
|
+
const reason = qualifiers.length === 1 ? "sole-qualifier"
|
|
469
|
+
: next === undefined ? "reliably-cheaper"
|
|
470
|
+
: failCmp(win, next) !== 0 ? "lower-fail-rate"
|
|
471
|
+
: usd(win) !== usd(next) ? "lower-usd" : "name";
|
|
472
|
+
return { ...record, cell: win, reason };
|
|
399
473
|
}
|
|
400
474
|
|
|
401
475
|
/**
|
|
402
476
|
* The re-fit table. `guard` is `{ tripped, n, window_start }` (readGuard's
|
|
403
477
|
* shape); `commit` is the sha of the commit the inputs were read from, or null
|
|
404
|
-
* for the empty corpus before any fit.
|
|
478
|
+
* for the empty corpus before any fit. `reasons` holds `judgeRow()`'s record
|
|
479
|
+
* for `*` and for each stratum with Pulls; a stratum whose record has no cell
|
|
480
|
+
* gets no row and follows `*`, and an unjudged `*` is the default cell.
|
|
405
481
|
*/
|
|
406
482
|
export function fitTable({ prior, features, members, verdicts, guard, commit = null }) {
|
|
407
483
|
const window = guard.window_start ?? prior.window_start ?? null;
|
|
408
484
|
const { units, order } = fitPulls({ features, members, verdicts, window });
|
|
409
|
-
const estimates = estimate(units, order);
|
|
485
|
+
const { estimates, pulls } = estimate(units, order);
|
|
410
486
|
const tripped = new Set(guard.tripped);
|
|
411
487
|
|
|
412
488
|
let { stage, cells } = prior;
|
|
413
489
|
cells = [...cells];
|
|
414
|
-
const
|
|
490
|
+
const star = judgeRow(estimates["*"], pulls["*"], cells, tripped);
|
|
491
|
+
const rows = { "*": star.cell ?? POLICY_CELL };
|
|
492
|
+
const reasons = { "*": { ...star, cell: rows["*"] } };
|
|
415
493
|
for (const s of STRATA) {
|
|
416
|
-
|
|
417
|
-
|
|
494
|
+
if (!estimates[s]) continue;
|
|
495
|
+
reasons[s] = judgeRow(estimates[s], pulls[s], cells, tripped);
|
|
496
|
+
if (reasons[s].cell !== null) rows[s] = reasons[s].cell;
|
|
418
497
|
}
|
|
419
498
|
const atN = (c) => (guard.n[c] ?? 0) >= MIN_N;
|
|
420
499
|
if (stage === 1 && cells.every(atN) && (rows["*"] !== POLICY_CELL || cells.some((c) => tripped.has(c)))) {
|
|
@@ -433,6 +512,7 @@ export function fitTable({ prior, features, members, verdicts, guard, commit = n
|
|
|
433
512
|
rows,
|
|
434
513
|
classifier: prior.classifier,
|
|
435
514
|
estimates,
|
|
515
|
+
reasons,
|
|
436
516
|
guard: { tripped: [...tripped].sort(), n: Object.fromEntries(Object.keys(guard.n).sort().map((c) => [c, guard.n[c]])) },
|
|
437
517
|
};
|
|
438
518
|
}
|
|
@@ -462,6 +542,16 @@ function cellCounts(estimates) {
|
|
|
462
542
|
.join(", ") || "none";
|
|
463
543
|
}
|
|
464
544
|
|
|
545
|
+
// One line per row the fit judged or skipped: its cell (`-` for a stratum
|
|
546
|
+
// that follows `*`) and its reason, then each candidate's verdict and range.
|
|
547
|
+
function rowLines(reasons) {
|
|
548
|
+
const range = (r) => (r === null ? "" : ` [${r.map((x) => x.toFixed(2)).join(", ")}]`);
|
|
549
|
+
return Object.entries(reasons).map(([s, r]) => {
|
|
550
|
+
const cells = Object.entries(r.cells).map(([c, x]) => `${c} ${x.verdict}${range(x.range)}`).join(", ");
|
|
551
|
+
return `${NAME}: row ${s} ${r.cell ?? "-"} ${r.reason}${cells ? ` (${cells})` : ""}\n`;
|
|
552
|
+
}).join("");
|
|
553
|
+
}
|
|
554
|
+
|
|
465
555
|
// ---------------------------------------------------------------------------
|
|
466
556
|
// the commit a fit reads
|
|
467
557
|
// ---------------------------------------------------------------------------
|
|
@@ -638,7 +728,7 @@ function main() {
|
|
|
638
728
|
const next = joined(() => fitTable({ prior: table, features, members, verdicts, guard, commit: sha }));
|
|
639
729
|
writeFileSync(`${tablePath}.tmp`, `${JSON.stringify(next, null, 2)}\n`);
|
|
640
730
|
renameSync(`${tablePath}.tmp`, tablePath);
|
|
641
|
-
process.stdout.write(`${NAME}: fitted ${next.n_rows} Pulls at ${short(sha)}; rows ${JSON.stringify(next.rows)}; burn_in ${next.burn_in}; stage ${next.stage}; cells ${cellCounts(next.estimates)}\n`);
|
|
731
|
+
process.stdout.write(`${NAME}: fitted ${next.n_rows} Pulls at ${short(sha)}; rows ${JSON.stringify(next.rows)}; burn_in ${next.burn_in}; stage ${next.stage}; cells ${cellCounts(next.estimates)}\n${rowLines(next.reasons)}`);
|
|
642
732
|
}
|
|
643
733
|
|
|
644
734
|
if (isCLI(import.meta.url)) main();
|
package/skills/run-team/SKILL.md
CHANGED
|
@@ -792,7 +792,7 @@ Match the failure to its row in the table below and run that response; where the
|
|
|
792
792
|
|---|---|
|
|
793
793
|
| SHA not on expected branch | Flag, do not enqueue, report |
|
|
794
794
|
| Implementer bails before implementing | Act by the report's `bailCause` as the **Relabel causes** table says, then refill (**What a wake owes beyond its call**) |
|
|
795
|
-
| Implementer blocked or ambiguous *mid-implementation* | Free the slot, `ledger.mjs settle impl-<N> blocked`,
|
|
795
|
+
| Implementer blocked or ambiguous *mid-implementation* | Free the slot. A `blocked` report settles on the **Implementer report** wake, no label moved; with no report, `ledger.mjs settle impl-<N> blocked`, its Ending still `hollow`. Leave `in-progress`, report — the ticket is not In flight and waits for a human; the row above is the pre-code bail |
|
|
796
796
|
| `review-pr` workflow throws or returns no tree | Retry once under the next letter (`review-pr-<pr#>-b`); still failing, report and leave the PR at review-due. Never enqueue the PR as reviewed — the workflow is not a member, so no other row here covers it |
|
|
797
797
|
| Reviewer, fix-applier or red-check fixer cannot reach green | Report, leave the PR unlabeled, free the slot — a fix-applier or red-check fixer settles `failed` |
|
|
798
798
|
| Merge bot hits the hold rule | Report `held-behind-#<lower>`, PR stays queued |
|
|
@@ -841,9 +841,7 @@ enqueue, do not return the ticket to the shortlist until the maintainer rules.
|
|
|
841
841
|
escalate if it repeats, and never read it as `not reachable` or record it as a
|
|
842
842
|
stray commit. See references/failure-handling.md.
|
|
843
843
|
|
|
844
|
-
**Never `--delete-branch` — the merge bot deletes the head branch itself.**
|
|
845
|
-
`gh pr merge <n> --merge` alone: the flag's local cleanup errors on a `main`
|
|
846
|
-
held by another worktree, or strands the feature worktree on `main`.
|
|
844
|
+
**Never `--delete-branch` — the merge bot deletes the head branch itself.** See references/failure-handling.md.
|
|
847
845
|
|
|
848
846
|
**Never force a rebase to start.** No `git clean`, `git checkout .`,
|
|
849
847
|
`git reset --hard`, `git stash`. Non-empty `git status --porcelain -unormal` →
|
|
@@ -66,6 +66,13 @@ escalate if it repeats. Never read exit 2 as `not reachable`, and never record i
|
|
|
66
66
|
as a stray commit — the flag and the maintainer's ruling above are what exit 1
|
|
67
67
|
earns, and spending them on a probe that never answered costs a member its
|
|
68
68
|
ticket over a failure a re-run would have cleared.
|
|
69
|
+
|
|
70
|
+
## Never `--delete-branch`
|
|
71
|
+
|
|
72
|
+
**Never `--delete-branch` — the merge bot deletes the head branch itself.**
|
|
73
|
+
`gh pr merge <n> --merge` alone: the flag's local cleanup errors on a `main`
|
|
74
|
+
held by another worktree, or strands the feature worktree on `main`.
|
|
75
|
+
|
|
69
76
|
## Never force a rebase to start
|
|
70
77
|
|
|
71
78
|
**Never force a rebase to start.** No `git clean`, `git checkout .`,
|