@coreplane/switchboard 1.250.0 → 1.252.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 +13 -2
- package/dist/assets/deploy/cloudflare/preflight.mjs +19 -21
- package/dist/assets/deploy/cloudflare/worker.ts +6 -3
- package/dist/assets/deploy/cloudflare-memory/worker.ts +108 -12
- package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
- package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +3 -2
- package/dist/assets/project.json +13 -9
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +5 -5
- package/dist/assets/src/core/authz/policy.ts +4 -0
- package/dist/assets/src/core/authz/resource.ts +6 -2
- package/dist/assets/src/core/authz/types.ts +2 -0
- package/dist/assets/src/core/budgets.ts +22 -0
- package/dist/assets/src/core/coordinator/contract.ts +42 -0
- package/dist/assets/src/core/coordinator/driver.ts +150 -12
- package/dist/assets/src/core/drain.ts +50 -0
- package/dist/assets/src/core/memory/engine.ts +98 -0
- package/dist/assets/src/core/memory/scorer.ts +12 -4
- package/dist/assets/src/core/memory/types.ts +69 -12
- package/dist/assets/src/core/modelCard.ts +51 -7
- package/dist/assets/src/core/modelPricing.ts +111 -1
- package/dist/assets/src/core/modelProxy/usage.ts +88 -0
- package/dist/assets/src/core/modelRegistry.ts +15 -1
- package/dist/assets/src/core/provider.ts +49 -0
- package/dist/assets/src/core/refusal.ts +6 -6
- package/dist/assets/src/core/reviewVerdict.ts +4 -0
- package/dist/assets/src/core/runEvents.ts +51 -2
- package/dist/assets/src/core/runFriction.ts +7 -2
- package/dist/assets/src/core/runLedger/types.ts +23 -4
- package/dist/assets/src/core/runRecord.ts +16 -0
- package/dist/assets/src/core/runUsage.ts +67 -13
- package/dist/assets/src/core/schedules.ts +3 -0
- package/dist/assets/src/core/ship/contract.ts +45 -5
- package/dist/assets/src/core/ship/coordinator.ts +441 -48
- package/dist/assets/src/core/ship/renewal.ts +10 -5
- package/dist/assets/src/core/trace/attrs.ts +24 -0
- package/dist/assets/src/core/types.ts +5 -5
- package/dist/assets/src/core/verbosity.ts +48 -0
- package/dist/assets/src/deploy/liveGate.ts +40 -13
- package/dist/assets/src/deploy/restart.ts +11 -12
- package/dist/assets/src/execution/residentDepCache.ts +50 -1
- package/dist/assets/src/execution/residentDepsStore.ts +40 -2
- package/dist/assets/src/execution/residentRefresh.ts +55 -3
- package/dist/assets/src/execution/residentSteps.ts +4 -0
- package/dist/assets/src/execution/sandboxErrors.ts +8 -0
- package/dist/assets/web/dist/.vite/manifest.json +58 -52
- package/dist/assets/web/dist/assets/DeliveryPage-3ELQWM0r.js +1 -0
- package/dist/assets/web/dist/assets/{HomePage-AnycA57D.js → HomePage-BG_ok-K2.js} +2 -2
- package/dist/assets/web/dist/assets/{PendingTurnRow-BuRre8it.js → PendingTurnRow-ChCQOLgZ.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-Cb3sFkfj.js → ResidentDetailPage-C9y3nbo8.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-BZ6n6UxF.js → ResidentsIndexPage-i1RG9e7g.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-D3wVpzBa.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-B3IirUVi.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DiFmtGaJ.js +1 -0
- package/dist/assets/web/dist/assets/{ScheduledPage-CBUxbeqN.js → ScheduledPage-DvYwM2TE.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-CBTnZ9Qv.js → SettingsPage-Bo6yCyXZ.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-BnRjWzFN.js → StatusDot-CAfS1AUi.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-Brge0wnd.js → Tooltip-tZoum_T-.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-o6sLju16.js → UnitRoutePage-BmdOHwNn.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +1 -0
- package/dist/assets/web/dist/assets/{dist-rgAhsmE-.js → dist-DfbEpHXR.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BT0cPVRw.js +1 -0
- package/dist/assets/web/dist/assets/{main-CeRuGONy.js → main-5Gm_1Gv8.js} +2 -2
- package/dist/assets/web/dist/assets/sseReplay-DmyMXfRC.js +11 -0
- package/dist/cli.js +3319 -1015
- package/package.json +1 -1
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
- package/dist/assets/web/dist/assets/DeliveryPage-CIfBiINK.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-BRXkjkgO.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-psSMI3fN.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-68YT_RWt.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-BmK74Vp1.js +0 -1
- package/dist/assets/web/dist/assets/sseReplay-DXC7kGbN.js +0 -9
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
// arrive in the input rather than from the agent registry.
|
|
31
31
|
|
|
32
32
|
import type { ShipRoundOutcome } from "../runEvents.js";
|
|
33
|
+
import { shows, type Verbosity } from "../verbosity.js";
|
|
33
34
|
import type { Handoff, HandoffLanded } from "./handoff.js";
|
|
34
35
|
import { progressOf, renderRenewal, renewalDecision, type PushedHeadFact, type RenewalDecision } from "./renewal.js";
|
|
35
36
|
import type { RunStatus } from "../runRecord.js";
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
carve,
|
|
52
53
|
DEFAULT_GRANT,
|
|
53
54
|
loopPosition,
|
|
55
|
+
MERGE_WAIT_ASK_MINUTES,
|
|
54
56
|
MINUTE_MS,
|
|
55
57
|
SHIP_WAIT,
|
|
56
58
|
type Carve,
|
|
@@ -430,8 +432,19 @@ export type Brief =
|
|
|
430
432
|
round: number;
|
|
431
433
|
/** The previous review round's run and the coding run that answered its findings, for a re-review. */
|
|
432
434
|
prior?: { reviewRunId: string; codingRunId?: string };
|
|
435
|
+
/** The previous round's check findings (record 0055): they sit on no
|
|
436
|
+
* run's record, so the brief carries them beside `prior` by value. */
|
|
437
|
+
checks?: Finding[];
|
|
433
438
|
}
|
|
434
|
-
| {
|
|
439
|
+
| {
|
|
440
|
+
kind: "findings";
|
|
441
|
+
unit: string;
|
|
442
|
+
pr: number;
|
|
443
|
+
reviewRunId: string;
|
|
444
|
+
/** The round's check findings (record 0055), carried by value — the
|
|
445
|
+
* review run's record holds only the reviewer's own findings. */
|
|
446
|
+
checks?: Finding[];
|
|
447
|
+
};
|
|
435
448
|
|
|
436
449
|
export interface PrRef {
|
|
437
450
|
number: number;
|
|
@@ -453,8 +466,21 @@ export type CoordinatorAction =
|
|
|
453
466
|
* submitted description when the record holds one) instead of answering
|
|
454
467
|
* `none` over stranded work. */
|
|
455
468
|
recover?: { runId: string };
|
|
469
|
+
/** The pull request the machine has adopted (`state.pr`), when it holds
|
|
470
|
+
* one: the child may have worked that pull request's own head branch,
|
|
471
|
+
* not the unit's (issue 1799), so when nothing heads the unit's branch
|
|
472
|
+
* the bot follows this number and answers the pull request's LIVE state
|
|
473
|
+
* — open at a fresh head, merged, or closed (`prClosed`) — never `none`
|
|
474
|
+
* over a record fact written minutes earlier. */
|
|
475
|
+
pr?: number;
|
|
456
476
|
}
|
|
457
477
|
| { type: "merge"; step: string; prNumber: number; headSha: string }
|
|
478
|
+
/** Read the check runs at the reviewed head (record 0055, the round verdict):
|
|
479
|
+
* the merge door's own reading, folded into the round. `retry` names the
|
|
480
|
+
* failed checks whose one flake re-run the machine is spending: the bot
|
|
481
|
+
* re-runs their failed jobs (the CI retry the deploy already uses) instead
|
|
482
|
+
* of reading. */
|
|
483
|
+
| { type: "checks"; step: string; prNumber: number; headSha: string; retry?: string[] }
|
|
458
484
|
/** Wait for the intake's checks-settled event at the approved head, bounded as the fallback. */
|
|
459
485
|
| { type: "wait-checks"; step: string; headSha: string; timeoutMs: number }
|
|
460
486
|
| { type: "sleep"; step: string; ms: number }
|
|
@@ -509,6 +535,10 @@ export type PrCheck =
|
|
|
509
535
|
* naming where the scope landed is the `already_landed` ending
|
|
510
536
|
* (agent-ship item 12); absent, the fact is unknown and never claimed. */
|
|
511
537
|
aheadOfBase?: number;
|
|
538
|
+
/** The action's followed pull request (`pr`) was verified CLOSED
|
|
539
|
+
* unmerged: the machine must not brief a review round on it. Absent when
|
|
540
|
+
* nothing was followed or the follow could not read the pull request. */
|
|
541
|
+
prClosed?: boolean;
|
|
512
542
|
}
|
|
513
543
|
| {
|
|
514
544
|
state: "open";
|
|
@@ -518,6 +548,12 @@ export type PrCheck =
|
|
|
518
548
|
autoMergeEnabled?: boolean;
|
|
519
549
|
/** The check runs at the head, when the read asked for them (the ending's facts). */
|
|
520
550
|
checks?: CommitChecksFacts;
|
|
551
|
+
/** GitHub's `mergeable_state`, when the read asked for the facts — `dirty`
|
|
552
|
+
* keeps the report from calling a conflicting head merge-ready (item 9). */
|
|
553
|
+
mergeableState?: string;
|
|
554
|
+
/** The head's self-declared fix-up commit subjects (`fixup!`/`squash!`/`amend!`),
|
|
555
|
+
* when the read asked for the facts — a head carrying one is not in the ready state. */
|
|
556
|
+
fixupCommits?: string[];
|
|
521
557
|
}
|
|
522
558
|
| { state: "merged"; prNumber: number; url: string; sha: string; mergedAt: string };
|
|
523
559
|
|
|
@@ -537,9 +573,56 @@ export type StepReturn =
|
|
|
537
573
|
// fired, or a person merged — so the runner merged nothing (`by: other`).
|
|
538
574
|
| { type: "merge"; step: string; outcome: "merged"; by: "other"; sha: string; mergedAt: string; at: number }
|
|
539
575
|
| { type: "merge"; step: string; outcome: "pending" | "refused"; reason: string; at: number }
|
|
576
|
+
/** The checks read at the reviewed head; `checks` absent means GitHub could
|
|
577
|
+
* not be read, which the machine treats as pending (record 0055). A retry
|
|
578
|
+
* ask answers `retried` instead: whether the bot dispatched the re-run —
|
|
579
|
+
* false means there is nothing to wait for at the unchanged head. */
|
|
580
|
+
| { type: "checks"; step: string; checks?: RoundChecks; retried?: boolean; at: number }
|
|
540
581
|
| { type: "wait-checks"; step: string; outcome: "event" | "timeout" }
|
|
541
582
|
| { type: "sleep"; step: string };
|
|
542
583
|
|
|
584
|
+
/** One failed check run at the reviewed head, as the round's checks step reads
|
|
585
|
+
* it (record 0055): the name, GitHub's conclusion, the run's URL, and whether
|
|
586
|
+
* the bot's classifier suspects a flake — a test timeout or runner stall on a
|
|
587
|
+
* shard whose test files the pull request's changed paths never touch. */
|
|
588
|
+
export interface CheckFailure {
|
|
589
|
+
name: string;
|
|
590
|
+
conclusion: string;
|
|
591
|
+
url?: string;
|
|
592
|
+
flakeSuspect?: boolean;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** The check runs at the reviewed head as the round's checks step reads them. */
|
|
596
|
+
export interface RoundChecks {
|
|
597
|
+
total: number;
|
|
598
|
+
pending: string[];
|
|
599
|
+
failed: CheckFailure[];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** A failed check as a finding of the round (record 0055): a finding row like
|
|
603
|
+
* a reviewer's — id `check:<name>`, the check's name as the file, severity
|
|
604
|
+
* `blocking` so the level in force always counts it, the conclusion and URL
|
|
605
|
+
* as the title — so the ledger, the findings brief and the dispositions
|
|
606
|
+
* carry it exactly as a reviewer's. */
|
|
607
|
+
export function checkFinding(f: CheckFailure): Finding {
|
|
608
|
+
return {
|
|
609
|
+
id: `check:${f.name}`,
|
|
610
|
+
severity: "blocking",
|
|
611
|
+
file: f.name,
|
|
612
|
+
title: `CI check failed (${f.conclusion})${f.url !== undefined ? ` — ${f.url}` : ""}`,
|
|
613
|
+
check: true,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** The check findings of a review round: the rows the checks step appended to
|
|
618
|
+
* the round's findings, told apart by provenance — the `check` flag only
|
|
619
|
+
* `checkFinding` sets, never the id: a reviewer's id is a free string, and a
|
|
620
|
+
* re-review brief names the prior check findings by id, so a reviewer
|
|
621
|
+
* plausibly reuses `check:…` in its own verdict — a check finding sits on no
|
|
622
|
+
* run's record, so a brief carries them by value. */
|
|
623
|
+
export const checkFindingsOf = (findings: readonly Finding[] | undefined): Finding[] =>
|
|
624
|
+
(findings ?? []).filter((f) => f.check === true);
|
|
625
|
+
|
|
543
626
|
/** How one unit's pipeline ended — the truthful vocabulary the ship pipeline
|
|
544
627
|
* has, plus the merge's own: `merged` by the runner, or found merged (`by:
|
|
545
628
|
* other` — a person's merge, or an earlier attempt's that died after it, so
|
|
@@ -687,6 +770,9 @@ export interface UnitPipelineInput {
|
|
|
687
770
|
* it yet: the renewal decision is the segment's end, not this unit's. */
|
|
688
771
|
grant?: Grant;
|
|
689
772
|
grantSource?: GrantSource;
|
|
773
|
+
/** The request's verbosity (routing-and-config item 28): the level the
|
|
774
|
+
* thread's copy of the unit report is rendered at; absent reads as quiet. */
|
|
775
|
+
verbosity?: Verbosity;
|
|
690
776
|
/** The segment this pipeline runs (decision 0046, Renewal): absent for the
|
|
691
777
|
* first; a renewal's carries its number, the renewals spent before it, the
|
|
692
778
|
* session's spend so far, the sha it continues from and the previous
|
|
@@ -704,6 +790,10 @@ export interface UnitPipelineInput {
|
|
|
704
790
|
* pre-check finds the open pull request still at exactly this head, there is
|
|
705
791
|
* nothing to code and the attempt starts at the review round. */
|
|
706
792
|
lastPush?: string;
|
|
793
|
+
/** The runs page base (`<PUBLIC_BASE_URL>/runs`), the plan route's answer:
|
|
794
|
+
* the report's pointer at a child's write-up links its run page with it and
|
|
795
|
+
* names the run id without it — the machine never reads an environment. */
|
|
796
|
+
runPageBase?: string;
|
|
707
797
|
}
|
|
708
798
|
|
|
709
799
|
type Phase =
|
|
@@ -732,6 +822,36 @@ type Phase =
|
|
|
732
822
|
}
|
|
733
823
|
| { at: "merge"; pr: PrRef; headSha: string; n: number; since: number; waitMs: number }
|
|
734
824
|
| { at: "merge-wait"; pr: PrRef; headSha: string; n: number; since: number; waitMs: number }
|
|
825
|
+
/** The round's checks step (record 0055): the check runs at the reviewed head
|
|
826
|
+
* are read after an approve settles, before merge_ready or the merge door.
|
|
827
|
+
* `graced`: a head with no check reported has had its one-chunk grace;
|
|
828
|
+
* `retried`: the one flake re-run is spent; `retry` names the failed checks
|
|
829
|
+
* the next ask re-runs instead of reading. */
|
|
830
|
+
| {
|
|
831
|
+
at: "checks";
|
|
832
|
+
round: RoundRef;
|
|
833
|
+
prNumber: number;
|
|
834
|
+
headSha: string;
|
|
835
|
+
n: number;
|
|
836
|
+
since: number;
|
|
837
|
+
waitMs: number;
|
|
838
|
+
graced: boolean;
|
|
839
|
+
retried: boolean;
|
|
840
|
+
retry?: string[];
|
|
841
|
+
}
|
|
842
|
+
/** Waiting on `checks-settled-<head>` between two checks reads, in the merge
|
|
843
|
+
* wait's own chunks — the intake wakes the machine when the head settles. */
|
|
844
|
+
| {
|
|
845
|
+
at: "checks-wait";
|
|
846
|
+
round: RoundRef;
|
|
847
|
+
prNumber: number;
|
|
848
|
+
headSha: string;
|
|
849
|
+
n: number;
|
|
850
|
+
since: number;
|
|
851
|
+
waitMs: number;
|
|
852
|
+
graced: boolean;
|
|
853
|
+
retried: boolean;
|
|
854
|
+
}
|
|
735
855
|
| { at: "ended" };
|
|
736
856
|
|
|
737
857
|
export interface UnitPipelineState {
|
|
@@ -864,9 +984,19 @@ function briefFor(s: UnitPipelineState, round: RoundRef): Brief {
|
|
|
864
984
|
};
|
|
865
985
|
}
|
|
866
986
|
const pr = s.pr!.number;
|
|
867
|
-
if (round.kind === "findings")
|
|
987
|
+
if (round.kind === "findings") {
|
|
988
|
+
const checks = checkFindingsOf(s.findingsByRound[round.index]);
|
|
989
|
+
return {
|
|
990
|
+
kind: "findings",
|
|
991
|
+
unit,
|
|
992
|
+
pr,
|
|
993
|
+
reviewRunId: s.reviewRunByRound[round.index]!,
|
|
994
|
+
...(checks.length > 0 ? { checks } : {}),
|
|
995
|
+
};
|
|
996
|
+
}
|
|
868
997
|
const priorReview = s.reviewRunByRound[round.index - 1];
|
|
869
998
|
const priorCoding = s.findingsRunByRound[round.index - 1];
|
|
999
|
+
const priorChecks = checkFindingsOf(s.findingsByRound[round.index - 1]);
|
|
870
1000
|
return {
|
|
871
1001
|
kind: "review",
|
|
872
1002
|
unit,
|
|
@@ -876,6 +1006,7 @@ function briefFor(s: UnitPipelineState, round: RoundRef): Brief {
|
|
|
876
1006
|
...(priorReview !== undefined
|
|
877
1007
|
? { prior: { reviewRunId: priorReview, ...(priorCoding !== undefined ? { codingRunId: priorCoding } : {}) } }
|
|
878
1008
|
: {}),
|
|
1009
|
+
...(priorChecks.length > 0 ? { checks: priorChecks } : {}),
|
|
879
1010
|
};
|
|
880
1011
|
}
|
|
881
1012
|
|
|
@@ -935,6 +1066,24 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
935
1066
|
type: "pr-check",
|
|
936
1067
|
step: `${roundStep(s, p.round)}/pr-check`,
|
|
937
1068
|
...(p.dead !== undefined ? { recover: { runId: p.runId } } : {}),
|
|
1069
|
+
// The adopted pull request rides the check so the bot can follow it
|
|
1070
|
+
// when nothing heads the unit's branch (issue 1799).
|
|
1071
|
+
...(s.pr !== undefined ? { pr: s.pr.number } : {}),
|
|
1072
|
+
};
|
|
1073
|
+
case "checks":
|
|
1074
|
+
return {
|
|
1075
|
+
type: "checks",
|
|
1076
|
+
step: `${roundStep(s, p.round)}/checks/${p.n}`,
|
|
1077
|
+
prNumber: p.prNumber,
|
|
1078
|
+
headSha: p.headSha,
|
|
1079
|
+
...(p.retry !== undefined ? { retry: p.retry } : {}),
|
|
1080
|
+
};
|
|
1081
|
+
case "checks-wait":
|
|
1082
|
+
return {
|
|
1083
|
+
type: "wait-checks",
|
|
1084
|
+
step: `${roundStep(s, p.round)}/checks/wait/${p.n}`,
|
|
1085
|
+
headSha: p.headSha,
|
|
1086
|
+
timeoutMs: Math.max(MIN, Math.min(MERGE_WAIT_CHUNK_MS, p.waitMs - (s.clock - p.since))),
|
|
938
1087
|
};
|
|
939
1088
|
case "merge":
|
|
940
1089
|
return { type: "merge", step: `${unit}/merge/${p.n}`, prNumber: p.pr.number, headSha: p.headSha };
|
|
@@ -1174,6 +1323,163 @@ function settleReview(
|
|
|
1174
1323
|
);
|
|
1175
1324
|
return enterRound(next, { index: round.index, kind: "findings" }, notes);
|
|
1176
1325
|
}
|
|
1326
|
+
// The round verdict (record 0055): the approve is joined with the check
|
|
1327
|
+
// runs at the reviewed head before merge_ready or the merge door — the
|
|
1328
|
+
// checks step reads them and a failed one becomes a finding of the round.
|
|
1329
|
+
return enterChecks(next, round, notes);
|
|
1330
|
+
}
|
|
1331
|
+
// request_changes: the verdict settled (and posted) — now a stop
|
|
1332
|
+
// short-circuits the findings step, naming the review standing on the pull request.
|
|
1333
|
+
if (mode !== undefined)
|
|
1334
|
+
return end(
|
|
1335
|
+
next,
|
|
1336
|
+
{
|
|
1337
|
+
kind: "stopped",
|
|
1338
|
+
mode,
|
|
1339
|
+
round,
|
|
1340
|
+
reviewRounds: next.reviewRounds,
|
|
1341
|
+
...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
|
|
1342
|
+
...(facts.reviewPosted === true ? { postedReview: true } : {}),
|
|
1343
|
+
},
|
|
1344
|
+
notes,
|
|
1345
|
+
);
|
|
1346
|
+
if (next.reviewRounds >= next.input.caps.maxRounds)
|
|
1347
|
+
return end(
|
|
1348
|
+
next,
|
|
1349
|
+
{ kind: "round_cap", maxRounds: next.input.caps.maxRounds, reviewRounds: next.reviewRounds },
|
|
1350
|
+
notes,
|
|
1351
|
+
);
|
|
1352
|
+
// The findings step (agent-ship item 7): the review's findings dispatched into
|
|
1353
|
+
// the unit thread as a coding run, under the review round's index. A run live
|
|
1354
|
+
// there is a person's (this machine awaited its own child's end), so the
|
|
1355
|
+
// spawn answers busy and the step waits under the unit's clock like any round.
|
|
1356
|
+
return enterRound(next, { index: round.index, kind: "findings" }, notes);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/** The round's checks step (record 0055): after an approve settles, the check
|
|
1360
|
+
* runs at the reviewed head are read with the merge door's own reading and
|
|
1361
|
+
* folded into the round verdict. With no reviewed head known the step is
|
|
1362
|
+
* skipped as the merge step's guard is, and the unit ends as today. The wait
|
|
1363
|
+
* on a pending head is bounded by the merge wait's own ask, clipped to the
|
|
1364
|
+
* pipeline's remainder. */
|
|
1365
|
+
function enterChecks(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNote[]): Transition {
|
|
1366
|
+
const headSha = normalizeHead(s.lastReviewHead);
|
|
1367
|
+
const pr = s.pr;
|
|
1368
|
+
if (headSha === undefined || pr === undefined) return approveOutcome(s, notes);
|
|
1369
|
+
const waitMs = Math.max(MIN, Math.min(MERGE_WAIT_ASK_MINUTES * MIN, remainingMs(s)));
|
|
1370
|
+
return {
|
|
1371
|
+
state: {
|
|
1372
|
+
...s,
|
|
1373
|
+
phase: {
|
|
1374
|
+
at: "checks",
|
|
1375
|
+
round,
|
|
1376
|
+
prNumber: pr.number,
|
|
1377
|
+
headSha,
|
|
1378
|
+
n: 1,
|
|
1379
|
+
since: s.clock,
|
|
1380
|
+
waitMs,
|
|
1381
|
+
graced: false,
|
|
1382
|
+
retried: false,
|
|
1383
|
+
},
|
|
1384
|
+
},
|
|
1385
|
+
notes,
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/** What the checks step answered, folded into the round (record 0055). Pure
|
|
1390
|
+
* over the phase: a retry ask waits for the head to settle and reads again;
|
|
1391
|
+
* an unreadable or pending head waits a chunk inside the step's ask and then
|
|
1392
|
+
* proceeds — the ending's facts read names what is still pending; a head with
|
|
1393
|
+
* no check reported waits one chunk of grace and never more; a failed check
|
|
1394
|
+
* becomes a check finding under a round note of its own and the findings step
|
|
1395
|
+
* runs as for any changes-requested round — never merge_ready, never the
|
|
1396
|
+
* merge door; a green head proceeds with no wait added. */
|
|
1397
|
+
function settleChecks(
|
|
1398
|
+
s: UnitPipelineState,
|
|
1399
|
+
p: Extract<Phase, { at: "checks" }>,
|
|
1400
|
+
checks: RoundChecks | undefined,
|
|
1401
|
+
retried?: boolean,
|
|
1402
|
+
): Transition {
|
|
1403
|
+
const { round } = p;
|
|
1404
|
+
const wait = (over: Partial<Extract<Phase, { at: "checks-wait" }>>): Transition => ({
|
|
1405
|
+
state: {
|
|
1406
|
+
...s,
|
|
1407
|
+
phase: {
|
|
1408
|
+
at: "checks-wait",
|
|
1409
|
+
round,
|
|
1410
|
+
prNumber: p.prNumber,
|
|
1411
|
+
headSha: p.headSha,
|
|
1412
|
+
n: p.n,
|
|
1413
|
+
since: p.since,
|
|
1414
|
+
waitMs: p.waitMs,
|
|
1415
|
+
graced: p.graced,
|
|
1416
|
+
retried: p.retried,
|
|
1417
|
+
...over,
|
|
1418
|
+
},
|
|
1419
|
+
},
|
|
1420
|
+
notes: [],
|
|
1421
|
+
});
|
|
1422
|
+
// The retry ask. Dispatched: wait for the head to settle, then read again;
|
|
1423
|
+
// a second failure is the finding. NOT dispatched (`retried: false` — no
|
|
1424
|
+
// re-runnable run behind the checks, or GitHub refused): the one re-run is
|
|
1425
|
+
// spent all the same — the machine never asks twice — but nothing changes
|
|
1426
|
+
// at the head, so the checks are read again at once instead of waiting on a
|
|
1427
|
+
// settle event that never comes, and the failure becomes the finding.
|
|
1428
|
+
if (p.retry !== undefined) {
|
|
1429
|
+
if (retried === false) {
|
|
1430
|
+
const { retry: _retry, ...read } = p;
|
|
1431
|
+
return { state: { ...s, phase: { ...read, n: p.n + 1, retried: true } }, notes: [] };
|
|
1432
|
+
}
|
|
1433
|
+
return wait({ retried: true });
|
|
1434
|
+
}
|
|
1435
|
+
// GitHub unreadable answers as pending and is re-read at the chunk's end
|
|
1436
|
+
// (record 0055); a head still pending or unreadable at the ask's end
|
|
1437
|
+
// proceeds — the ending's facts read names what is still pending, and the
|
|
1438
|
+
// merge door (under `merge: runner`) is the guard that never merges over it.
|
|
1439
|
+
if (checks === undefined || checks.pending.length > 0) {
|
|
1440
|
+
if (s.clock - p.since >= p.waitMs) return approveOutcome(s, []);
|
|
1441
|
+
return wait({});
|
|
1442
|
+
}
|
|
1443
|
+
if (checks.failed.length > 0) {
|
|
1444
|
+
// The flake rule (record 0055): a suspected flake — a test timeout or
|
|
1445
|
+
// runner stall on a shard whose test files the pull request's changed
|
|
1446
|
+
// paths never touch — is re-run once before it becomes a finding. One
|
|
1447
|
+
// real failure among them makes the round's answer already known, so the
|
|
1448
|
+
// re-run is spent only when every failure is a suspect.
|
|
1449
|
+
if (!p.retried && checks.failed.every((f) => f.flakeSuspect === true))
|
|
1450
|
+
return {
|
|
1451
|
+
state: { ...s, phase: { ...p, n: p.n + 1, retry: checks.failed.map((f) => f.name) } },
|
|
1452
|
+
notes: [],
|
|
1453
|
+
};
|
|
1454
|
+
const findings = [...(s.findingsByRound[round.index] ?? []), ...checks.failed.map(checkFinding)];
|
|
1455
|
+
const next: UnitPipelineState = {
|
|
1456
|
+
...s,
|
|
1457
|
+
findingsByRound: { ...s.findingsByRound, [round.index]: findings },
|
|
1458
|
+
};
|
|
1459
|
+
// The failed checks get a round note of their own (record 0055) — never
|
|
1460
|
+
// the parser-mismatch gate — and the findings step runs as for any
|
|
1461
|
+
// changes-requested round: dispositions, a fix round, a re-review.
|
|
1462
|
+
const notes: CoordinatorNote[] = [roundNote(round, "checks_failed")];
|
|
1463
|
+
if (next.reviewRounds >= next.input.caps.maxRounds)
|
|
1464
|
+
return end(
|
|
1465
|
+
next,
|
|
1466
|
+
{ kind: "round_cap", maxRounds: next.input.caps.maxRounds, reviewRounds: next.reviewRounds },
|
|
1467
|
+
notes,
|
|
1468
|
+
);
|
|
1469
|
+
return enterRound(next, { index: round.index, kind: "findings" }, notes);
|
|
1470
|
+
}
|
|
1471
|
+
// No check reported at the head: one chunk of grace — the first check starts
|
|
1472
|
+
// within minutes where CI exists — then the round proceeds, so a repository
|
|
1473
|
+
// without CI costs one chunk per round and never idles (record 0055).
|
|
1474
|
+
if (checks.total === 0 && !p.graced) return wait({ graced: true });
|
|
1475
|
+
// Green (or none reported after the grace): today's path, no wait added.
|
|
1476
|
+
return approveOutcome(s, []);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/** An approve past the checks read: merge_ready for a person, the merge door
|
|
1480
|
+
* under `merge: runner` — the tail the round verdict guards (record 0055). */
|
|
1481
|
+
function approveOutcome(next: UnitPipelineState, notes: CoordinatorNote[]): Transition {
|
|
1482
|
+
{
|
|
1177
1483
|
const pr = next.pr!;
|
|
1178
1484
|
if (next.input.merge !== "runner")
|
|
1179
1485
|
return end(next, { kind: "merge_ready", pr, reviewRounds: next.reviewRounds }, notes);
|
|
@@ -1207,32 +1513,6 @@ function settleReview(
|
|
|
1207
1513
|
notes,
|
|
1208
1514
|
};
|
|
1209
1515
|
}
|
|
1210
|
-
// request_changes: the verdict settled (and posted) — now a stop
|
|
1211
|
-
// short-circuits the findings step, naming the review standing on the pull request.
|
|
1212
|
-
if (mode !== undefined)
|
|
1213
|
-
return end(
|
|
1214
|
-
next,
|
|
1215
|
-
{
|
|
1216
|
-
kind: "stopped",
|
|
1217
|
-
mode,
|
|
1218
|
-
round,
|
|
1219
|
-
reviewRounds: next.reviewRounds,
|
|
1220
|
-
...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
|
|
1221
|
-
...(facts.reviewPosted === true ? { postedReview: true } : {}),
|
|
1222
|
-
},
|
|
1223
|
-
notes,
|
|
1224
|
-
);
|
|
1225
|
-
if (next.reviewRounds >= next.input.caps.maxRounds)
|
|
1226
|
-
return end(
|
|
1227
|
-
next,
|
|
1228
|
-
{ kind: "round_cap", maxRounds: next.input.caps.maxRounds, reviewRounds: next.reviewRounds },
|
|
1229
|
-
notes,
|
|
1230
|
-
);
|
|
1231
|
-
// The findings step (agent-ship item 7): the review's findings dispatched into
|
|
1232
|
-
// the unit thread as a coding run, under the review round's index. A run live
|
|
1233
|
-
// there is a person's (this machine awaited its own child's end), so the
|
|
1234
|
-
// spawn answers busy and the step waits under the unit's clock like any round.
|
|
1235
|
-
return enterRound(next, { index: round.index, kind: "findings" }, notes);
|
|
1236
1516
|
}
|
|
1237
1517
|
|
|
1238
1518
|
/** The unit's ending when its pull request is found merged — by a person, or
|
|
@@ -1286,6 +1566,22 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
1286
1566
|
[roundNote(round, "aborted")],
|
|
1287
1567
|
);
|
|
1288
1568
|
}
|
|
1569
|
+
// Nothing heads the unit's branch, but the machine holds the round's pull
|
|
1570
|
+
// request (`pr_opened` off the child's record, or an earlier round's
|
|
1571
|
+
// adoption): the child worked on that pull request's own head branch, not
|
|
1572
|
+
// the unit's — a re-issued task in the thread of an existing pull request
|
|
1573
|
+
// (issue 1799). The round HAS its pull request — at round 0 and after a
|
|
1574
|
+
// findings step alike, since the same child keeps repushing that branch
|
|
1575
|
+
// through every later round — so it carries on to the (re-)review at the
|
|
1576
|
+
// head the child pushed through the shared open settle (a findings step
|
|
1577
|
+
// that repushed nothing keeps its abort, item 7), instead of the unit
|
|
1578
|
+
// ending "no pull request" or "closed out from under" over work that
|
|
1579
|
+
// stands. Never over a followed answer that VERIFIED the pull request
|
|
1580
|
+
// closed (`prClosed`): a review briefed on a closed pull request reviews
|
|
1581
|
+
// nothing, so the endings below stand — and are then truthful. Dead
|
|
1582
|
+
// children never reach here: their endings are above.
|
|
1583
|
+
if (s.pr !== undefined && pr.prClosed !== true)
|
|
1584
|
+
return roundOnOpenPr(s, phase, { prNumber: s.pr.number, url: s.pr.url });
|
|
1289
1585
|
if (round.index === 0) {
|
|
1290
1586
|
// The scope already landed (agent-ship item 12): the child's handoff
|
|
1291
1587
|
// names where, and the branch carries no commits over the base — two
|
|
@@ -1370,6 +1666,18 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
1370
1666
|
[roundNote(round, "aborted")],
|
|
1371
1667
|
);
|
|
1372
1668
|
}
|
|
1669
|
+
return roundOnOpenPr(s, phase, pr);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/** The round carried on its open pull request: the pr-check's `open` answer,
|
|
1673
|
+
* or — when nothing heads the unit's branch — the pull request the machine
|
|
1674
|
+
* already holds (issue 1799), at the head the child pushed. */
|
|
1675
|
+
function roundOnOpenPr(
|
|
1676
|
+
s: UnitPipelineState,
|
|
1677
|
+
phase: Extract<Phase, { at: "pr-check" }>,
|
|
1678
|
+
pr: { prNumber: number; url: string; headSha?: string },
|
|
1679
|
+
): Transition {
|
|
1680
|
+
const { round } = phase;
|
|
1373
1681
|
const head = pr.headSha ?? phase.childHead;
|
|
1374
1682
|
const next: UnitPipelineState = { ...s, pr: { number: pr.prNumber, url: pr.url } };
|
|
1375
1683
|
if (round.kind === "findings") {
|
|
@@ -1581,6 +1889,29 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1581
1889
|
}
|
|
1582
1890
|
case "pr-check":
|
|
1583
1891
|
return settlePrCheck(clocked, p, (ret as Extract<StepReturn, { type: "pr-check" }>).pr);
|
|
1892
|
+
case "checks": {
|
|
1893
|
+
const r = ret as Extract<StepReturn, { type: "checks" }>;
|
|
1894
|
+
return settleChecks(clocked, p, r.checks, r.retried);
|
|
1895
|
+
}
|
|
1896
|
+
case "checks-wait":
|
|
1897
|
+
// The event fired or the chunk elapsed either way the head is read again.
|
|
1898
|
+
return {
|
|
1899
|
+
state: {
|
|
1900
|
+
...s,
|
|
1901
|
+
phase: {
|
|
1902
|
+
at: "checks",
|
|
1903
|
+
round: p.round,
|
|
1904
|
+
prNumber: p.prNumber,
|
|
1905
|
+
headSha: p.headSha,
|
|
1906
|
+
n: p.n + 1,
|
|
1907
|
+
since: p.since,
|
|
1908
|
+
waitMs: p.waitMs,
|
|
1909
|
+
graced: p.graced,
|
|
1910
|
+
retried: p.retried,
|
|
1911
|
+
},
|
|
1912
|
+
},
|
|
1913
|
+
notes: [],
|
|
1914
|
+
};
|
|
1584
1915
|
case "merge": {
|
|
1585
1916
|
const r = ret as Extract<StepReturn, { type: "merge" }>;
|
|
1586
1917
|
if (r.outcome === "merged")
|
|
@@ -1647,6 +1978,18 @@ function dispositionFor(s: UnitPipelineState, finding: Finding, round: number):
|
|
|
1647
1978
|
return undefined;
|
|
1648
1979
|
}
|
|
1649
1980
|
|
|
1981
|
+
/** Where a child's write-up lives, for the report's pointer (agent-ship item
|
|
1982
|
+
* 12): the child's own message in the thread is the single copy of the detail
|
|
1983
|
+
* — posted moments before the unit-end — so the report points at it instead
|
|
1984
|
+
* of repeating it: the run page when the plan named the base, the run id
|
|
1985
|
+
* otherwise. Undefined when no run is known to point at. */
|
|
1986
|
+
function writeUpPointer(s: UnitPipelineState, kind: RoundKind, runId: string | undefined): string | undefined {
|
|
1987
|
+
if (runId === undefined) return undefined;
|
|
1988
|
+
const base = s.input.runPageBase;
|
|
1989
|
+
const at = base !== undefined ? `${base.replace(/\/+$/, "")}/${encodeURIComponent(runId)}` : `run ${runId}`;
|
|
1990
|
+
return `The ${presetOf(kind)} child's write-up is its own message above in this thread — the single copy of the detail (${at}).`;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1650
1993
|
/** How the budget went, in the card's words: coding, review, waiting minutes. */
|
|
1651
1994
|
function budgetSplitLine(spent: ShipBudgetSpent, maxMinutes: number): string {
|
|
1652
1995
|
const min = (ms: number) => Math.round(ms / MIN);
|
|
@@ -1687,6 +2030,14 @@ export interface MergeReadyFacts {
|
|
|
1687
2030
|
merged?: { sha: string; mergedAt: string };
|
|
1688
2031
|
/** The check runs at the approved head as the merge door reads them (record 0055). */
|
|
1689
2032
|
checks?: CommitChecksFacts;
|
|
2033
|
+
/** GitHub's `mergeable_state` for the pull request — `dirty` is a conflict
|
|
2034
|
+
* with the base, and the report says so instead of "merge-ready", exactly
|
|
2035
|
+
* as the merge door refuses it before reading the checks (agent-ship item 9). */
|
|
2036
|
+
mergeableState?: string;
|
|
2037
|
+
/** The subjects of the head's commits that mark themselves as fix-ups
|
|
2038
|
+
* (git's autosquash prefixes `fixup!`, `squash!`, `amend!`): a head
|
|
2039
|
+
* carrying one is by its own words not in the ready state. */
|
|
2040
|
+
fixupCommits?: string[];
|
|
1690
2041
|
}
|
|
1691
2042
|
|
|
1692
2043
|
/** The check runs at one commit: how many, which still run, which failed. */
|
|
@@ -1697,9 +2048,19 @@ export interface CommitChecksFacts {
|
|
|
1697
2048
|
}
|
|
1698
2049
|
|
|
1699
2050
|
/** The merge-ready report's headline is a claim about the approved head
|
|
1700
|
-
* (record 0055): a
|
|
1701
|
-
*
|
|
1702
|
-
|
|
2051
|
+
* (record 0055, agent-ship item 9): a head that conflicts with its base is
|
|
2052
|
+
* never called merge-ready and neither is one carrying an unsquashed fix-up
|
|
2053
|
+
* commit — the ready state is read beside the checks, in the merge door's
|
|
2054
|
+
* order (the conflict first) — then a failed check is never called
|
|
2055
|
+
* merge-ready, a pending one is named, green is said, and without any fact
|
|
2056
|
+
* the line is unchanged. */
|
|
2057
|
+
function mergeReadyHeadline(rounds: string, url: string, base: string, facts: MergeReadyFacts | undefined): string {
|
|
2058
|
+
if (facts?.mergeableState === "dirty")
|
|
2059
|
+
return `⚠️ Approved but not merge-ready after ${rounds}: ${url} — the head conflicts with \`${base}\`: rebase onto \`${base}\`, push, and re-review. The approved work stands.`;
|
|
2060
|
+
const fixups = facts?.fixupCommits ?? [];
|
|
2061
|
+
if (fixups.length > 0)
|
|
2062
|
+
return `⚠️ Approved but not merge-ready after ${rounds}: ${url} — ${fixups.length} unsquashed fix-up commit${fixups.length === 1 ? "" : "s"} on the head (${fixups.join("; ")}): squash into the unit's commit, push, and re-review.`;
|
|
2063
|
+
const checks = facts?.checks;
|
|
1703
2064
|
if (checks === undefined) return `✅ Merge-ready after ${rounds}: ${url}`;
|
|
1704
2065
|
if (checks.failed.length > 0) {
|
|
1705
2066
|
const pending = checks.pending.length > 0 ? `; pending: ${checks.pending.join(", ")}` : "";
|
|
@@ -1711,9 +2072,21 @@ function mergeReadyHeadline(rounds: string, url: string, checks: CommitChecksFac
|
|
|
1711
2072
|
return `✅ Merge-ready after ${rounds}: ${url} — ${checks.total} check${checks.total === 1 ? "" : "s"} green at the approved head.`;
|
|
1712
2073
|
}
|
|
1713
2074
|
|
|
1714
|
-
export function renderUnitReport(
|
|
2075
|
+
export function renderUnitReport(
|
|
2076
|
+
s: UnitPipelineState,
|
|
2077
|
+
facts?: MergeReadyFacts,
|
|
2078
|
+
/** The level the report speaks at (routing-and-config item 28). The default
|
|
2079
|
+
* is the full report — the row's and the board's copy; the thread's copy is
|
|
2080
|
+
* rendered at the request's level, where the asides about the machinery
|
|
2081
|
+
* (the level in force, the grant, the write-up pointer, the budget split,
|
|
2082
|
+
* a segment boundary) are `verbose` and the outcome, the verdict, the
|
|
2083
|
+
* findings left below the gate, the declined ones and what to do next are
|
|
2084
|
+
* everyone's. */
|
|
2085
|
+
verbosity: Verbosity = "verbose",
|
|
2086
|
+
): string {
|
|
1715
2087
|
const e = s.ending;
|
|
1716
2088
|
if (!e) return "";
|
|
2089
|
+
const aside = (line: string | undefined) => (shows(verbosity, "verbose") ? line : undefined);
|
|
1717
2090
|
const rounds = `${e.reviewRounds} review round${e.reviewRounds === 1 ? "" : "s"}`;
|
|
1718
2091
|
const prUrl = s.pr?.url;
|
|
1719
2092
|
const prLine = prUrl ? ` PR: ${prUrl}` : "";
|
|
@@ -1751,11 +2124,13 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1751
2124
|
return [
|
|
1752
2125
|
`✅ Merged after ${rounds}: ${e.pr.url} (squash \`${e.sha.slice(0, 7)}\`) — merged by the plan runner under \`plan:merge\`: the review approved at this head and the guards were green.`,
|
|
1753
2126
|
verdictLine,
|
|
1754
|
-
levelLine,
|
|
1755
|
-
grantLine,
|
|
1756
|
-
|
|
2127
|
+
aside(levelLine),
|
|
2128
|
+
aside(grantLine),
|
|
2129
|
+
skippedLine,
|
|
1757
2130
|
declinedLine,
|
|
1758
|
-
]
|
|
2131
|
+
]
|
|
2132
|
+
.filter(Boolean)
|
|
2133
|
+
.join("\n");
|
|
1759
2134
|
case "already_landed":
|
|
1760
2135
|
// No compare link, no renewal line, no re-issue prompt: there was
|
|
1761
2136
|
// nothing to ship, so none of them has a question to answer.
|
|
@@ -1765,11 +2140,11 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1765
2140
|
// A merge that already happened outranks the checks: there is no head left to gate.
|
|
1766
2141
|
facts?.merged
|
|
1767
2142
|
? `✅ Merge-ready after ${rounds}: ${e.pr.url}`
|
|
1768
|
-
: mergeReadyHeadline(rounds, e.pr.url, facts
|
|
2143
|
+
: mergeReadyHeadline(rounds, e.pr.url, s.input.base, facts),
|
|
1769
2144
|
verdictLine,
|
|
1770
|
-
levelLine,
|
|
1771
|
-
grantLine,
|
|
1772
|
-
|
|
2145
|
+
aside(levelLine),
|
|
2146
|
+
aside(grantLine),
|
|
2147
|
+
skippedLine,
|
|
1773
2148
|
declinedLine,
|
|
1774
2149
|
// What the driver read at the approved head when it composed this
|
|
1775
2150
|
// ending (agent-ship item 9): a merge that already happened is named
|
|
@@ -1779,7 +2154,9 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1779
2154
|
: facts?.autoMergeEnabled
|
|
1780
2155
|
? "Auto-merge is on for this pull request: the approval merges it once checks pass."
|
|
1781
2156
|
: "Remaining gate: a person's merge — the runner merges only when the instance's `merge` field says runner, and ship never approves.",
|
|
1782
|
-
]
|
|
2157
|
+
]
|
|
2158
|
+
.filter(Boolean)
|
|
2159
|
+
.join("\n");
|
|
1783
2160
|
case "merge_refused":
|
|
1784
2161
|
// The approved work is on the branch, so the remedy is a person's hand
|
|
1785
2162
|
// merge, never a re-run: a seeded plan re-issued afterwards finds the
|
|
@@ -1802,20 +2179,26 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1802
2179
|
case "wall_clock_cap":
|
|
1803
2180
|
return join([
|
|
1804
2181
|
`🧢 Ship stopped at a cap: the remaining pipeline time (~${Math.max(0, Math.round(e.remainingMs / MIN))} min of the ${s.input.caps.maxMinutes}-minute budget) cannot hold another round${e.refused ? ` (the ${e.refused.round} round would get ${e.refused.minutes} min, under its floor of ${e.refused.floor})` : ""} — no approval after ${rounds}.${prLine}`,
|
|
1805
|
-
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
2182
|
+
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
1806
2183
|
splitReport(s),
|
|
1807
2184
|
reissue,
|
|
1808
2185
|
]);
|
|
1809
2186
|
case "review_pending":
|
|
1810
2187
|
return join([
|
|
1811
2188
|
`⏳ Review pending: the coding child shipped ${e.pr.url}${e.headSha !== undefined ? ` (head \`${e.headSha.slice(0, 7)}\`)` : ""} but the remaining pipeline time cannot hold the review round — the work stands, only the review is missing. The next attempt starts at the review round while the pull request still heads at the child's own last push.`,
|
|
1812
|
-
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
2189
|
+
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
1813
2190
|
reissue,
|
|
1814
2191
|
]);
|
|
1815
2192
|
case "stopped":
|
|
1816
2193
|
return join([
|
|
1817
2194
|
`${e.mode === "hard" ? "⛔" : "⏹"} Ship stopped by operator (${e.mode} stop) after ${rounds}.${prLine}`,
|
|
1818
|
-
|
|
2195
|
+
aside(
|
|
2196
|
+
writeUpPointer(
|
|
2197
|
+
s,
|
|
2198
|
+
e.round.kind,
|
|
2199
|
+
e.round.kind === "review" ? s.reviewRunByRound[e.round.index] : s.lastCodingRunId,
|
|
2200
|
+
),
|
|
2201
|
+
),
|
|
1819
2202
|
e.postedReview
|
|
1820
2203
|
? "ℹ️ A changes-requested review was posted this round before the stop — its findings stand on the PR."
|
|
1821
2204
|
: undefined,
|
|
@@ -1823,22 +2206,32 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1823
2206
|
]);
|
|
1824
2207
|
case "aborted":
|
|
1825
2208
|
return join([
|
|
1826
|
-
e.finalReply,
|
|
1827
2209
|
e.reason,
|
|
2210
|
+
aside(
|
|
2211
|
+
writeUpPointer(
|
|
2212
|
+
s,
|
|
2213
|
+
e.round?.kind ?? "coding",
|
|
2214
|
+
e.round?.kind === "review" ? s.reviewRunByRound[e.round.index] : s.lastCodingRunId,
|
|
2215
|
+
),
|
|
2216
|
+
),
|
|
1828
2217
|
e.renewal !== undefined ? `🔁 Not renewed: ${e.renewal.line}.` : undefined,
|
|
1829
2218
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
1830
2219
|
reissue,
|
|
1831
2220
|
]);
|
|
1832
2221
|
case "continued":
|
|
2222
|
+
// A segment boundary is the runner continuing — an acknowledgement, verbose
|
|
2223
|
+
// material (item 28); the thread's copy is empty at quiet, and the row keeps
|
|
2224
|
+
// no ending either way (decision 0046).
|
|
2225
|
+
if (!shows(verbosity, "verbose")) return "";
|
|
1833
2226
|
return join([
|
|
1834
|
-
e.
|
|
2227
|
+
writeUpPointer(s, e.round.kind, e.runId),
|
|
1835
2228
|
`🔁 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` : ""}.`,
|
|
1836
|
-
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
2229
|
+
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
1837
2230
|
]);
|
|
1838
2231
|
case "no_verdict":
|
|
1839
2232
|
return join([
|
|
1840
2233
|
`⚠️ 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.`,
|
|
1841
|
-
e.
|
|
2234
|
+
aside(writeUpPointer(s, e.round.kind, s.reviewRunByRound[e.round.index])),
|
|
1842
2235
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
1843
2236
|
reissue,
|
|
1844
2237
|
]);
|