@coreplane/switchboard 1.254.1 → 1.255.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/.dockerignore +3 -0
- package/dist/assets/Dockerfile +12 -1
- package/dist/assets/config/config.example.yaml +6 -1
- package/dist/assets/deploy/cloudflare/worker.ts +39 -23
- package/dist/assets/deploy/cloudflare-memory/worker.ts +552 -5
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +5 -0
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +13 -1
- package/dist/assets/deploy/cloudflare-resident/levels.ts +84 -0
- package/dist/assets/deploy/cloudflare-resident/prepare-commit-msg +17 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +202 -31
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -1
- package/dist/assets/deploy/cloudflare-sandbox/prepare-commit-msg +17 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +21 -4
- package/dist/assets/deploy/hooks/prepare-commit-msg +17 -0
- package/dist/assets/deploy/secrets.manifest.json +12 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +21 -0
- package/dist/assets/src/core/budgets.ts +35 -2
- package/dist/assets/src/core/coordinator/contract.ts +6 -0
- package/dist/assets/src/core/coordinator/driver.ts +49 -6
- package/dist/assets/src/core/costs.ts +39 -16
- package/dist/assets/src/core/pipelineStanding.ts +5 -0
- package/dist/assets/src/core/plane/decide.ts +389 -22
- package/dist/assets/src/core/refusal.ts +3 -0
- package/dist/assets/src/core/reviewVerdict.ts +15 -2
- package/dist/assets/src/core/runEvents.ts +31 -12
- package/dist/assets/src/core/runLedger/sessionLog.ts +128 -0
- package/dist/assets/src/core/runLedger/types.ts +3 -0
- package/dist/assets/src/core/runRecord.ts +24 -7
- package/dist/assets/src/core/ship/coordinator.ts +327 -61
- package/dist/assets/src/core/trace/workerTrace.ts +3 -0
- package/dist/assets/src/execution/sandboxErrors.ts +77 -6
- package/dist/assets/web/dist/.vite/manifest.json +59 -58
- package/dist/assets/web/dist/assets/CostsPage-BaeWnm-o.js +1 -0
- package/dist/assets/web/dist/assets/{DeliveryPage-ngPsO2to.js → DeliveryPage-BBAyLwPq.js} +1 -1
- package/dist/assets/web/dist/assets/{HomePage-DvxTHzPx.js → HomePage-Be7jLLnU.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-DGINv9XT.js +1 -0
- package/dist/assets/web/dist/assets/{PlanePage-DpWfiX4C.js → PlanePage-JEj-lqgz.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DG86v39Y.js → ResidentDetailPage-D_RD6wLo.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-x6p689VH.js → ResidentsIndexPage-BLkSuCxo.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CfuZqf_O.js +1 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-ysJBY8xQ.js → RunRoutePage-CmWYGR36.js} +4 -4
- package/dist/assets/web/dist/assets/{RunsIndexPage-CuzFchcn.js → RunsIndexPage-DW-HHuZa.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-BuLmfcbG.js → ScheduledPage-3aYsDf-q.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-BujWkdU_.js → SettingsPage-DG-p5Xy1.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-C8Bc0pTX.js → StatusDot-CEnGlyAL.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-BWwJx27K.js → Tooltip-CiunVowT.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-iOYnmTcQ.js +1 -0
- package/dist/assets/web/dist/assets/budgets-c1eumrqD.js +1 -0
- package/dist/assets/web/dist/assets/{dist-CpnyQGOb.js → dist-luhv3YSo.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-DborJPFp.js +1 -0
- package/dist/assets/web/dist/assets/{main-Comxmwi4.js → main-mAKx_zo9.js} +2 -2
- package/dist/assets/web/dist/assets/{sseReplay-IzTdD4-3.js → sseReplay-DE6wv1Ua.js} +6 -6
- package/dist/cli.js +6022 -4956
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-BuKjw3nv.js +0 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-ZYIRCCZ2.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DG29LOTs.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-B9kjA1AT.js +0 -1
- package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-DABQtONT.js +0 -1
|
@@ -404,6 +404,11 @@ export interface RoundRef {
|
|
|
404
404
|
/** Round 0 is the coding round; review round n and its findings step share n. */
|
|
405
405
|
index: number;
|
|
406
406
|
kind: RoundKind;
|
|
407
|
+
/** The round's attempt when it was re-run (agent-ship item 9, issue 1932):
|
|
408
|
+
* 2 for round 0's one re-run after a provider transient with nothing
|
|
409
|
+
* pushed. Absent on a first attempt; suffixes the round's step names so the
|
|
410
|
+
* Workflow's durable cache never hands the re-run the dead attempt's answers. */
|
|
411
|
+
attempt?: number;
|
|
407
412
|
}
|
|
408
413
|
export type ChildPreset = "coding" | "review";
|
|
409
414
|
|
|
@@ -480,7 +485,10 @@ export type CoordinatorAction =
|
|
|
480
485
|
* over a record fact written minutes earlier. */
|
|
481
486
|
pr?: number;
|
|
482
487
|
}
|
|
483
|
-
|
|
488
|
+
/** `queued`: the pull request is in the base's merge queue — the bot reads
|
|
489
|
+
* the queue's outcome (merged, still queued, or removed with the reason)
|
|
490
|
+
* instead of attempting the squash. */
|
|
491
|
+
| { type: "merge"; step: string; prNumber: number; headSha: string; queued?: true }
|
|
484
492
|
/** Read the check runs at the reviewed head (record 0055, the round verdict):
|
|
485
493
|
* the merge door's own reading, folded into the round. `retry` names the
|
|
486
494
|
* failed checks whose one flake re-run the machine is spending: the bot
|
|
@@ -524,6 +532,10 @@ export type ChildFacts =
|
|
|
524
532
|
leaseStartedAt?: number;
|
|
525
533
|
costUsd?: number | null;
|
|
526
534
|
handoffLists?: Handoff;
|
|
535
|
+
/** The failure by name off a `failed` run's record (run-history item 57):
|
|
536
|
+
* `provider_transient` marks a child a gateway 5xx, a cut stream or a
|
|
537
|
+
* gateway timeout ended past the harness's retry ladder (issue 1932). */
|
|
538
|
+
failure?: { kind: string };
|
|
527
539
|
};
|
|
528
540
|
|
|
529
541
|
/** What heads the unit's branch on GitHub: nothing, an open pull request, or —
|
|
@@ -580,14 +592,22 @@ export type StepReturn =
|
|
|
580
592
|
| { type: "spawn"; step: string; outcome: "busy"; runId?: string; at: number }
|
|
581
593
|
| { type: "spawn"; step: string; outcome: "refused"; refusal: string; message?: string; at: number }
|
|
582
594
|
| { type: "spawn"; step: string; outcome: "failed"; reason: string; at: number }
|
|
595
|
+
// The hosted parent's hard stop landed (record 0060; issue 1924): the bot
|
|
596
|
+
// refuses the spawn over the instance row's stop mark, and the unit ends
|
|
597
|
+
// `stopped` — nothing more is run.
|
|
598
|
+
| { type: "spawn"; step: string; outcome: "stopped"; at: number }
|
|
583
599
|
| { type: "wait"; step: string; outcome: "event" | "timeout" }
|
|
584
|
-
|
|
600
|
+
// `stopped` on a read: the instance row carries the hard stop's mark, so a
|
|
601
|
+
// finished child ends its unit `stopped` whatever the child's own status.
|
|
602
|
+
| { type: "read-record"; step: string; run: ChildFacts; stopped?: true; at: number }
|
|
585
603
|
| { type: "pr-check"; step: string; pr: PrCheck; at: number }
|
|
586
604
|
| { type: "merge"; step: string; outcome: "merged"; sha: string; at: number }
|
|
587
605
|
// The door found the pull request already merged after the approval — auto-merge
|
|
588
606
|
// fired, or a person merged — so the runner merged nothing (`by: other`).
|
|
589
607
|
| { type: "merge"; step: string; outcome: "merged"; by: "other"; sha: string; mergedAt: string; at: number }
|
|
590
|
-
|
|
608
|
+
/** The door enqueued the pull request (or found it still queued), or the
|
|
609
|
+
* queue removed it — the reason is the queue's own (issue 2011). */
|
|
610
|
+
| { type: "merge"; step: string; outcome: "pending" | "refused" | "enqueued" | "removed"; reason: string; at: number }
|
|
591
611
|
/** The checks read at the reviewed head; `checks` absent means GitHub could
|
|
592
612
|
* not be read, which the machine treats as pending (record 0055). A retry
|
|
593
613
|
* ask answers `retried` instead: whether the bot dispatched the re-run —
|
|
@@ -629,6 +649,20 @@ export function checkFinding(f: CheckFailure): Finding {
|
|
|
629
649
|
};
|
|
630
650
|
}
|
|
631
651
|
|
|
652
|
+
/** A merge-queue removal as a finding of the round (issue 2011): the queue's
|
|
653
|
+
* reason — a failing check inside the queue, a conflict — rides the findings
|
|
654
|
+
* brief exactly as a red check does, severity `blocking` so the level in
|
|
655
|
+
* force always counts it, and a fix round follows. */
|
|
656
|
+
export function queueRemovalFinding(reason: string): Finding {
|
|
657
|
+
return {
|
|
658
|
+
id: "check:merge-queue",
|
|
659
|
+
severity: "blocking",
|
|
660
|
+
file: "merge queue",
|
|
661
|
+
title: `removed from the merge queue — ${reason}`,
|
|
662
|
+
check: true,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
632
666
|
/** The check findings of a review round: the rows the checks step appended to
|
|
633
667
|
* the round's findings, told apart by provenance — the `check` flag only
|
|
634
668
|
* `checkFinding` sets, never the id: a reviewer's id is a free string, and a
|
|
@@ -653,6 +687,20 @@ export type UnitEnding =
|
|
|
653
687
|
* The unit is done and its dependents start on a base that carries it. */
|
|
654
688
|
| { kind: "already_landed"; landed: HandoffLanded[]; round: RoundRef; runId: string; reviewRounds: number }
|
|
655
689
|
| { kind: "merge_ready"; pr: PrRef; reviewRounds: number }
|
|
690
|
+
/** Every finding the round would act on is human-gated — a receipt only a
|
|
691
|
+
* person can produce (issue 1990; the reviewer set the flag through
|
|
692
|
+
* `submit_verdict`, agent-review item 5) — so a fix round could change
|
|
693
|
+
* nothing: the unit ends held for a person, the ending carrying the
|
|
694
|
+
* human-gated rows, and a re-issue with the pull request resumes at the
|
|
695
|
+
* review round once the receipt is posted (item 10's resume path). */
|
|
696
|
+
| {
|
|
697
|
+
kind: "held";
|
|
698
|
+
pr?: PrRef;
|
|
699
|
+
round: RoundRef;
|
|
700
|
+
findings: Finding[];
|
|
701
|
+
verdict: "approve" | "request_changes";
|
|
702
|
+
reviewRounds: number;
|
|
703
|
+
}
|
|
656
704
|
| { kind: "merge_refused"; pr: PrRef; reason: string; reviewRounds: number }
|
|
657
705
|
| { kind: "round_cap"; maxRounds: number; reviewRounds: number }
|
|
658
706
|
| {
|
|
@@ -710,6 +758,14 @@ export type UnitEnding =
|
|
|
710
758
|
spent: ShipBudgetSpent;
|
|
711
759
|
}
|
|
712
760
|
| { kind: "no_verdict"; round: RoundRef; reviewRounds: number; finalReply?: string }
|
|
761
|
+
/** Round 0's coding child died on a provider transient — a model-gateway
|
|
762
|
+
* 5xx, a cut stream, a gateway timeout, past the harness's retry ladder —
|
|
763
|
+
* with nothing pushed, TWICE: the first such death re-ran the round once
|
|
764
|
+
* (the ledger row and the branch untouched, a re-run costs only minutes),
|
|
765
|
+
* and the second is the ending (agent-ship item 9, issue 1932). Named
|
|
766
|
+
* `transient` so it reads as a condition beside `checks_failed` and `held`
|
|
767
|
+
* in the plane's table, never as the child failing on its task. */
|
|
768
|
+
| { kind: "transient"; round: RoundRef; runId: string; reviewRounds: number }
|
|
713
769
|
| { kind: "interrupted"; round: RoundRef; runId: string; reviewRounds: number }
|
|
714
770
|
| { kind: "refused"; refusal: string; message?: string; round: RoundRef; reviewRounds: number }
|
|
715
771
|
/** The unit idles instead of ending (record 0051): with the resolved
|
|
@@ -737,18 +793,24 @@ export type UnitEnding =
|
|
|
737
793
|
reviewRounds: number;
|
|
738
794
|
};
|
|
739
795
|
|
|
740
|
-
/** The kinds that idle: every ending but the
|
|
796
|
+
/** The kinds that idle: every ending but the ended ones — the unit is
|
|
741
797
|
* unfinished (a cap, a stop, an abort, a refused merge, a segment's end) and
|
|
742
|
-
* a reply could continue it. `merged`, `already_landed`, `merge_ready
|
|
743
|
-
* `refused` never idle: the first two are done, merge-ready waits
|
|
744
|
-
* person's merge,
|
|
745
|
-
|
|
798
|
+
* a reply could continue it. `merged`, `already_landed`, `merge_ready`,
|
|
799
|
+
* `held` and `refused` never idle: the first two are done, merge-ready waits
|
|
800
|
+
* only for a person's merge, held waits only for a person's receipt (a fix
|
|
801
|
+
* round could change nothing, so nothing here can continue it), and a
|
|
802
|
+
* refused child would be refused again. */
|
|
803
|
+
export type IdleWhy = Exclude<
|
|
804
|
+
UnitEnding["kind"],
|
|
805
|
+
"idle" | "merged" | "already_landed" | "merge_ready" | "held" | "refused"
|
|
806
|
+
>;
|
|
746
807
|
|
|
747
808
|
const NEVER_IDLES: ReadonlySet<UnitEnding["kind"]> = new Set([
|
|
748
809
|
"idle",
|
|
749
810
|
"merged",
|
|
750
811
|
"already_landed",
|
|
751
812
|
"merge_ready",
|
|
813
|
+
"held",
|
|
752
814
|
"refused",
|
|
753
815
|
]);
|
|
754
816
|
|
|
@@ -878,9 +940,17 @@ type Phase =
|
|
|
878
940
|
* pushed: the pr-check recovers a pushed branch by opening its pull
|
|
879
941
|
* request; with nothing pushed the unit ends with the child's own reason. */
|
|
880
942
|
dead?: "failed" | "interrupted";
|
|
943
|
+
/** The dead child's record names a provider transient (`failure:
|
|
944
|
+
* provider_transient`, issue 1932): with nothing pushed, round 0 is
|
|
945
|
+
* re-run once instead of the unit aborting; a second transient in the
|
|
946
|
+
* same round is the `transient` ending. */
|
|
947
|
+
transient?: true;
|
|
881
948
|
}
|
|
882
|
-
|
|
883
|
-
|
|
949
|
+
/** `queued`: the door enqueued the pull request — the base takes changes
|
|
950
|
+
* only through a merge queue (issue 2011) — so every later ask reads the
|
|
951
|
+
* queue's outcome instead of attempting the squash again. */
|
|
952
|
+
| { at: "merge"; pr: PrRef; headSha: string; n: number; since: number; waitMs: number; queued?: true }
|
|
953
|
+
| { at: "merge-wait"; pr: PrRef; headSha: string; n: number; since: number; waitMs: number; queued?: true }
|
|
884
954
|
/** The round's checks step (record 0055): the check runs at the reviewed head
|
|
885
955
|
* are read after an approve settles, before merge_ready or the merge door.
|
|
886
956
|
* `graced`: a head with no check reported has had its one-chunk grace;
|
|
@@ -1025,7 +1095,8 @@ function roundCarve(s: UnitPipelineState, round: RoundRef): Carve {
|
|
|
1025
1095
|
export const stepPrefixOf = (unit: string, session: UnitSession | undefined): string =>
|
|
1026
1096
|
session !== undefined && session.segment > 1 ? `${unit}/s${session.segment}` : unit;
|
|
1027
1097
|
const stepPrefix = (s: UnitPipelineState) => stepPrefixOf(s.input.unit.id, s.input.session);
|
|
1028
|
-
const roundStep = (s: UnitPipelineState, round: RoundRef) =>
|
|
1098
|
+
const roundStep = (s: UnitPipelineState, round: RoundRef) =>
|
|
1099
|
+
`${stepPrefix(s)}/${round.index}/${round.kind}${round.attempt !== undefined ? `/a${round.attempt}` : ""}`;
|
|
1029
1100
|
|
|
1030
1101
|
function briefFor(s: UnitPipelineState, round: RoundRef): Brief {
|
|
1031
1102
|
const unit = s.input.unit.id;
|
|
@@ -1149,7 +1220,13 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
1149
1220
|
timeoutMs: Math.max(MIN, Math.min(MERGE_WAIT_CHUNK_MS, p.waitMs - (s.clock - p.since))),
|
|
1150
1221
|
};
|
|
1151
1222
|
case "merge":
|
|
1152
|
-
return {
|
|
1223
|
+
return {
|
|
1224
|
+
type: "merge",
|
|
1225
|
+
step: `${unit}/merge/${p.n}`,
|
|
1226
|
+
prNumber: p.pr.number,
|
|
1227
|
+
headSha: p.headSha,
|
|
1228
|
+
...(p.queued === true ? { queued: true as const } : {}),
|
|
1229
|
+
};
|
|
1153
1230
|
case "merge-wait":
|
|
1154
1231
|
return {
|
|
1155
1232
|
type: "wait-checks",
|
|
@@ -1194,7 +1271,10 @@ function end(s: UnitPipelineState, ending: UnitEnding, notes: CoordinatorNote[]
|
|
|
1194
1271
|
function idleEnding(s: UnitPipelineState, ending: UnitEnding): Extract<UnitEnding, { kind: "idle" }> | undefined {
|
|
1195
1272
|
if ((s.input.idleDays ?? 0) <= 0) return undefined;
|
|
1196
1273
|
if (NEVER_IDLES.has(ending.kind)) return undefined;
|
|
1197
|
-
const old = ending as Exclude<
|
|
1274
|
+
const old = ending as Exclude<
|
|
1275
|
+
UnitEnding,
|
|
1276
|
+
{ kind: "idle" | "merged" | "already_landed" | "merge_ready" | "held" | "refused" }
|
|
1277
|
+
>;
|
|
1198
1278
|
const grant = s.input.grant ?? DEFAULT_GRANT;
|
|
1199
1279
|
// Unspent: an idle spends no renewal — the wake's segment does (this plan's
|
|
1200
1280
|
// fifth unit) — so the row says what the grant still holds.
|
|
@@ -1234,6 +1314,29 @@ function idleEnding(s: UnitPipelineState, ending: UnitEnding): Extract<UnitEndin
|
|
|
1234
1314
|
/** A gated finding as the gate note names it: `F1 (minor)`. */
|
|
1235
1315
|
const gateLabel = (f: Finding): string => `${f.id} (${f.severity})`;
|
|
1236
1316
|
|
|
1317
|
+
/** The held decision (issue 1990): the findings the round would act on — the
|
|
1318
|
+
* gated set on an approve, every finding on a request_changes — are all
|
|
1319
|
+
* human-gated, read off the reviewer's own flag and never prose. One
|
|
1320
|
+
* actionable finding beside a human-gated one keeps the fix round: the
|
|
1321
|
+
* dispositions cover the human-gated row like any other (declined, with the
|
|
1322
|
+
* person named). An empty set decides nothing. */
|
|
1323
|
+
const allHumanGated = (findings: readonly Finding[]): boolean =>
|
|
1324
|
+
findings.length > 0 && findings.every((f) => f.humanGated === true);
|
|
1325
|
+
|
|
1326
|
+
const heldEnding = (
|
|
1327
|
+
s: UnitPipelineState,
|
|
1328
|
+
round: RoundRef,
|
|
1329
|
+
findings: Finding[],
|
|
1330
|
+
verdict: "approve" | "request_changes",
|
|
1331
|
+
): UnitEnding => ({
|
|
1332
|
+
kind: "held",
|
|
1333
|
+
...(s.pr !== undefined ? { pr: s.pr } : {}),
|
|
1334
|
+
round,
|
|
1335
|
+
findings,
|
|
1336
|
+
verdict,
|
|
1337
|
+
reviewRounds: s.reviewRounds,
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1237
1340
|
const roundNote = (round: RoundRef, outcome: ShipRoundOutcome): RoundNote => ({
|
|
1238
1341
|
type: "round",
|
|
1239
1342
|
index: round.index,
|
|
@@ -1332,7 +1435,19 @@ function settleCoding(
|
|
|
1332
1435
|
// request (agent-ship items 10 and 15), and only a branch with
|
|
1333
1436
|
// nothing on it ends the unit with the child's own reason.
|
|
1334
1437
|
if (facts.status === "failed")
|
|
1335
|
-
return {
|
|
1438
|
+
return {
|
|
1439
|
+
state: {
|
|
1440
|
+
...next,
|
|
1441
|
+
phase: {
|
|
1442
|
+
at: "pr-check",
|
|
1443
|
+
round,
|
|
1444
|
+
runId,
|
|
1445
|
+
dead: "failed",
|
|
1446
|
+
...(facts.failure?.kind === "provider_transient" ? { transient: true as const } : {}),
|
|
1447
|
+
},
|
|
1448
|
+
},
|
|
1449
|
+
notes: [],
|
|
1450
|
+
};
|
|
1336
1451
|
next = {
|
|
1337
1452
|
...next,
|
|
1338
1453
|
phase: {
|
|
@@ -1429,6 +1544,9 @@ function settleReview(
|
|
|
1429
1544
|
},
|
|
1430
1545
|
notes,
|
|
1431
1546
|
);
|
|
1547
|
+
// Every gated finding is human-gated (issue 1990): a fix round could
|
|
1548
|
+
// change nothing, so the unit ends held for a person instead.
|
|
1549
|
+
if (allHumanGated(gated)) return end(next, heldEnding(next, round, gated, "approve"), notes);
|
|
1432
1550
|
if (next.reviewRounds >= next.input.caps.maxRounds)
|
|
1433
1551
|
return end(
|
|
1434
1552
|
next,
|
|
@@ -1457,6 +1575,11 @@ function settleReview(
|
|
|
1457
1575
|
},
|
|
1458
1576
|
notes,
|
|
1459
1577
|
);
|
|
1578
|
+
// Every finding of the round is human-gated (issue 1990): no fix round can
|
|
1579
|
+
// change anything, so the unit ends held for a person's receipt instead of
|
|
1580
|
+
// spending a coding child — or the round cap — on it.
|
|
1581
|
+
if (allHumanGated(verdict.findings ?? []))
|
|
1582
|
+
return end(next, heldEnding(next, round, verdict.findings ?? [], "request_changes"), notes);
|
|
1460
1583
|
if (next.reviewRounds >= next.input.caps.maxRounds)
|
|
1461
1584
|
return end(
|
|
1462
1585
|
next,
|
|
@@ -1500,14 +1623,51 @@ function enterChecks(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNo
|
|
|
1500
1623
|
};
|
|
1501
1624
|
}
|
|
1502
1625
|
|
|
1626
|
+
/** The order the checks step reads a head in (record 0055, issue 1991):
|
|
1627
|
+
* a failed check is the round's answer as soon as it is read, whatever else
|
|
1628
|
+
* is still pending — with the flake rule spending its one re-run first when
|
|
1629
|
+
* every failure is a suspect — and only a head with no failed check waits on
|
|
1630
|
+
* the pending ones; a head with no check reported gets one chunk of grace;
|
|
1631
|
+
* the rest is green. One function so the siblings that fold more outcomes
|
|
1632
|
+
* into the step (a held ending, a transient round re-run) read the same order. */
|
|
1633
|
+
function checksVerdict(
|
|
1634
|
+
checks: RoundChecks | undefined,
|
|
1635
|
+
p: { retried: boolean; graced: boolean },
|
|
1636
|
+
):
|
|
1637
|
+
| { kind: "retry"; names: string[] }
|
|
1638
|
+
| { kind: "failed"; failed: CheckFailure[] }
|
|
1639
|
+
| { kind: "pending" }
|
|
1640
|
+
| { kind: "grace" }
|
|
1641
|
+
| { kind: "green" } {
|
|
1642
|
+
if (checks !== undefined && checks.failed.length > 0) {
|
|
1643
|
+
// The flake rule (record 0055): a suspected flake — a test timeout or
|
|
1644
|
+
// runner stall on a shard whose test files the pull request's changed
|
|
1645
|
+
// paths never touch — is re-run once before it becomes a finding. One
|
|
1646
|
+
// real failure among them makes the round's answer already known, so the
|
|
1647
|
+
// re-run is spent only when every failure is a suspect.
|
|
1648
|
+
if (!p.retried && checks.failed.every((f) => f.flakeSuspect === true))
|
|
1649
|
+
return { kind: "retry", names: checks.failed.map((f) => f.name) };
|
|
1650
|
+
return { kind: "failed", failed: checks.failed };
|
|
1651
|
+
}
|
|
1652
|
+
// GitHub unreadable answers as pending and is re-read at the chunk's end.
|
|
1653
|
+
if (checks === undefined || checks.pending.length > 0) return { kind: "pending" };
|
|
1654
|
+
// No check reported at the head: one chunk of grace — the first check starts
|
|
1655
|
+
// within minutes where CI exists — then the round proceeds, so a repository
|
|
1656
|
+
// without CI costs one chunk per round and never idles (record 0055).
|
|
1657
|
+
if (checks.total === 0 && !p.graced) return { kind: "grace" };
|
|
1658
|
+
return { kind: "green" };
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1503
1661
|
/** What the checks step answered, folded into the round (record 0055). Pure
|
|
1504
1662
|
* over the phase: a retry ask waits for the head to settle and reads again;
|
|
1505
|
-
*
|
|
1663
|
+
* a failed check becomes a check finding under a round note of its own and
|
|
1664
|
+
* the findings step runs as for any changes-requested round — never
|
|
1665
|
+
* merge_ready, never the merge door — as soon as it is read, whatever else
|
|
1666
|
+
* is still pending (issue 1991); only a head with no failed check waits a
|
|
1667
|
+
* chunk inside the step's ask on what is unreadable or pending and then
|
|
1506
1668
|
* proceeds — the ending's facts read names what is still pending; a head with
|
|
1507
|
-
* no check reported waits one chunk of grace and never more; a
|
|
1508
|
-
*
|
|
1509
|
-
* runs as for any changes-requested round — never merge_ready, never the
|
|
1510
|
-
* merge door; a green head proceeds with no wait added. */
|
|
1669
|
+
* no check reported waits one chunk of grace and never more; a green head
|
|
1670
|
+
* proceeds with no wait added. */
|
|
1511
1671
|
function settleChecks(
|
|
1512
1672
|
s: UnitPipelineState,
|
|
1513
1673
|
p: Extract<Phase, { at: "checks" }>,
|
|
@@ -1546,48 +1706,43 @@ function settleChecks(
|
|
|
1546
1706
|
}
|
|
1547
1707
|
return wait({ retried: true });
|
|
1548
1708
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
// merge door (under `merge: runner`) is the guard that never merges over it.
|
|
1553
|
-
if (checks === undefined || checks.pending.length > 0) {
|
|
1554
|
-
if (s.clock - p.since >= p.waitMs) return approveOutcome(s, []);
|
|
1555
|
-
return wait({});
|
|
1556
|
-
}
|
|
1557
|
-
if (checks.failed.length > 0) {
|
|
1558
|
-
// The flake rule (record 0055): a suspected flake — a test timeout or
|
|
1559
|
-
// runner stall on a shard whose test files the pull request's changed
|
|
1560
|
-
// paths never touch — is re-run once before it becomes a finding. One
|
|
1561
|
-
// real failure among them makes the round's answer already known, so the
|
|
1562
|
-
// re-run is spent only when every failure is a suspect.
|
|
1563
|
-
if (!p.retried && checks.failed.every((f) => f.flakeSuspect === true))
|
|
1709
|
+
const verdict = checksVerdict(checks, p);
|
|
1710
|
+
switch (verdict.kind) {
|
|
1711
|
+
case "retry":
|
|
1564
1712
|
return {
|
|
1565
|
-
state: { ...s, phase: { ...p, n: p.n + 1, retry:
|
|
1713
|
+
state: { ...s, phase: { ...p, n: p.n + 1, retry: verdict.names } },
|
|
1566
1714
|
notes: [],
|
|
1567
1715
|
};
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1716
|
+
case "failed": {
|
|
1717
|
+
const findings = [...(s.findingsByRound[round.index] ?? []), ...verdict.failed.map(checkFinding)];
|
|
1718
|
+
const next: UnitPipelineState = {
|
|
1719
|
+
...s,
|
|
1720
|
+
findingsByRound: { ...s.findingsByRound, [round.index]: findings },
|
|
1721
|
+
};
|
|
1722
|
+
// The failed checks get a round note of their own (record 0055) — never
|
|
1723
|
+
// the parser-mismatch gate — and the findings step runs as for any
|
|
1724
|
+
// changes-requested round: dispositions, a fix round, a re-review.
|
|
1725
|
+
const notes: CoordinatorNote[] = [roundNote(round, "checks_failed")];
|
|
1726
|
+
if (next.reviewRounds >= next.input.caps.maxRounds)
|
|
1727
|
+
return end(
|
|
1728
|
+
next,
|
|
1729
|
+
{ kind: "round_cap", maxRounds: next.input.caps.maxRounds, reviewRounds: next.reviewRounds },
|
|
1730
|
+
notes,
|
|
1731
|
+
);
|
|
1732
|
+
return enterRound(next, { index: round.index, kind: "findings" }, notes);
|
|
1733
|
+
}
|
|
1734
|
+
case "pending":
|
|
1735
|
+
// A head still pending or unreadable at the ask's end proceeds — the
|
|
1736
|
+
// ending's facts read names what is still pending, and the merge door
|
|
1737
|
+
// (under `merge: runner`) is the guard that never merges over it.
|
|
1738
|
+
if (s.clock - p.since >= p.waitMs) return approveOutcome(s, []);
|
|
1739
|
+
return wait({});
|
|
1740
|
+
case "grace":
|
|
1741
|
+
return wait({ graced: true });
|
|
1742
|
+
case "green":
|
|
1743
|
+
// Green (or none reported after the grace): today's path, no wait added.
|
|
1744
|
+
return approveOutcome(s, []);
|
|
1584
1745
|
}
|
|
1585
|
-
// No check reported at the head: one chunk of grace — the first check starts
|
|
1586
|
-
// within minutes where CI exists — then the round proceeds, so a repository
|
|
1587
|
-
// without CI costs one chunk per round and never idles (record 0055).
|
|
1588
|
-
if (checks.total === 0 && !p.graced) return wait({ graced: true });
|
|
1589
|
-
// Green (or none reported after the grace): today's path, no wait added.
|
|
1590
|
-
return approveOutcome(s, []);
|
|
1591
1746
|
}
|
|
1592
1747
|
|
|
1593
1748
|
/** An approve past the checks read: merge_ready for a person, the merge door
|
|
@@ -1661,6 +1816,16 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
1661
1816
|
roundNote(round, "aborted"),
|
|
1662
1817
|
]);
|
|
1663
1818
|
if (phase.dead === "failed") {
|
|
1819
|
+
// A provider transient with nothing pushed is not the child's failure
|
|
1820
|
+
// (issue 1932): the ledger row and the branch are untouched, so round 0
|
|
1821
|
+
// is re-run once — a fresh attempt under fresh step names — and only a
|
|
1822
|
+
// second transient in the same round is the ending, named `transient`.
|
|
1823
|
+
if (phase.transient && round.kind === "coding" && pr.unrecovered === "no_commits") {
|
|
1824
|
+
if ((round.attempt ?? 1) < 2) return enterRound(s, { ...round, attempt: 2 }, [roundNote(round, "transient")]);
|
|
1825
|
+
return end(s, { kind: "transient", round, runId: phase.runId, reviewRounds: s.reviewRounds }, [
|
|
1826
|
+
roundNote(round, "transient"),
|
|
1827
|
+
]);
|
|
1828
|
+
}
|
|
1664
1829
|
// The abort repeats the bot's reason for recovering nothing, and claims
|
|
1665
1830
|
// no more than the answer carried.
|
|
1666
1831
|
const why =
|
|
@@ -1967,6 +2132,12 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1967
2132
|
round: p.round,
|
|
1968
2133
|
reviewRounds: s.reviewRounds,
|
|
1969
2134
|
});
|
|
2135
|
+
case "stopped":
|
|
2136
|
+
// The hosted parent's hard stop (record 0060; issue 1924): the spawn
|
|
2137
|
+
// was refused over the stop mark, so the unit ends stopped here.
|
|
2138
|
+
return end(clocked, { kind: "stopped", mode: "hard", round: p.round, reviewRounds: s.reviewRounds }, [
|
|
2139
|
+
roundNote(p.round, "stopped"),
|
|
2140
|
+
]);
|
|
1970
2141
|
case "failed":
|
|
1971
2142
|
return end(clocked, {
|
|
1972
2143
|
kind: "aborted",
|
|
@@ -2005,6 +2176,21 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2005
2176
|
},
|
|
2006
2177
|
notes: [],
|
|
2007
2178
|
};
|
|
2179
|
+
// The hosted parent's hard stop landed while this child ran (record 0060;
|
|
2180
|
+
// issue 1924): the unit ends stopped as the child ends, whatever the
|
|
2181
|
+
// child's own status — the runner runs nothing more of it.
|
|
2182
|
+
if (r.stopped === true)
|
|
2183
|
+
return end(
|
|
2184
|
+
clocked,
|
|
2185
|
+
{
|
|
2186
|
+
kind: "stopped",
|
|
2187
|
+
mode: "hard",
|
|
2188
|
+
round: p.round,
|
|
2189
|
+
reviewRounds: s.reviewRounds,
|
|
2190
|
+
...(r.run.finalReply !== undefined ? { finalReply: r.run.finalReply } : {}),
|
|
2191
|
+
},
|
|
2192
|
+
[roundNote(p.round, "stopped")],
|
|
2193
|
+
);
|
|
2008
2194
|
if (r.run.status === "interrupted") {
|
|
2009
2195
|
// A dead CODING child may have pushed before the ledger closed it: the
|
|
2010
2196
|
// pr-check recovers the branch. A review child has nothing on the
|
|
@@ -2064,27 +2250,81 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2064
2250
|
: end(clocked, { kind: "merged", by: "runner", pr: p.pr, sha: r.sha, reviewRounds: s.reviewRounds });
|
|
2065
2251
|
if (r.outcome === "refused")
|
|
2066
2252
|
return end(clocked, { kind: "merge_refused", pr: p.pr, reason: r.reason, reviewRounds: s.reviewRounds });
|
|
2253
|
+
if (r.outcome === "removed") {
|
|
2254
|
+
// The queue removed the pull request — a failing check in the queue, a
|
|
2255
|
+
// conflict: the removal reason becomes a finding of the round, like a
|
|
2256
|
+
// red check does through the checks step, and a fix round follows
|
|
2257
|
+
// (issue 2011). With no review run to brief the fix from (a resume
|
|
2258
|
+
// straight at the merge decision) a person decides, the refusal
|
|
2259
|
+
// carrying the queue's own reason.
|
|
2260
|
+
const round: RoundRef = { index: s.reviewRounds, kind: "findings" };
|
|
2261
|
+
if (s.reviewRunByRound[round.index] === undefined)
|
|
2262
|
+
return end(clocked, {
|
|
2263
|
+
kind: "merge_refused",
|
|
2264
|
+
pr: p.pr,
|
|
2265
|
+
reason: `the merge queue removed the pull request: ${r.reason}`,
|
|
2266
|
+
reviewRounds: s.reviewRounds,
|
|
2267
|
+
});
|
|
2268
|
+
const findings = [...(s.findingsByRound[round.index] ?? []), queueRemovalFinding(r.reason)];
|
|
2269
|
+
const next: UnitPipelineState = {
|
|
2270
|
+
...clocked,
|
|
2271
|
+
findingsByRound: { ...s.findingsByRound, [round.index]: findings },
|
|
2272
|
+
};
|
|
2273
|
+
const notes: CoordinatorNote[] = [roundNote(round, "dequeued")];
|
|
2274
|
+
if (next.reviewRounds >= next.input.caps.maxRounds)
|
|
2275
|
+
return end(
|
|
2276
|
+
next,
|
|
2277
|
+
{ kind: "round_cap", maxRounds: next.input.caps.maxRounds, reviewRounds: next.reviewRounds },
|
|
2278
|
+
notes,
|
|
2279
|
+
);
|
|
2280
|
+
return enterRound(next, round, notes);
|
|
2281
|
+
}
|
|
2067
2282
|
const waited = r.at - p.since;
|
|
2068
2283
|
if (waited >= p.waitMs)
|
|
2069
2284
|
return end(clocked, {
|
|
2070
2285
|
kind: "merge_refused",
|
|
2071
2286
|
pr: p.pr,
|
|
2072
|
-
reason:
|
|
2287
|
+
reason:
|
|
2288
|
+
r.outcome === "enqueued"
|
|
2289
|
+
? `still in the merge queue after ${Math.round(waited / MIN)} minutes (${r.reason}) — the queue merges it on its own; a re-issued plan finds it merged`
|
|
2290
|
+
: `still pending after ${Math.round(waited / MIN)} minutes (${r.reason})`,
|
|
2073
2291
|
reviewRounds: s.reviewRounds,
|
|
2074
2292
|
});
|
|
2293
|
+
// `enqueued` waits like `pending`, marked so every later ask reads the
|
|
2294
|
+
// queue's outcome; the boundary is noted once, when the queue takes it.
|
|
2295
|
+
const queued = r.outcome === "enqueued" ? true : p.queued;
|
|
2075
2296
|
return {
|
|
2076
2297
|
state: {
|
|
2077
2298
|
...clocked,
|
|
2078
|
-
phase: {
|
|
2299
|
+
phase: {
|
|
2300
|
+
at: "merge-wait",
|
|
2301
|
+
pr: p.pr,
|
|
2302
|
+
headSha: p.headSha,
|
|
2303
|
+
n: p.n,
|
|
2304
|
+
since: p.since,
|
|
2305
|
+
waitMs: p.waitMs,
|
|
2306
|
+
...(queued === true ? { queued: true as const } : {}),
|
|
2307
|
+
},
|
|
2079
2308
|
},
|
|
2080
|
-
notes:
|
|
2309
|
+
notes:
|
|
2310
|
+
r.outcome === "enqueued" && p.queued !== true
|
|
2311
|
+
? [roundNote({ index: s.reviewRounds, kind: "review" }, "enqueued")]
|
|
2312
|
+
: [],
|
|
2081
2313
|
};
|
|
2082
2314
|
}
|
|
2083
2315
|
case "merge-wait":
|
|
2084
2316
|
return {
|
|
2085
2317
|
state: {
|
|
2086
2318
|
...s,
|
|
2087
|
-
phase: {
|
|
2319
|
+
phase: {
|
|
2320
|
+
at: "merge",
|
|
2321
|
+
pr: p.pr,
|
|
2322
|
+
headSha: p.headSha,
|
|
2323
|
+
n: p.n + 1,
|
|
2324
|
+
since: p.since,
|
|
2325
|
+
waitMs: p.waitMs,
|
|
2326
|
+
...(p.queued === true ? { queued: true as const } : {}),
|
|
2327
|
+
},
|
|
2088
2328
|
},
|
|
2089
2329
|
notes: [],
|
|
2090
2330
|
};
|
|
@@ -2292,6 +2532,25 @@ export function renderUnitReport(
|
|
|
2292
2532
|
]
|
|
2293
2533
|
.filter(Boolean)
|
|
2294
2534
|
.join("\n");
|
|
2535
|
+
case "held": {
|
|
2536
|
+
// The person's next step is the report's whole point (issue 1990): the
|
|
2537
|
+
// human-gated rows are named with the reviewer's own words, and the
|
|
2538
|
+
// re-issue line says the attempt resumes at the review round — item 10's
|
|
2539
|
+
// resume path — once the receipt stands on the pull request. That path
|
|
2540
|
+
// fires only when the invocation carries NO new task text: re-issuing
|
|
2541
|
+
// with the task would ADOPT the pull request and run a coding round
|
|
2542
|
+
// first (item 10), the very round this ending exists to avoid — so the
|
|
2543
|
+
// held case renders its own re-issue line instead of the shared one.
|
|
2544
|
+
const rows = e.findings.map((f) => `${f.id} (${f.severity}) — ${f.title}`).join("; ");
|
|
2545
|
+
const heldReissue = s.input.generated
|
|
2546
|
+
? `To continue, re-issue \`agent:ship\` in this thread with only the PR URL${e.pr !== undefined ? ` (${e.pr.url})` : ""} — no new task text.`
|
|
2547
|
+
: reissue;
|
|
2548
|
+
return join([
|
|
2549
|
+
`⏸️ ${e.verdict === "approve" ? "Approved but held" : "Changes requested but held"} after ${rounds}${e.pr !== undefined ? `: ${e.pr.url}` : ""} — every finding of review round ${e.round.index} is human-gated, a receipt only a person can produce: ${rows}. No fix round was opened: a coding child cannot produce the receipt.`,
|
|
2550
|
+
aside(levelLine),
|
|
2551
|
+
`Next step: produce the receipt each finding names and post it on the pull request. ${heldReissue} The re-issued attempt resumes at the review round — no coding round runs first.`,
|
|
2552
|
+
]);
|
|
2553
|
+
}
|
|
2295
2554
|
case "merge_refused":
|
|
2296
2555
|
// The approved work is on the branch, so the remedy is a person's hand
|
|
2297
2556
|
// merge, never a re-run: a seeded plan re-issued afterwards finds the
|
|
@@ -2363,6 +2622,13 @@ export function renderUnitReport(
|
|
|
2363
2622
|
`🔁 Segment ${e.segment - 1} ended at its lease with the unit unfinished — ${e.line}. Segment ${e.segment} opens in this thread${e.from !== undefined ? ` from \`${e.from.slice(0, 7)}\`` : ""} under a fresh ${s.input.caps.maxMinutes}-minute lease, with this segment's write-up as its request; ${e.renewalsLeft} renewal${e.renewalsLeft === 1 ? "" : "s"} remain${e.spendUsd !== null ? `, $${e.spendUsd.toFixed(2)} spent so far` : ""}.`,
|
|
2364
2623
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
2365
2624
|
]);
|
|
2625
|
+
case "transient":
|
|
2626
|
+
return join([
|
|
2627
|
+
`⚠️ The coding child of round ${e.round.index} (run ${e.runId}) died on a provider transient — a model-gateway 5xx, a cut stream or a gateway timeout past the harness's retry ladder — with nothing pushed, after the round was already re-run once for the same reason. The task itself was never the problem.`,
|
|
2628
|
+
aside(writeUpPointer(s, e.round.kind, s.lastCodingRunId)),
|
|
2629
|
+
`⚠️ Ship ended after ${rounds}; re-issue once the provider settles.`,
|
|
2630
|
+
reissue,
|
|
2631
|
+
]);
|
|
2366
2632
|
case "no_verdict":
|
|
2367
2633
|
return join([
|
|
2368
2634
|
`⚠️ Review round ${e.round.index} ended without a submitted verdict (budget, refusal, or stop) — ship never converts that into a request for changes, so no findings step ran.`,
|
|
@@ -54,6 +54,9 @@ export function shimRoute(pathname: string): string | undefined {
|
|
|
54
54
|
if (pathname.startsWith("/admin/")) return "admin";
|
|
55
55
|
// The artifact copy (deploy/cloudflare/artifactsCopy.ts): the shim's own route, a 1 GB stream.
|
|
56
56
|
if (pathname === "/artifacts/copy") return "artifacts";
|
|
57
|
+
// The plane's effect push (record 0064): the state Worker's service-binding
|
|
58
|
+
// call, bearer-gated by the shim and forwarded to the container.
|
|
59
|
+
if (pathname === "/plane/effects") return "plane";
|
|
57
60
|
// The model proxy's three routes (docs/reference/specs/model-proxy.md): a bounded
|
|
58
61
|
// request per model call, forwarded to the container like everything else.
|
|
59
62
|
if (pathname === "/v1/messages" || pathname === "/v1/chat/completions" || pathname === "/v1/responses")
|