@coreplane/switchboard 1.255.0 → 1.257.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/dist/assets/config/config.example.yaml +14 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +340 -7
- package/dist/assets/deploy/cloudflare-resident/drain.ts +100 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +227 -34
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +5 -3
- package/dist/assets/project.json +17 -9
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +55 -1
- package/dist/assets/src/core/authz/grants.ts +12 -0
- package/dist/assets/src/core/authz/policy.ts +15 -0
- package/dist/assets/src/core/budgets.ts +51 -17
- package/dist/assets/src/core/coordinator/contract.ts +45 -5
- package/dist/assets/src/core/coordinator/driver.ts +17 -4
- package/dist/assets/src/core/delivery.ts +8 -5
- package/dist/assets/src/core/pipelineStanding.ts +57 -0
- package/dist/assets/src/core/plane/decide.ts +468 -12
- package/dist/assets/src/core/plane/findings.ts +120 -0
- package/dist/assets/src/core/reviewVerdict.ts +49 -0
- package/dist/assets/src/core/runEvents.ts +41 -14
- package/dist/assets/src/core/runFriction.ts +16 -0
- package/dist/assets/src/core/runLedger/types.ts +16 -3
- package/dist/assets/src/core/runRecord.ts +12 -0
- package/dist/assets/src/core/ship/contract.ts +34 -32
- package/dist/assets/src/core/ship/coordinator.ts +291 -95
- package/dist/assets/src/core/ship/renewal.ts +19 -17
- package/dist/assets/src/core/trace/attrs.ts +1 -1
- package/dist/assets/src/core/untrusted.ts +35 -0
- package/dist/assets/src/execution/residentCredentials.ts +7 -4
- package/dist/assets/web/dist/.vite/manifest.json +82 -57
- package/dist/assets/web/dist/assets/HomePage-DJqJQzrL.js +1 -0
- package/dist/assets/web/dist/assets/{PendingTurnRow-DGINv9XT.js → PendingTurnRow-fhKqKUVv.js} +1 -1
- package/dist/assets/web/dist/assets/PlanePage-Bood0hNO.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-D_RD6wLo.js → ResidentDetailPage-DOe1HtUC.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-BLkSuCxo.js → ResidentsIndexPage-TyCGjXM4.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-C_4t-sit.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-BNepPMRX.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-jZnMw1mM.js +1 -0
- package/dist/assets/web/dist/assets/{ScheduledPage-3aYsDf-q.js → ScheduledPage-BkNoudDP.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-DG-p5Xy1.js → SettingsPage-COvWGXl-.js} +1 -1
- package/dist/assets/web/dist/assets/SilentTurn-DNAANKgN.js +2 -0
- package/dist/assets/web/dist/assets/SlackMark-CtiHrNjr.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-CEnGlyAL.js → StatusDot-BgmkSV0S.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-CiunVowT.js → Tooltip-DEUPuCPW.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DNhdzxvY.js +1 -0
- package/dist/assets/web/dist/assets/{dist-luhv3YSo.js → dist-CF3jz9LM.js} +1 -1
- package/dist/assets/web/dist/assets/durationTone-CVpX_yIk.js +1 -0
- package/dist/assets/web/dist/assets/main-7SAujY_s.js +28 -0
- package/dist/assets/web/dist/assets/main-DGC6WFqS.css +1 -0
- package/dist/assets/web/dist/assets/{sseReplay-DE6wv1Ua.js → sseReplay-BRJEIh83.js} +1 -1
- package/dist/cli.js +2941 -1116
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/HomePage-Be7jLLnU.js +0 -3
- package/dist/assets/web/dist/assets/PlanePage-JEj-lqgz.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CfuZqf_O.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-CmWYGR36.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-DW-HHuZa.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-iOYnmTcQ.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +0 -1
- package/dist/assets/web/dist/assets/main-C4QfwybO.css +0 -1
- package/dist/assets/web/dist/assets/main-mAKx_zo9.js +0 -28
|
@@ -63,6 +63,12 @@ export const POLICY: readonly Rule[] = [
|
|
|
63
63
|
// NAMED run (`authorizeSteerOwner`, authorization item 16a) — the requester,
|
|
64
64
|
// a linked id, or `runs:write` — which no command-door row can see.
|
|
65
65
|
{ action: "steer:write", resource: "command", when: [grant("steer:write")] },
|
|
66
|
+
// The plane's checkpoint steer (record 0064): the system actor `plane`
|
|
67
|
+
// (`PLANE_ACTOR_ID`, a `service` actor holding `PLANE_GRANTS`' one standing
|
|
68
|
+
// `steer:write`) writes the steer into a write-preset run's inbox on the
|
|
69
|
+
// ledger object itself, sender `plane`; this row records that authority in
|
|
70
|
+
// the one table even though the object, not the bot, executes the write.
|
|
71
|
+
{ action: "steer:write", resource: "run", actorKinds: ["service"], when: [grant("steer:write")] },
|
|
66
72
|
|
|
67
73
|
// ── conversations ────────────────────────────────────────────────────────
|
|
68
74
|
// Who may point the bot at another thread (record 0037). Asked for a
|
|
@@ -108,6 +114,11 @@ export const POLICY: readonly Rule[] = [
|
|
|
108
114
|
// read is never a chat baseline, like the costs reads.
|
|
109
115
|
{ action: "providers:read", resource: "command", when: [grant("providers:read")] },
|
|
110
116
|
|
|
117
|
+
// ── pull requests ──────────────────────────────────────────────────
|
|
118
|
+
// `pulls rebase` force-pushes the pipeline's branches and can start a paid
|
|
119
|
+
// fix round (record 0071): a grant, never a chat baseline.
|
|
120
|
+
{ action: "pulls:write", resource: "command", when: [grant("pulls:write")] },
|
|
121
|
+
|
|
111
122
|
// ── repos ────────────────────────────────────────────────────────────────
|
|
112
123
|
{ action: "repo:read", resource: "command", when: [grant("repo:read")] },
|
|
113
124
|
{ action: "repo:write", resource: "repo", when: [grant("repo:write")] },
|
|
@@ -150,6 +161,10 @@ export const POLICY: readonly Rule[] = [
|
|
|
150
161
|
// channel-config right — whoever may set the channel may set a thread in it;
|
|
151
162
|
// never a baseline, so membership alone admits nobody.
|
|
152
163
|
{ action: "config:write", resource: "config-scope", resourceKind: "thread", when: [grant("config:write")] },
|
|
164
|
+
// The ORG scope (`config set org --pulls.…`, record 0071) — and a repository
|
|
165
|
+
// scope, the org setting's per-repository slice — affects every requester:
|
|
166
|
+
// the same `config:write` grant, never a baseline.
|
|
167
|
+
{ action: "config:write", resource: "config-scope", resourceKind: "org", when: [grant("config:write")] },
|
|
153
168
|
// A user edits only their own scope.
|
|
154
169
|
{ action: "config:write", resource: "config-scope", resourceKind: "user", when: [IS_SELF] },
|
|
155
170
|
// The author binding's one trusted write (record 0062, authorization.md item
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
// Workflow-driven coordinator and the deploy Workers can bundle it — the
|
|
17
17
|
// dependency runs registry → budgets, never the reverse.
|
|
18
18
|
|
|
19
|
+
export const SECOND_MS = 1_000;
|
|
20
|
+
|
|
21
|
+
export const secondsToMs = (seconds: number): number => seconds * SECOND_MS;
|
|
22
|
+
|
|
19
23
|
export const MINUTE_MS = 60_000;
|
|
20
24
|
|
|
21
25
|
/** Minutes → milliseconds, for a duration a request names in minutes (a drain's
|
|
@@ -109,6 +113,14 @@ export const DRAIN = {
|
|
|
109
113
|
marginMinutes: 5,
|
|
110
114
|
maxMinutes: 90,
|
|
111
115
|
defaultMinutes: 60,
|
|
116
|
+
/** How long a held drain waits for a container's post-deploy cycle before
|
|
117
|
+
* reopening the fleet anyway with the stale container named (issue 2044):
|
|
118
|
+
* the platform replaces container processes three to ten minutes after a
|
|
119
|
+
* deploy (docs/reference/specs/resident-repos.md item 65), so a cycle that
|
|
120
|
+
* has not landed within this bound is not coming on its own — the gate
|
|
121
|
+
* asked for the cycle, so it owns the outcome and never holds the fleet
|
|
122
|
+
* closed to the drain's whole `until` on a report nothing will send. */
|
|
123
|
+
cycleBoundMinutes: 10,
|
|
112
124
|
} as const;
|
|
113
125
|
|
|
114
126
|
/** How long an intake receipt row is kept on the run history object
|
|
@@ -143,7 +155,7 @@ export type Preset = LoopPreset | "ship";
|
|
|
143
155
|
* that pushes at minute 29 and then runs the repo's whole gate (seven to ten
|
|
144
156
|
* minutes on a resident) was cut at 45 one run in twelve, so the ask is
|
|
145
157
|
* double the 90th percentile (31.5) with the gate inside it. Ship's holds
|
|
146
|
-
* its own loop at the default rounds with a
|
|
158
|
+
* its own loop at the default rounds with a findings round at that ask, not at
|
|
147
159
|
* its floor (`fit` proves the floor case; the ask leaves room above it). */
|
|
148
160
|
export const ASKS: Readonly<Record<Preset, number>> = {
|
|
149
161
|
general: 5,
|
|
@@ -155,17 +167,18 @@ export const ASKS: Readonly<Record<Preset, number>> = {
|
|
|
155
167
|
conductor: 120,
|
|
156
168
|
};
|
|
157
169
|
|
|
158
|
-
/** The rounds a ship loop is made of. `
|
|
170
|
+
/** The rounds a ship loop is made of. `findings` is a coding child handed the
|
|
159
171
|
* review's findings; `merge` is the runner's wait on the guards. */
|
|
160
|
-
export type RoundKind = "coding" | "review" | "
|
|
172
|
+
export type RoundKind = "coding" | "review" | "findings" | "merge";
|
|
161
173
|
|
|
162
174
|
/** The least lease in which a round does useful work, in minutes. A carve that
|
|
163
175
|
* falls under the floor is refused rather than dispatched: a two-minute
|
|
164
|
-
* review or
|
|
165
|
-
* Review's is the ledger's 90th percentile of completed reviews (5.1
|
|
166
|
-
* 181); coding's is the least a
|
|
167
|
-
* gate alone takes seven to ten minutes on a resident, and a
|
|
168
|
-
* run it finishes nothing its contract asks; the merge
|
|
176
|
+
* review or findings round costs an attach and a model turn and finishes
|
|
177
|
+
* nothing. Review's is the ledger's 90th percentile of completed reviews (5.1
|
|
178
|
+
* min over 181); coding's is the least a findings round can run the repo's
|
|
179
|
+
* gate in — the gate alone takes seven to ten minutes on a resident, and a
|
|
180
|
+
* round that cannot run it finishes nothing its contract asks; the merge
|
|
181
|
+
* wait's is a guess
|
|
169
182
|
* until the ledger says. */
|
|
170
183
|
export const PRESET_FLOORS: Readonly<Record<LoopPreset, number>> = {
|
|
171
184
|
general: 2,
|
|
@@ -177,7 +190,7 @@ export const PRESET_FLOORS: Readonly<Record<LoopPreset, number>> = {
|
|
|
177
190
|
};
|
|
178
191
|
export const FLOORS: Readonly<Record<RoundKind, number>> = {
|
|
179
192
|
coding: PRESET_FLOORS.coding,
|
|
180
|
-
|
|
193
|
+
findings: PRESET_FLOORS.coding,
|
|
181
194
|
review: PRESET_FLOORS.review,
|
|
182
195
|
merge: 10,
|
|
183
196
|
};
|
|
@@ -186,6 +199,12 @@ export const FLOORS: Readonly<Record<RoundKind, number>> = {
|
|
|
186
199
|
* when the remainder allows it. */
|
|
187
200
|
export const MERGE_WAIT_ASK_MINUTES = 60;
|
|
188
201
|
|
|
202
|
+
/** The sweep's bounds (record 0071, mechanism two): the short lease of the one
|
|
203
|
+
* model round a conflicting pull request may buy, and the spend that round is
|
|
204
|
+
* capped at per pull request — a round refused at either bound ends the pull
|
|
205
|
+
* request's line with the conflict named, never a retry loop. */
|
|
206
|
+
export const PULL_SWEEP = { leaseMinutes: 15, spendCapUsd: 5 } as const;
|
|
207
|
+
|
|
189
208
|
/** The provider retry ladder (issue 1932): the backoff before each retry of a
|
|
190
209
|
* transient model-call failure — a gateway 5xx, a stream cut before
|
|
191
210
|
* `message_stop`, a gateway timeout. Three attempts with growing waits,
|
|
@@ -208,7 +227,15 @@ export const SHIP_WAIT = { marginMinutes: 5, chunkMinutes: 5, mergeChunkMinutes:
|
|
|
208
227
|
* it, a reservation's window, and the default re-ask cadence — how often a
|
|
209
228
|
* silent resident something waits on is probed (`plane.reaskMinutes`
|
|
210
229
|
* overrides it per deployment). */
|
|
211
|
-
export const PLANE = {
|
|
230
|
+
export const PLANE = {
|
|
231
|
+
recentMinutes: 60,
|
|
232
|
+
reservationMinutes: 2,
|
|
233
|
+
reaskMinutes: 2,
|
|
234
|
+
/** A coding round with no pushed head past this is steered once (record 0064, "The backpressure contract"). */
|
|
235
|
+
noPushMinutes: 15,
|
|
236
|
+
/** The line an in-flight call with NO declared bound is judged against for the same steer. */
|
|
237
|
+
noBoundMinutes: 20,
|
|
238
|
+
} as const;
|
|
212
239
|
|
|
213
240
|
/** The named amounts a lease holds back, in minutes. Each stands for a step
|
|
214
241
|
* every run or round pays: `provision` is attach and restore before the
|
|
@@ -328,6 +355,12 @@ export function provisionalBearerExpiresAt(now: number, leaseMinutes: number): n
|
|
|
328
355
|
return now + (ALLOWANCES.provision + leaseMinutes + ALLOWANCES.bearerGrace) * MINUTE_MS;
|
|
329
356
|
}
|
|
330
357
|
|
|
358
|
+
/** The e2b credential-write exec's own timeout: the store-file write is one
|
|
359
|
+
* `printf` plus a `git config` reset, so a minute is generous headroom while
|
|
360
|
+
* still failing fast when the micro-VM is wedged
|
|
361
|
+
* (docs/reference/specs/execution.md item 5). */
|
|
362
|
+
export const SANDBOX_CREDENTIAL_WRITE_TIMEOUT_MS = MINUTE_MS;
|
|
363
|
+
|
|
331
364
|
/** The per-command bash budget's rows: the default when a call names none, the
|
|
332
365
|
* ceiling a call may raise it to, and the floor under which a number is a
|
|
333
366
|
* typo (docs/reference/specs/execution.md item 11). */
|
|
@@ -361,19 +394,20 @@ export interface RoundPosition {
|
|
|
361
394
|
}
|
|
362
395
|
|
|
363
396
|
/** The rounds of a loop in order: the coding round, then a review and, after
|
|
364
|
-
* every review but the last, a
|
|
397
|
+
* every review but the last, a findings round, then the merge wait. */
|
|
365
398
|
export function loopRounds(loop: Loop): RoundKind[] {
|
|
366
399
|
const rounds: RoundKind[] = ["coding"];
|
|
367
400
|
for (let i = 0; i < loop.maxRounds; i++) {
|
|
368
401
|
rounds.push("review");
|
|
369
|
-
if (i < loop.maxRounds - 1) rounds.push("
|
|
402
|
+
if (i < loop.maxRounds - 1) rounds.push("findings");
|
|
370
403
|
}
|
|
371
404
|
rounds.push("merge");
|
|
372
405
|
return rounds;
|
|
373
406
|
}
|
|
374
407
|
|
|
375
408
|
/** Where a round sits in `loopRounds`: the coding round first; review round
|
|
376
|
-
* `n` (counted from 1) and the
|
|
409
|
+
* `n` (counted from 1) and the findings round that follows it at `2n − 1`
|
|
410
|
+
* and `2n`; the
|
|
377
411
|
* merge wait last. The ship coordinator numbers its rounds this way, so the
|
|
378
412
|
* reserve it carves with is the one the fit assumed. */
|
|
379
413
|
export function loopPosition(loop: Loop, kind: RoundKind, n = 0): number {
|
|
@@ -382,7 +416,7 @@ export function loopPosition(loop: Loop, kind: RoundKind, n = 0): number {
|
|
|
382
416
|
return 0;
|
|
383
417
|
case "review":
|
|
384
418
|
return 2 * n - 1;
|
|
385
|
-
case "
|
|
419
|
+
case "findings":
|
|
386
420
|
return 2 * n;
|
|
387
421
|
case "merge":
|
|
388
422
|
return 2 * loop.maxRounds;
|
|
@@ -403,7 +437,7 @@ export function carveChildOfParent(
|
|
|
403
437
|
}
|
|
404
438
|
|
|
405
439
|
/** The minutes a round holds back for what must follow it in the loop: the
|
|
406
|
-
* floor plus provisioning of every later review and
|
|
440
|
+
* floor plus provisioning of every later review and findings round, and the merge
|
|
407
441
|
* wait's floor. A merge holds nothing back; a round with no loop (a
|
|
408
442
|
* conductor's child) holds nothing back either. */
|
|
409
443
|
export function reserveMinutes(
|
|
@@ -419,12 +453,12 @@ export function reserveMinutes(
|
|
|
419
453
|
return reserve;
|
|
420
454
|
}
|
|
421
455
|
|
|
422
|
-
/** What a round asks for: the preset's ask for a coding or
|
|
456
|
+
/** What a round asks for: the preset's ask for a coding or findings round, the
|
|
423
457
|
* review's for a review, the merge wait's own for the merge. */
|
|
424
458
|
export function roundAskMinutes(kind: RoundKind): number {
|
|
425
459
|
switch (kind) {
|
|
426
460
|
case "coding":
|
|
427
|
-
case "
|
|
461
|
+
case "findings":
|
|
428
462
|
return ASKS.coding;
|
|
429
463
|
case "review":
|
|
430
464
|
return ASKS.review;
|
|
@@ -233,6 +233,26 @@ export interface ChecksSettledPayload {
|
|
|
233
233
|
settledAt: number;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/** The event the merge watch sends a waiting unit when its pull request
|
|
237
|
+
* merged, whoever merged (record 0071, mechanism three): the unit ends
|
|
238
|
+
* `merged` by other. The type names the unit within the instance, the
|
|
239
|
+
* nudge's alphabet and clip rule. */
|
|
240
|
+
export const PULL_MERGED_EVENT_PREFIX = "pr-merged-";
|
|
241
|
+
export function pullMergedEventType(key: { instanceId: string; unit: string }): string {
|
|
242
|
+
const suffix = `-${key.unit}`;
|
|
243
|
+
const room = 100 - PULL_MERGED_EVENT_PREFIX.length - suffix.length;
|
|
244
|
+
return `${PULL_MERGED_EVENT_PREFIX}${key.instanceId.slice(0, room)}${suffix}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** What the merged event carries — the pull request and the merge's facts;
|
|
248
|
+
* the waiting unit confirms through its own pr-check before it acts. */
|
|
249
|
+
export interface PullMergedPayload {
|
|
250
|
+
repo: string;
|
|
251
|
+
number: number;
|
|
252
|
+
sha: string;
|
|
253
|
+
mergedAt: string;
|
|
254
|
+
}
|
|
255
|
+
|
|
236
256
|
/** What the event carries — ids, a status and a clock; the parent confirms
|
|
237
257
|
* through `read-record` before it acts, so nothing more rides here. */
|
|
238
258
|
export interface RunFinishedPayload {
|
|
@@ -379,17 +399,17 @@ export interface UnitIdle {
|
|
|
379
399
|
wakes: number;
|
|
380
400
|
}
|
|
381
401
|
|
|
382
|
-
/** One unit of the plan an instance runs (a task string is a generated plan of
|
|
383
|
-
* one unit, `U1`): its branch, the units it waits on, and — as the runner
|
|
384
|
-
* reaches it — its thread, its pull request, the round boundaries the card
|
|
385
|
-
* drew and how it ended. One row a person can read for "what happened to this
|
|
386
|
-
* unit". */
|
|
387
402
|
/** What the severity gate caught on a round: the level in force and the gated findings as `id (severity)`. */
|
|
388
403
|
export interface RoundGate {
|
|
389
404
|
level: AddressSeverity;
|
|
390
405
|
findings: string[];
|
|
391
406
|
}
|
|
392
407
|
|
|
408
|
+
/** One unit of the plan an instance runs (a task string is a generated plan of
|
|
409
|
+
* one unit, `U1`): its branch, the units it waits on, and — as the runner
|
|
410
|
+
* reaches it — its thread, its pull request, the round boundaries the card
|
|
411
|
+
* drew and how it ended. One row a person can read for "what happened to this
|
|
412
|
+
* unit". */
|
|
393
413
|
export interface CoordinatorUnit {
|
|
394
414
|
instanceId: string;
|
|
395
415
|
/** `U<n>` as the plan spells it. */
|
|
@@ -606,6 +626,26 @@ export async function sendChecksSettled(
|
|
|
606
626
|
}
|
|
607
627
|
}
|
|
608
628
|
|
|
629
|
+
/** The one send per merged fact the watch observed (record 0071): best effort
|
|
630
|
+
* like `sendRunFinished` — a refusal is answered, never thrown; the waiting
|
|
631
|
+
* unit's bounded wait re-asks its own pr-check on its next chunk. */
|
|
632
|
+
export async function sendPullMerged(
|
|
633
|
+
workflow: WorkflowSender | undefined,
|
|
634
|
+
key: { instanceId: string; unit: string },
|
|
635
|
+
payload: PullMergedPayload,
|
|
636
|
+
): Promise<RunFinishedSend> {
|
|
637
|
+
const instance = key.instanceId;
|
|
638
|
+
if (!workflow) return { kind: "no-binding", instance };
|
|
639
|
+
const type = pullMergedEventType(key);
|
|
640
|
+
try {
|
|
641
|
+
const handle = await workflow.get(instance);
|
|
642
|
+
await handle.sendEvent({ type, payload });
|
|
643
|
+
return { kind: "sent", instance, type };
|
|
644
|
+
} catch (err) {
|
|
645
|
+
return { kind: "failed", instance, type, reason: err instanceof Error ? err.message : String(err) };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
609
649
|
/** One deploy-roll signal to the child's parent (run-history item 47a): best
|
|
610
650
|
* effort like `sendRunFinished` — a refusal is answered, never thrown; a lost
|
|
611
651
|
* send costs the wait a chunk, never the round. */
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
type UnitEnding,
|
|
62
62
|
type UnitPipelineState,
|
|
63
63
|
stepPrefixOf,
|
|
64
|
-
type
|
|
64
|
+
type LeaseSegmentProgress,
|
|
65
65
|
} from "../ship/coordinator.js";
|
|
66
66
|
import {
|
|
67
67
|
checksSettledEventType,
|
|
@@ -316,7 +316,11 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
316
316
|
// The hard stop's mark, as the bot's answer carries it (record 0060; issue
|
|
317
317
|
// 1924): a finished child's unit ends stopped on it.
|
|
318
318
|
const stopped = a.body.stopped === true ? { stopped: true as const } : {};
|
|
319
|
-
|
|
319
|
+
// The interrupted child restarted from its request (issue 1903): the bot
|
|
320
|
+
// answers the live successor's id, and the machine keeps the wait on it.
|
|
321
|
+
const restarted = typeof a.body.restartedAs === "string" ? { restartedAs: a.body.restartedAs } : {};
|
|
322
|
+
if (!run.finished)
|
|
323
|
+
return { type: "read-record", step, run: { finished: false }, ...stopped, ...restarted, at: a.body.at };
|
|
320
324
|
if (typeof run.status !== "string") throw new UnreadableAnswer("read-record", a, "status");
|
|
321
325
|
// The typed artifacts as the bot's record carries them — shape-checked where
|
|
322
326
|
// they were written (the run record's validator), read here as they are.
|
|
@@ -337,6 +341,7 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
337
341
|
costUsd,
|
|
338
342
|
handoffLists,
|
|
339
343
|
failure,
|
|
344
|
+
interruption,
|
|
340
345
|
} = facts;
|
|
341
346
|
return {
|
|
342
347
|
type: "read-record",
|
|
@@ -363,6 +368,11 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
363
368
|
// The failure by name (run-history item 57), shape-checked: a
|
|
364
369
|
// `provider_transient` drives the round-0 re-run (agent-ship item 9).
|
|
365
370
|
...(isRecord(failure) && typeof failure.kind === "string" ? { failure: { kind: failure.kind } } : {}),
|
|
371
|
+
// What ended an interrupted child (issue 1876): the ending's sentence
|
|
372
|
+
// names the cause instead of claiming a bot restart for every one.
|
|
373
|
+
...(interruption === "bot_restart" || interruption === "container_replaced" || interruption === "sandbox_fault"
|
|
374
|
+
? { interruption }
|
|
375
|
+
: {}),
|
|
366
376
|
},
|
|
367
377
|
at: a.body.at,
|
|
368
378
|
};
|
|
@@ -441,12 +451,15 @@ function prCheckReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
441
451
|
* ask's answer carries `retried` instead: whether the re-run was dispatched,
|
|
442
452
|
* so the machine never waits on a head an undispatched re-run left unchanged. */
|
|
443
453
|
function checksReturn(step: string, a: BotAnswer): StepReturn {
|
|
444
|
-
const { ok, checks, retried, at } = a.body;
|
|
454
|
+
const { ok, checks, draft, retried, at } = a.body;
|
|
445
455
|
if (ok !== true) throw new UnreadableAnswer("checks", a, "ok");
|
|
446
456
|
return {
|
|
447
457
|
type: "checks",
|
|
448
458
|
step,
|
|
449
459
|
...(isRoundChecks(checks) ? { checks } : {}),
|
|
460
|
+
// The pull request is a draft (issue 2063): the machine's table holds the
|
|
461
|
+
// unit for the ready event instead of merging or ending without a cause.
|
|
462
|
+
...(draft === true ? { draft: true } : {}),
|
|
450
463
|
...(typeof retried === "boolean" ? { retried } : {}),
|
|
451
464
|
at,
|
|
452
465
|
};
|
|
@@ -681,7 +694,7 @@ async function runUnit(
|
|
|
681
694
|
instanceId: string,
|
|
682
695
|
node: PlanUnitNode,
|
|
683
696
|
plan: PlanFacts,
|
|
684
|
-
session?:
|
|
697
|
+
session?: LeaseSegmentProgress,
|
|
685
698
|
): Promise<UnitEnding> {
|
|
686
699
|
const unit = node.id;
|
|
687
700
|
// A renewal's segment names its steps under the segment (`U10/s2/…`), so
|
|
@@ -186,7 +186,10 @@ export interface WeekRow extends DeliveryIndicators {
|
|
|
186
186
|
prs: number[];
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
/** One row per board issue the pull requests link — the report's "Issues:"
|
|
190
|
+
* block and the Delivery page's table. The JSON field stays `units` (a served
|
|
191
|
+
* shape older readers hold); only the words are the issue's. */
|
|
192
|
+
export interface IssueRow extends DeliveryIndicators {
|
|
190
193
|
/** The board issue, or null for the pull requests no issue claims. */
|
|
191
194
|
issue: number | null;
|
|
192
195
|
title: string;
|
|
@@ -198,7 +201,7 @@ export interface DeliveryReport {
|
|
|
198
201
|
repo: string;
|
|
199
202
|
range: DeliveryRange;
|
|
200
203
|
weeks: WeekRow[];
|
|
201
|
-
units:
|
|
204
|
+
units: IssueRow[];
|
|
202
205
|
prs: PullRequestRow[];
|
|
203
206
|
totals: DeliveryIndicators;
|
|
204
207
|
identities: DeliveryIdentities;
|
|
@@ -448,7 +451,7 @@ export function buildDeliveryReport(input: DeliveryInput): DeliveryReport {
|
|
|
448
451
|
const key = p.issue?.number ?? null;
|
|
449
452
|
byIssue.set(key, [...(byIssue.get(key) ?? []), p]);
|
|
450
453
|
}
|
|
451
|
-
const units:
|
|
454
|
+
const units: IssueRow[] = [...byIssue.entries()]
|
|
452
455
|
.sort(([a], [b]) => (a === null ? 1 : b === null ? -1 : a - b))
|
|
453
456
|
.map(([issue, rows]) => {
|
|
454
457
|
const first = rows.find((r) => r.issue !== undefined)?.issue;
|
|
@@ -572,7 +575,7 @@ function coverage(report: DeliveryReport): string {
|
|
|
572
575
|
return `the newest ${report.totals.prsMerged} pull requests only${from}`;
|
|
573
576
|
}
|
|
574
577
|
|
|
575
|
-
/** One block per week, then the
|
|
578
|
+
/** One block per week, then the issues — single-spaced lines, so chat carries them as they are. */
|
|
576
579
|
export function renderDeliveryReport(report: DeliveryReport, nowMs: number = systemClock()): string {
|
|
577
580
|
const lines: string[] = [
|
|
578
581
|
[
|
|
@@ -597,7 +600,7 @@ export function renderDeliveryReport(report: DeliveryReport, nowMs: number = sys
|
|
|
597
600
|
);
|
|
598
601
|
}
|
|
599
602
|
if (report.units.length > 0) {
|
|
600
|
-
lines.push("", "
|
|
603
|
+
lines.push("", "Issues:");
|
|
601
604
|
for (const u of report.units) {
|
|
602
605
|
const prs = `${u.prs.length === 1 ? "PR" : "PRs"} ${u.prs.join(", ")}`;
|
|
603
606
|
const who = u.issue === null ? "(no issue)" : `${u.issue} ${u.title}`;
|
|
@@ -130,6 +130,63 @@ export const ENDING_STAGE = {
|
|
|
130
130
|
blocked: "ended",
|
|
131
131
|
} as const satisfies Record<UnitEnding["kind"] | "blocked", Stage | "hold">;
|
|
132
132
|
|
|
133
|
+
/** The user's words for a unit's ending (record 0066): what a card, a plan
|
|
134
|
+
* summary and the web unit page print instead of the internal token —
|
|
135
|
+
* `merge_ready` reads `merge-ready`, `round_cap` reads `round cap reached`,
|
|
136
|
+
* never the snake_case kind. Pinned to the union plus `blocked` (the driver's
|
|
137
|
+
* never-started status), so a new ending kind fails the build here. */
|
|
138
|
+
export const ENDING_WORDS = {
|
|
139
|
+
merged: "merged",
|
|
140
|
+
already_landed: "merged",
|
|
141
|
+
merge_ready: "merge-ready",
|
|
142
|
+
held: "held",
|
|
143
|
+
merge_refused: "not merged",
|
|
144
|
+
round_cap: "round cap reached",
|
|
145
|
+
wall_clock_cap: "out of budget",
|
|
146
|
+
review_pending: "review pending",
|
|
147
|
+
stopped: "stopped",
|
|
148
|
+
aborted: "aborted",
|
|
149
|
+
transient: "aborted",
|
|
150
|
+
continued: "continuing",
|
|
151
|
+
no_verdict: "no verdict",
|
|
152
|
+
interrupted: "interrupted",
|
|
153
|
+
refused: "refused",
|
|
154
|
+
idle: "idle",
|
|
155
|
+
blocked: "blocked",
|
|
156
|
+
} as const satisfies Record<UnitEnding["kind"] | "blocked", string>;
|
|
157
|
+
|
|
158
|
+
/** The user's words for a round outcome (record 0066): the card's unit line
|
|
159
|
+
* prints these beside the round header, never `checks_failed` or another
|
|
160
|
+
* internal token. Pinned to the union: a new outcome fails the build here. */
|
|
161
|
+
export const ROUND_OUTCOME_WORDS = {
|
|
162
|
+
started: "started",
|
|
163
|
+
pr_opened: "pull request opened",
|
|
164
|
+
completed: "done",
|
|
165
|
+
approve: "approved",
|
|
166
|
+
request_changes: "changes requested",
|
|
167
|
+
no_verdict: "no verdict",
|
|
168
|
+
checks_failed: "checks failed",
|
|
169
|
+
transient: "retried",
|
|
170
|
+
enqueued: "queued to merge",
|
|
171
|
+
dequeued: "removed from the merge queue",
|
|
172
|
+
aborted: "aborted",
|
|
173
|
+
stopped: "stopped",
|
|
174
|
+
continued: "continuing",
|
|
175
|
+
idle: "idle",
|
|
176
|
+
} as const satisfies Record<ShipRoundOutcome, string>;
|
|
177
|
+
|
|
178
|
+
/** The user's word for any ending or unit-status token — total over strings
|
|
179
|
+
* read off a stored row, so an unknown word prints as itself rather than
|
|
180
|
+
* throwing on an older record. */
|
|
181
|
+
export function endingWordOf(kind: string): string {
|
|
182
|
+
return (ENDING_WORDS as Record<string, string>)[kind] ?? kind;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The user's word for any round-outcome token, total like `endingWordOf`. */
|
|
186
|
+
export function roundOutcomeWordOf(outcome: string): string {
|
|
187
|
+
return (ROUND_OUTCOME_WORDS as Record<string, string>)[outcome] ?? outcome;
|
|
188
|
+
}
|
|
189
|
+
|
|
133
190
|
type ShipEvent = Extract<RunEvent, { type: "ship_round" } | { type: "ship_unit" }>;
|
|
134
191
|
|
|
135
192
|
function isShipEvent(e: RunEvent): e is ShipEvent {
|