@coreplane/switchboard 1.259.0 → 1.260.1
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 +17 -24
- package/dist/assets/deploy/cloudflare/worker.ts +17 -56
- package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +6 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +178 -27
- package/dist/assets/deploy/cloudflare-resident/worker.ts +74 -52
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +6 -3
- package/dist/assets/deploy/profile.example.json +2 -1
- 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 +3 -5
- package/dist/assets/src/core/authz/policy.ts +5 -0
- package/dist/assets/src/core/budgets.ts +19 -6
- package/dist/assets/src/core/coordinator/contract.ts +93 -5
- package/dist/assets/src/core/coordinator/driver.ts +395 -128
- package/dist/assets/src/core/memory/types.ts +5 -0
- package/dist/assets/src/core/pipelineStanding.ts +13 -4
- package/dist/assets/src/core/plane/decide.ts +53 -12
- package/dist/assets/src/core/provider.ts +5 -0
- package/dist/assets/src/core/runEvents.ts +22 -15
- package/dist/assets/src/core/runLedger/types.ts +3 -3
- package/dist/assets/src/core/runRecord.ts +64 -11
- package/dist/assets/src/core/ship/coordinator.ts +202 -53
- package/dist/assets/src/core/ship/renewal.ts +23 -8
- package/dist/assets/src/core/types.ts +11 -0
- package/dist/assets/src/deploy/liveGate.ts +8 -0
- package/dist/assets/src/deploy/profile.ts +8 -0
- package/dist/assets/src/deploy/restart.ts +49 -35
- package/dist/assets/web/dist/.vite/manifest.json +67 -67
- package/dist/assets/web/dist/assets/{DeliveryPage-DadntjSp.js → DeliveryPage-62qCSLvk.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-Dk3HRBc3.js +1 -0
- package/dist/assets/web/dist/assets/{PendingTurnRow-DBPMLVsm.js → PendingTurnRow-CeTGOXUy.js} +1 -1
- package/dist/assets/web/dist/assets/{PlanePage-DLusMGQ0.js → PlanePage-RD0M6O6W.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-CPnHnrxy.js → ResidentDetailPage-Dl2KId95.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-C9hASwHL.js → ResidentsIndexPage-DBy43I1R.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CwHDL-Mo.js +1 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-o1xzkTGZ.js → RunRoutePage-Du1HqLZs.js} +4 -4
- package/dist/assets/web/dist/assets/RunsIndexPage-BZphy3Hr.js +1 -0
- package/dist/assets/web/dist/assets/{ScheduledPage-BIdfC0V3.js → ScheduledPage-B7jh898t.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-CnqWAjl3.js → SettingsPage-3uLEsiiE.js} +1 -1
- package/dist/assets/web/dist/assets/{SilentTurn-DGNg0sIZ.js → SilentTurn-CKxSxnQC.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-D_D_iJS8.js → StatusDot-x7vcK0iE.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DVFmM7on.js → Tooltip-wVXCWhMp.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-7fAXFoB_.js → UnitRoutePage-V5BXK7FH.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-KNNkT4PZ.js +1 -0
- package/dist/assets/web/dist/assets/{dist-C1VX-_mm.js → dist-C-XGnvM4.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-BO_GIdF6.js → indexRow-Dxg7C8s9.js} +1 -1
- package/dist/assets/web/dist/assets/{main-Cl7xHG34.js → main-DAL--hyj.js} +2 -2
- package/dist/assets/web/dist/assets/sseReplay-BqgOggyQ.js +11 -0
- package/dist/cli.js +9136 -8197
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/HomePage-BEEpTnVT.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-OPe7Z0Cz.js +0 -1
- package/dist/assets/web/dist/assets/RunsIndexPage-0YAClYQh.js +0 -1
- package/dist/assets/web/dist/assets/budgets-BRLm6oID.js +0 -1
- package/dist/assets/web/dist/assets/sseReplay-BjlKmQsY.js +0 -11
|
@@ -32,7 +32,14 @@
|
|
|
32
32
|
import type { ShipRoundOutcome } from "../runEvents.js";
|
|
33
33
|
import { shows, type Verbosity } from "../verbosity.js";
|
|
34
34
|
import type { Handoff, HandoffLanded } from "./handoff.js";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
progressOf,
|
|
37
|
+
renderRenewal,
|
|
38
|
+
renewalDecision,
|
|
39
|
+
type PushedHeadFact,
|
|
40
|
+
type RenewalDecision,
|
|
41
|
+
type RenewalWhy,
|
|
42
|
+
} from "./renewal.js";
|
|
36
43
|
import type { RunStatus } from "../runRecord.js";
|
|
37
44
|
import { normalizeHead, sameCommit } from "../reviewedHead.js";
|
|
38
45
|
import {
|
|
@@ -125,13 +132,15 @@ export function interruptionCauseOfWords(words: string): InterruptionCause | und
|
|
|
125
132
|
return undefined;
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
export function shipInterruptedNote(prUrl?: string, cause?: InterruptionCause): string {
|
|
135
|
+
export function shipInterruptedNote(prUrl?: string, cause?: InterruptionCause, idle = false): string {
|
|
129
136
|
const stands = prUrl
|
|
130
137
|
? `Its work stands on GitHub: ${prUrl}.`
|
|
131
138
|
: "Whatever it pushed stands on its pipeline branch; no PR was opened yet.";
|
|
132
|
-
const reissue =
|
|
133
|
-
?
|
|
134
|
-
:
|
|
139
|
+
const reissue = idle
|
|
140
|
+
? "To continue, reply in this thread to continue."
|
|
141
|
+
: prUrl
|
|
142
|
+
? `To continue the review loop, re-issue \`agent:ship\` in this thread with only the PR URL (${prUrl}).`
|
|
143
|
+
: "To continue, re-issue `agent:ship` in this thread with the task — round 0 runs again on the same branch.";
|
|
135
144
|
const opening =
|
|
136
145
|
cause === "container_replaced"
|
|
137
146
|
? "⚠️ The resident container running this pipeline's child was replaced (a deploy's image swap) and the child could not resume, so the pipeline stopped."
|
|
@@ -477,7 +486,7 @@ export type Brief =
|
|
|
477
486
|
rebase: { branch: string; onto: string };
|
|
478
487
|
/** A renewal's segment (decision 0046): the child continues the previous
|
|
479
488
|
* segment's work from `from`, briefed with that run's write-up and handoff. */
|
|
480
|
-
continue?: { segment: number; from?: string; previousRunId?: string };
|
|
489
|
+
continue?: { segment: number; from?: string; previousRunId?: string; texts?: string[] };
|
|
481
490
|
}
|
|
482
491
|
| {
|
|
483
492
|
kind: "review";
|
|
@@ -542,10 +551,10 @@ export type CoordinatorAction =
|
|
|
542
551
|
| { type: "merge"; step: string; prNumber: number; headSha: string; queued?: true }
|
|
543
552
|
/** Read the check runs at the reviewed head (record 0055, the round verdict):
|
|
544
553
|
* the merge door's own reading, folded into the round. `retry` names the
|
|
545
|
-
* failed checks whose one flake re-run the machine is spending
|
|
546
|
-
*
|
|
547
|
-
* of
|
|
548
|
-
| { type: "checks"; step: string; prNumber: number; headSha: string; retry?: string[] }
|
|
554
|
+
* failed checks whose one flake re-run the machine is spending; `refire`
|
|
555
|
+
* asks for the one close/reopen recovery when no required check launched.
|
|
556
|
+
* Either effect runs instead of a read. */
|
|
557
|
+
| { type: "checks"; step: string; prNumber: number; headSha: string; retry?: string[]; refire?: true }
|
|
549
558
|
/** Wait for the intake's checks-settled event at the approved head, bounded as the fallback. */
|
|
550
559
|
| { type: "wait-checks"; step: string; headSha: string; timeoutMs: number }
|
|
551
560
|
| { type: "sleep"; step: string; ms: number }
|
|
@@ -636,6 +645,10 @@ export type PrCheck =
|
|
|
636
645
|
/** The head's self-declared fix-up commit subjects (`fixup!`/`squash!`/`amend!`),
|
|
637
646
|
* when the read asked for the facts — a head carrying one is not in the ready state. */
|
|
638
647
|
fixupCommits?: string[];
|
|
648
|
+
/** The base branch takes changes only through a merge queue (issue 2011):
|
|
649
|
+
* read beside the checks on the ending's facts read, so a `merge_ready`
|
|
650
|
+
* report tells the person their merge is queued, never a direct one. */
|
|
651
|
+
baseHasMergeQueue?: boolean;
|
|
639
652
|
}
|
|
640
653
|
| { state: "merged"; prNumber: number; url: string; sha: string; mergedAt: string };
|
|
641
654
|
|
|
@@ -667,10 +680,18 @@ export type StepReturn =
|
|
|
667
680
|
* queue removed it — the reason is the queue's own (issue 2011). */
|
|
668
681
|
| { type: "merge"; step: string; outcome: "pending" | "refused" | "enqueued" | "removed"; reason: string; at: number }
|
|
669
682
|
/** The checks read at the reviewed head; `checks` absent means GitHub could
|
|
670
|
-
* not be read, which the machine treats as pending (record 0055).
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
| {
|
|
683
|
+
* not be read, which the machine treats as pending (record 0055). Effect
|
|
684
|
+
* asks answer `retried` or `refired` instead; false still spends that one
|
|
685
|
+
* recovery so an unchanged head cannot loop on the write. */
|
|
686
|
+
| {
|
|
687
|
+
type: "checks";
|
|
688
|
+
step: string;
|
|
689
|
+
checks?: RoundChecks;
|
|
690
|
+
draft?: boolean;
|
|
691
|
+
retried?: boolean;
|
|
692
|
+
refired?: boolean;
|
|
693
|
+
at: number;
|
|
694
|
+
}
|
|
674
695
|
| { type: "wait-checks"; step: string; outcome: "event" | "timeout" }
|
|
675
696
|
| { type: "sleep"; step: string };
|
|
676
697
|
|
|
@@ -686,14 +707,18 @@ export interface CheckFailure {
|
|
|
686
707
|
}
|
|
687
708
|
|
|
688
709
|
/** The check runs at the reviewed head as the round's checks step reads them.
|
|
689
|
-
* `
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
710
|
+
* `required` carries the base's whole required-context set and `expected`
|
|
711
|
+
* its unreported subset. The whole set lets the table distinguish no required
|
|
712
|
+
* check launching from an unrelated run that did report; an ordinary missing
|
|
713
|
+
* subset reads exactly as pending (issue 2063). */
|
|
693
714
|
export interface RoundChecks {
|
|
694
715
|
total: number;
|
|
695
716
|
pending: string[];
|
|
696
717
|
failed: CheckFailure[];
|
|
718
|
+
/** Every check context required by the base. Together with `expected`, this
|
|
719
|
+
* distinguishes an empty required-check launch from an unrelated check
|
|
720
|
+
* that did report (for example the title workflow). */
|
|
721
|
+
required?: string[];
|
|
697
722
|
expected?: string[];
|
|
698
723
|
}
|
|
699
724
|
|
|
@@ -818,6 +843,8 @@ export type UnitEnding =
|
|
|
818
843
|
finalReply?: string;
|
|
819
844
|
/** A changes-requested review posted this round before the stop. */
|
|
820
845
|
postedReview?: boolean;
|
|
846
|
+
/** The mechanical WIP checkpoint a stopped coding child pushed before teardown. */
|
|
847
|
+
checkpoint?: { branch: string; sha: string };
|
|
821
848
|
}
|
|
822
849
|
| {
|
|
823
850
|
kind: "aborted";
|
|
@@ -852,6 +879,7 @@ export type UnitEnding =
|
|
|
852
879
|
spent: ShipBudgetSpent;
|
|
853
880
|
}
|
|
854
881
|
| { kind: "no_verdict"; round: RoundRef; reviewRounds: number; finalReply?: string }
|
|
882
|
+
| { kind: "idle_expired"; reviewRounds: number }
|
|
855
883
|
/** Round 0's coding child died on a provider transient — a model-gateway
|
|
856
884
|
* 5xx, a cut stream, a gateway timeout, past the harness's retry ladder —
|
|
857
885
|
* with nothing pushed, TWICE: the first such death re-ran the round once
|
|
@@ -860,7 +888,14 @@ export type UnitEnding =
|
|
|
860
888
|
* `transient` so it reads as a condition beside `checks_failed` and `held`
|
|
861
889
|
* in the plane's table, never as the child failing on its task. */
|
|
862
890
|
| { kind: "transient"; round: RoundRef; runId: string; reviewRounds: number }
|
|
863
|
-
| {
|
|
891
|
+
| {
|
|
892
|
+
kind: "interrupted";
|
|
893
|
+
round: RoundRef;
|
|
894
|
+
runId: string;
|
|
895
|
+
reviewRounds: number;
|
|
896
|
+
cause?: InterruptionCause;
|
|
897
|
+
checkpoint?: { branch: string; sha: string };
|
|
898
|
+
}
|
|
864
899
|
| { kind: "refused"; refusal: string; message?: string; round: RoundRef; reviewRounds: number }
|
|
865
900
|
/** The unit idles instead of ending (record 0051): with the resolved
|
|
866
901
|
* `ship.idleDays` above zero, `end()` wraps an idling kind — every kind but
|
|
@@ -896,7 +931,8 @@ export type UnitEnding =
|
|
|
896
931
|
* the pull request, which no thread reply can produce — but a blocked hold
|
|
897
932
|
* (issue 2086) waits for exactly the person's word the idle's wake carries,
|
|
898
933
|
* so it alone idles; `idleEnding` draws that line by cause. */
|
|
899
|
-
export type IdleWhy =
|
|
934
|
+
export type IdleWhy =
|
|
935
|
+
Exclude<UnitEnding["kind"], "idle" | "merged" | "already_landed" | "merge_ready" | "refused"> | RenewalWhy;
|
|
900
936
|
|
|
901
937
|
const NEVER_IDLES: ReadonlySet<UnitEnding["kind"]> = new Set([
|
|
902
938
|
"idle",
|
|
@@ -904,6 +940,7 @@ const NEVER_IDLES: ReadonlySet<UnitEnding["kind"]> = new Set([
|
|
|
904
940
|
"already_landed",
|
|
905
941
|
"merge_ready",
|
|
906
942
|
"refused",
|
|
943
|
+
"idle_expired",
|
|
907
944
|
]);
|
|
908
945
|
|
|
909
946
|
/** What a transition tells the driver beyond the next action: a round boundary
|
|
@@ -957,6 +994,12 @@ export interface LeaseSegmentProgress {
|
|
|
957
994
|
/** The previous segment's coding run: its write-up and handoff brief the continuation. */
|
|
958
995
|
previousRunId?: string;
|
|
959
996
|
previousHandoff?: Handoff;
|
|
997
|
+
/** Words that woke an idle segment, already attributed in arrival order. */
|
|
998
|
+
texts?: string[];
|
|
999
|
+
/** A stopped segment reopens under the remainder of its cut lease. Each
|
|
1000
|
+
* reopen gets a fresh attempt so its Workflow steps cannot replay the
|
|
1001
|
+
* stopped attempt's cached answers. */
|
|
1002
|
+
resume?: { leaseMs: number; attempt: number };
|
|
960
1003
|
}
|
|
961
1004
|
|
|
962
1005
|
export interface UnitPipelineInput {
|
|
@@ -1050,9 +1093,10 @@ type Phase =
|
|
|
1050
1093
|
| { at: "merge-wait"; pr: PrRef; headSha: string; n: number; since: number; waitMs: number; queued?: true }
|
|
1051
1094
|
/** The round's checks step (record 0055): the check runs at the reviewed head
|
|
1052
1095
|
* are read after an approve settles, before merge_ready or the merge door.
|
|
1053
|
-
* `graced`: a head with no check reported has had its one-chunk
|
|
1054
|
-
* `
|
|
1055
|
-
* the
|
|
1096
|
+
* `graced`: a head with no required check reported has had its one-chunk
|
|
1097
|
+
* grace; `refired`: the one pull_request close/reopen recovery is spent;
|
|
1098
|
+
* `retried`: the one flake re-run is spent. `retry` and `refire` make the
|
|
1099
|
+
* next ask perform that effect instead of reading. */
|
|
1056
1100
|
| {
|
|
1057
1101
|
at: "checks";
|
|
1058
1102
|
round: RoundRef;
|
|
@@ -1063,7 +1107,9 @@ type Phase =
|
|
|
1063
1107
|
waitMs: number;
|
|
1064
1108
|
graced: boolean;
|
|
1065
1109
|
retried: boolean;
|
|
1110
|
+
refired: boolean;
|
|
1066
1111
|
retry?: string[];
|
|
1112
|
+
refire?: true;
|
|
1067
1113
|
}
|
|
1068
1114
|
/** Waiting on `checks-settled-<head>` between two checks reads, in the merge
|
|
1069
1115
|
* wait's own chunks — the intake wakes the machine when the head settles. */
|
|
@@ -1077,6 +1123,7 @@ type Phase =
|
|
|
1077
1123
|
waitMs: number;
|
|
1078
1124
|
graced: boolean;
|
|
1079
1125
|
retried: boolean;
|
|
1126
|
+
refired: boolean;
|
|
1080
1127
|
}
|
|
1081
1128
|
| { at: "ended" };
|
|
1082
1129
|
|
|
@@ -1162,7 +1209,8 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
|
|
|
1162
1209
|
return nextReview(resumed).state;
|
|
1163
1210
|
}
|
|
1164
1211
|
|
|
1165
|
-
const deadlineAt = (s: UnitPipelineState) =>
|
|
1212
|
+
const deadlineAt = (s: UnitPipelineState) =>
|
|
1213
|
+
s.startedAt + (s.input.session?.resume?.leaseMs ?? s.input.caps.maxMinutes * MIN);
|
|
1166
1214
|
const remainingMs = (s: UnitPipelineState) => deadlineAt(s) - s.clock;
|
|
1167
1215
|
|
|
1168
1216
|
/** The next slice of a wait: a chunk; the remainder when less is left before
|
|
@@ -1189,11 +1237,14 @@ function roundCarve(s: UnitPipelineState, round: RoundRef): Carve {
|
|
|
1189
1237
|
);
|
|
1190
1238
|
}
|
|
1191
1239
|
|
|
1192
|
-
/** The prefix every step of this pipeline is named under: the unit id,
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
|
|
1196
|
-
|
|
1240
|
+
/** The prefix every step of this pipeline is named under: the unit id, a
|
|
1241
|
+
* renewal's segment (`U10/s2/…`), and a stopped segment's resumed attempt
|
|
1242
|
+
* (`U10/s2/r1/…`). Both transitions need fresh Workflow identities so the
|
|
1243
|
+
* durable step cache cannot answer a new attempt with an older one's result. */
|
|
1244
|
+
export const stepPrefixOf = (unit: string, session: LeaseSegmentProgress | undefined): string => {
|
|
1245
|
+
const segment = session !== undefined && session.segment > 1 ? `${unit}/s${session.segment}` : unit;
|
|
1246
|
+
return session?.resume !== undefined ? `${segment}/r${session.resume.attempt}` : segment;
|
|
1247
|
+
};
|
|
1197
1248
|
const stepPrefix = (s: UnitPipelineState) => stepPrefixOf(s.input.unit.id, s.input.session);
|
|
1198
1249
|
const roundStep = (s: UnitPipelineState, round: RoundRef) =>
|
|
1199
1250
|
`${stepPrefix(s)}/${round.index}/${round.kind}${round.attempt !== undefined ? `/a${round.attempt}` : ""}`;
|
|
@@ -1212,6 +1263,7 @@ function briefFor(s: UnitPipelineState, round: RoundRef): Brief {
|
|
|
1212
1263
|
segment: session.segment,
|
|
1213
1264
|
...(session.continueFrom !== undefined ? { from: session.continueFrom } : {}),
|
|
1214
1265
|
...(session.previousRunId !== undefined ? { previousRunId: session.previousRunId } : {}),
|
|
1266
|
+
...(session.texts !== undefined ? { texts: session.texts } : {}),
|
|
1215
1267
|
},
|
|
1216
1268
|
}
|
|
1217
1269
|
: {}),
|
|
@@ -1311,6 +1363,7 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
1311
1363
|
prNumber: p.prNumber,
|
|
1312
1364
|
headSha: p.headSha,
|
|
1313
1365
|
...(p.retry !== undefined ? { retry: p.retry } : {}),
|
|
1366
|
+
...(p.refire === true ? { refire: true as const } : {}),
|
|
1314
1367
|
};
|
|
1315
1368
|
case "checks-wait":
|
|
1316
1369
|
return {
|
|
@@ -1400,7 +1453,7 @@ function idleEnding(s: UnitPipelineState, ending: UnitEnding): Extract<UnitEndin
|
|
|
1400
1453
|
const round = "round" in old ? old.round : undefined;
|
|
1401
1454
|
return {
|
|
1402
1455
|
kind: "idle",
|
|
1403
|
-
why: old.kind,
|
|
1456
|
+
why: old.kind === "aborted" && old.renewal !== undefined ? old.renewal.decision.why : old.kind,
|
|
1404
1457
|
idled: old,
|
|
1405
1458
|
renewalsLeft,
|
|
1406
1459
|
...(from !== undefined ? { from } : {}),
|
|
@@ -1491,6 +1544,16 @@ function nextReview(s: UnitPipelineState, notes: CoordinatorNote[] = []): Transi
|
|
|
1491
1544
|
const stopMode = (status: RunStatus): "soft" | "hard" | undefined =>
|
|
1492
1545
|
status === "stopped_soft" ? "soft" : status === "stopped_hard" ? "hard" : undefined;
|
|
1493
1546
|
|
|
1547
|
+
/** The last mechanical WIP push to this unit branch. Unlike an ordinary push,
|
|
1548
|
+
* it says the child ended before its work was ready for review. */
|
|
1549
|
+
function interruptedCheckpoint(
|
|
1550
|
+
s: UnitPipelineState,
|
|
1551
|
+
pushed: readonly PushedHeadFact[] | undefined,
|
|
1552
|
+
): { branch: string; sha: string } | undefined {
|
|
1553
|
+
const found = pushed?.filter((p) => p.ref === s.input.unit.branch && p.by === "salvage").at(-1);
|
|
1554
|
+
return found ? { branch: found.ref, sha: found.sha } : undefined;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1494
1557
|
/** A coding run's confirmed end: round 0's child, or the run a findings step dispatched. */
|
|
1495
1558
|
function settleCoding(
|
|
1496
1559
|
s: UnitPipelineState,
|
|
@@ -1518,6 +1581,7 @@ function settleCoding(
|
|
|
1518
1581
|
}
|
|
1519
1582
|
: {}),
|
|
1520
1583
|
};
|
|
1584
|
+
const checkpoint = interruptedCheckpoint(next, facts.pushed);
|
|
1521
1585
|
const mode = stopMode(facts.status);
|
|
1522
1586
|
if (mode !== undefined)
|
|
1523
1587
|
return end(
|
|
@@ -1528,13 +1592,33 @@ function settleCoding(
|
|
|
1528
1592
|
round,
|
|
1529
1593
|
reviewRounds: next.reviewRounds,
|
|
1530
1594
|
...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
|
|
1595
|
+
...(checkpoint !== undefined ? { checkpoint } : {}),
|
|
1531
1596
|
},
|
|
1532
1597
|
[roundNote(round, "stopped")],
|
|
1533
1598
|
);
|
|
1599
|
+
// A mechanical WIP push means the child did not declare the work ready for
|
|
1600
|
+
// review. Keep the unit branch as the resumption point and abort this
|
|
1601
|
+
// attempt; an ordinary push still takes the established recover-PR path.
|
|
1602
|
+
if (facts.status === "failed" && checkpoint !== undefined) {
|
|
1603
|
+
const cause =
|
|
1604
|
+
facts.failure?.kind === "provider_transient"
|
|
1605
|
+
? "the model provider's transport retry budget was spent"
|
|
1606
|
+
: "it failed before finishing";
|
|
1607
|
+
return end(
|
|
1608
|
+
next,
|
|
1609
|
+
{
|
|
1610
|
+
kind: "aborted",
|
|
1611
|
+
reason: `⚠️ The coding child ended because ${cause}; the branch carries the interrupted work at \`${checkpoint.sha.slice(0, 7)}\` on \`${checkpoint.branch}\`. Re-issue the request to resume from it instead of starting over.`,
|
|
1612
|
+
round,
|
|
1613
|
+
reviewRounds: next.reviewRounds,
|
|
1614
|
+
},
|
|
1615
|
+
[roundNote(round, "aborted")],
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1534
1618
|
// A failed coding child no longer aborts outright: the pr-check looks at the
|
|
1535
|
-
// branch first —
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1619
|
+
// branch first — an ordinary push before the death is recovered as the
|
|
1620
|
+
// round's pull request, and only a branch with nothing on it ends the unit
|
|
1621
|
+
// with the child's own reason.
|
|
1538
1622
|
if (facts.status === "failed")
|
|
1539
1623
|
return {
|
|
1540
1624
|
state: {
|
|
@@ -1718,6 +1802,7 @@ function enterChecks(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNo
|
|
|
1718
1802
|
waitMs,
|
|
1719
1803
|
graced: false,
|
|
1720
1804
|
retried: false,
|
|
1805
|
+
refired: false,
|
|
1721
1806
|
},
|
|
1722
1807
|
},
|
|
1723
1808
|
notes,
|
|
@@ -1737,14 +1822,16 @@ function enterChecks(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNo
|
|
|
1737
1822
|
* (`expected`: a required check, or the repository's approve workflow
|
|
1738
1823
|
* whose run does not exist yet), or GitHub unreadable: the head waits on
|
|
1739
1824
|
* the settled event and is read again;
|
|
1740
|
-
* (—) no check reported
|
|
1825
|
+
* (—) no required check reported: one chunk of grace, then the pull_request
|
|
1826
|
+
* event is re-fired once by closing and reopening the pull request;
|
|
1741
1827
|
* (c) every expected check reported green: the round proceeds with no wait. */
|
|
1742
1828
|
function checksVerdict(
|
|
1743
1829
|
checks: RoundChecks | undefined,
|
|
1744
|
-
p: { retried: boolean; graced: boolean },
|
|
1830
|
+
p: { retried: boolean; graced: boolean; refired: boolean },
|
|
1745
1831
|
draft?: boolean,
|
|
1746
1832
|
):
|
|
1747
1833
|
| { kind: "retry"; names: string[] }
|
|
1834
|
+
| { kind: "refire" }
|
|
1748
1835
|
| { kind: "failed"; failed: CheckFailure[] }
|
|
1749
1836
|
| { kind: "draft" }
|
|
1750
1837
|
| { kind: "pending" }
|
|
@@ -1764,14 +1851,18 @@ function checksVerdict(
|
|
|
1764
1851
|
// person's "ready" changes anything — a red check above still gets its fix
|
|
1765
1852
|
// round, since that work stands whether or not the pull request is a draft.
|
|
1766
1853
|
if (draft === true) return { kind: "draft" };
|
|
1854
|
+
const required = checks?.required ?? [];
|
|
1855
|
+
const missing = checks?.expected ?? [];
|
|
1856
|
+
const noRequiredReported = required.length > 0 && missing.length === required.length;
|
|
1857
|
+
// A check launcher may report an unrelated workflow while every required
|
|
1858
|
+
// context is still absent. Give it one normal chunk; if it remains empty,
|
|
1859
|
+
// re-fire pull_request once. This is the same recovery a person performs for
|
|
1860
|
+
// a CI run that exists but started no workflows.
|
|
1861
|
+
if ((checks?.total === 0 || noRequiredReported) && !p.graced) return { kind: "grace" };
|
|
1862
|
+
if (noRequiredReported && !p.refired) return { kind: "refire" };
|
|
1767
1863
|
// GitHub unreadable answers as pending and is re-read at the chunk's end;
|
|
1768
1864
|
// an expected check not yet reported (issue 2063) is pending the same way.
|
|
1769
|
-
if (checks === undefined || checks.pending.length > 0 ||
|
|
1770
|
-
return { kind: "pending" };
|
|
1771
|
-
// No check reported at the head: one chunk of grace — the first check starts
|
|
1772
|
-
// within minutes where CI exists — then the round proceeds, so a repository
|
|
1773
|
-
// without CI costs one chunk per round and never idles (record 0055).
|
|
1774
|
-
if (checks.total === 0 && !p.graced) return { kind: "grace" };
|
|
1865
|
+
if (checks === undefined || checks.pending.length > 0 || missing.length > 0) return { kind: "pending" };
|
|
1775
1866
|
return { kind: "green" };
|
|
1776
1867
|
}
|
|
1777
1868
|
|
|
@@ -1783,14 +1874,15 @@ function checksVerdict(
|
|
|
1783
1874
|
* is still pending (issue 1991); only a head with no failed check waits a
|
|
1784
1875
|
* chunk inside the step's ask on what is unreadable or pending and then
|
|
1785
1876
|
* proceeds — the ending's facts read names what is still pending; a head with
|
|
1786
|
-
* no check reported
|
|
1787
|
-
* proceeds with no wait added. */
|
|
1877
|
+
* no required check reported gets one grace chunk and one event re-fire; a
|
|
1878
|
+
* green head proceeds with no wait added. */
|
|
1788
1879
|
function settleChecks(
|
|
1789
1880
|
s: UnitPipelineState,
|
|
1790
1881
|
p: Extract<Phase, { at: "checks" }>,
|
|
1791
1882
|
checks: RoundChecks | undefined,
|
|
1792
1883
|
retried?: boolean,
|
|
1793
1884
|
draft?: boolean,
|
|
1885
|
+
refired?: boolean,
|
|
1794
1886
|
): Transition {
|
|
1795
1887
|
const { round } = p;
|
|
1796
1888
|
const wait = (over: Partial<Extract<Phase, { at: "checks-wait" }>>): Transition => ({
|
|
@@ -1806,11 +1898,23 @@ function settleChecks(
|
|
|
1806
1898
|
waitMs: p.waitMs,
|
|
1807
1899
|
graced: p.graced,
|
|
1808
1900
|
retried: p.retried,
|
|
1901
|
+
refired: p.refired,
|
|
1809
1902
|
...over,
|
|
1810
1903
|
},
|
|
1811
1904
|
},
|
|
1812
1905
|
notes: [],
|
|
1813
1906
|
});
|
|
1907
|
+
// The pull_request re-fire is an effect ask after a whole grace chunk with
|
|
1908
|
+
// no required check. Whether GitHub accepted it or not, spend the one attempt
|
|
1909
|
+
// and read immediately: that read registers the ordinary bounded wait. A
|
|
1910
|
+
// successful effect adds the round boundary the card renders.
|
|
1911
|
+
if (p.refire === true) {
|
|
1912
|
+
const { refire: _refire, ...read } = p;
|
|
1913
|
+
return {
|
|
1914
|
+
state: { ...s, phase: { ...read, n: p.n + 1, refired: true } },
|
|
1915
|
+
notes: refired === true ? [roundNote(p.round, "checks_restarted")] : [],
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1814
1918
|
// The retry ask. Dispatched: wait for the head to settle, then read again;
|
|
1815
1919
|
// a second failure is the finding. NOT dispatched (`retried: false` — no
|
|
1816
1920
|
// re-runnable run behind the checks, or GitHub refused): the one re-run is
|
|
@@ -1831,6 +1935,11 @@ function settleChecks(
|
|
|
1831
1935
|
state: { ...s, phase: { ...p, n: p.n + 1, retry: verdict.names } },
|
|
1832
1936
|
notes: [],
|
|
1833
1937
|
};
|
|
1938
|
+
case "refire":
|
|
1939
|
+
return {
|
|
1940
|
+
state: { ...s, phase: { ...p, n: p.n + 1, refire: true } },
|
|
1941
|
+
notes: [],
|
|
1942
|
+
};
|
|
1834
1943
|
case "failed": {
|
|
1835
1944
|
const findings = [...(s.findingsByRound[round.index] ?? []), ...verdict.failed.map(checkFinding)];
|
|
1836
1945
|
const next: UnitPipelineState = {
|
|
@@ -2360,7 +2469,8 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2360
2469
|
// The hosted parent's hard stop landed while this child ran (record 0060;
|
|
2361
2470
|
// issue 1924): the unit ends stopped as the child ends, whatever the
|
|
2362
2471
|
// child's own status — the runner runs nothing more of it.
|
|
2363
|
-
if (r.stopped === true)
|
|
2472
|
+
if (r.stopped === true) {
|
|
2473
|
+
const checkpoint = p.round.kind === "review" ? undefined : interruptedCheckpoint(clocked, r.run.pushed);
|
|
2364
2474
|
return end(
|
|
2365
2475
|
clocked,
|
|
2366
2476
|
{
|
|
@@ -2369,14 +2479,33 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2369
2479
|
round: p.round,
|
|
2370
2480
|
reviewRounds: s.reviewRounds,
|
|
2371
2481
|
...(r.run.finalReply !== undefined ? { finalReply: r.run.finalReply } : {}),
|
|
2482
|
+
...(checkpoint !== undefined ? { checkpoint } : {}),
|
|
2372
2483
|
},
|
|
2373
2484
|
[roundNote(p.round, "stopped")],
|
|
2374
2485
|
);
|
|
2486
|
+
}
|
|
2375
2487
|
if (r.run.status === "interrupted") {
|
|
2376
2488
|
const cause = r.run.interruption;
|
|
2377
|
-
|
|
2378
|
-
//
|
|
2379
|
-
//
|
|
2489
|
+
const checkpoint = interruptedCheckpoint(clocked, r.run.pushed);
|
|
2490
|
+
// A mechanical WIP checkpoint is deliberately not opened for review:
|
|
2491
|
+
// the interrupted child never declared it finished. The next attempt
|
|
2492
|
+
// starts from the unit branch at that head.
|
|
2493
|
+
if (p.round.kind !== "review" && checkpoint !== undefined)
|
|
2494
|
+
return end(
|
|
2495
|
+
clocked,
|
|
2496
|
+
{
|
|
2497
|
+
kind: "interrupted",
|
|
2498
|
+
round: p.round,
|
|
2499
|
+
runId: p.runId,
|
|
2500
|
+
reviewRounds: s.reviewRounds,
|
|
2501
|
+
...(cause !== undefined ? { cause } : {}),
|
|
2502
|
+
checkpoint,
|
|
2503
|
+
},
|
|
2504
|
+
[roundNote(p.round, "aborted")],
|
|
2505
|
+
);
|
|
2506
|
+
// A dead CODING child may have pushed normally before the ledger closed
|
|
2507
|
+
// it: the pr-check recovers that branch. A review child has nothing on
|
|
2508
|
+
// the branch to recover, so its interruption still ends the unit at once.
|
|
2380
2509
|
if (p.round.kind !== "review")
|
|
2381
2510
|
return {
|
|
2382
2511
|
state: {
|
|
@@ -2411,7 +2540,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2411
2540
|
return settlePrCheck(clocked, p, (ret as Extract<StepReturn, { type: "pr-check" }>).pr);
|
|
2412
2541
|
case "checks": {
|
|
2413
2542
|
const r = ret as Extract<StepReturn, { type: "checks" }>;
|
|
2414
|
-
return settleChecks(clocked, p, r.checks, r.retried, r.draft);
|
|
2543
|
+
return settleChecks(clocked, p, r.checks, r.retried, r.draft, r.refired);
|
|
2415
2544
|
}
|
|
2416
2545
|
case "checks-wait":
|
|
2417
2546
|
// The event fired or the chunk elapsed either way the head is read again.
|
|
@@ -2428,6 +2557,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2428
2557
|
waitMs: p.waitMs,
|
|
2429
2558
|
graced: p.graced,
|
|
2430
2559
|
retried: p.retried,
|
|
2560
|
+
refired: p.refired,
|
|
2431
2561
|
},
|
|
2432
2562
|
},
|
|
2433
2563
|
notes: [],
|
|
@@ -2612,6 +2742,10 @@ export interface MergeReadyFacts {
|
|
|
2612
2742
|
* (git's autosquash prefixes `fixup!`, `squash!`, `amend!`): a head
|
|
2613
2743
|
* carrying one is by its own words not in the ready state. */
|
|
2614
2744
|
fixupCommits?: string[];
|
|
2745
|
+
/** The base branch takes changes only through a merge queue (issue 2011):
|
|
2746
|
+
* the merge:person path names "queued" in its line — the person's merge
|
|
2747
|
+
* enqueues the pull request and the queue merges it on its own. */
|
|
2748
|
+
baseHasMergeQueue?: boolean;
|
|
2615
2749
|
}
|
|
2616
2750
|
|
|
2617
2751
|
/** The check runs at one commit: how many, which still run, which failed. */
|
|
@@ -2694,6 +2828,10 @@ export function renderUnitReport(
|
|
|
2694
2828
|
? `Findings below ${level}, left as-is: ${skipped.map((f) => `${f.id} (${f.severity}) — ${f.title}`).join("; ")}`
|
|
2695
2829
|
: undefined;
|
|
2696
2830
|
const join = (parts: Array<string | undefined>) => parts.filter(Boolean).join("\n\n");
|
|
2831
|
+
const checkpointLine = (checkpoint: { branch: string; sha: string } | undefined) =>
|
|
2832
|
+
checkpoint === undefined
|
|
2833
|
+
? undefined
|
|
2834
|
+
: `The branch carries the interrupted work at \`${checkpoint.sha.slice(0, 7)}\` on \`${checkpoint.branch}\`; re-issue to resume from it instead of starting over.`;
|
|
2697
2835
|
switch (e.kind) {
|
|
2698
2836
|
case "merged":
|
|
2699
2837
|
if (e.by === "other")
|
|
@@ -2733,7 +2871,12 @@ export function renderUnitReport(
|
|
|
2733
2871
|
? `Already merged: ${e.pr.url} (merge commit \`${facts.merged.sha.slice(0, 7)}\`, merged ${facts.merged.mergedAt}) — auto-merge or a person merged it after the approval; the pipeline merged nothing.`
|
|
2734
2872
|
: facts?.autoMergeEnabled
|
|
2735
2873
|
? "Auto-merge is on for this pull request: the approval merges it once checks pass."
|
|
2736
|
-
:
|
|
2874
|
+
: facts?.baseHasMergeQueue === true
|
|
2875
|
+
? // The merge:person path names "queued" in its line (issue 2011):
|
|
2876
|
+
// the base takes changes only through a merge queue, so the
|
|
2877
|
+
// person's merge enqueues — never a direct merge.
|
|
2878
|
+
`Remaining gate: a person's merge, queued — \`${s.input.base}\` takes changes only through a merge queue, so the merge enqueues the pull request (\`gh pr merge --auto\`) and the queue merges it on its own; the pipeline merges only when the plan's \`merge\` setting says so, and ship never approves.`
|
|
2879
|
+
: "Remaining gate: a person's merge — the pipeline merges only when the plan's `merge` setting says so, and ship never approves.",
|
|
2737
2880
|
),
|
|
2738
2881
|
]
|
|
2739
2882
|
.filter(Boolean)
|
|
@@ -2837,9 +2980,11 @@ export function renderUnitReport(
|
|
|
2837
2980
|
aside(reissue),
|
|
2838
2981
|
]);
|
|
2839
2982
|
case "stopped":
|
|
2840
|
-
if (!shows(verbosity, "verbose"))
|
|
2983
|
+
if (!shows(verbosity, "verbose"))
|
|
2984
|
+
return join([`${e.mode === "hard" ? "⛔" : "⏹"} Stopped.${prLine}`, checkpointLine(e.checkpoint)]);
|
|
2841
2985
|
return join([
|
|
2842
2986
|
`${e.mode === "hard" ? "⛔" : "⏹"} Ship stopped by operator (${e.mode} stop) after ${rounds}.${prLine}`,
|
|
2987
|
+
checkpointLine(e.checkpoint),
|
|
2843
2988
|
writeUpPointer(
|
|
2844
2989
|
s,
|
|
2845
2990
|
e.round.kind,
|
|
@@ -2848,7 +2993,7 @@ export function renderUnitReport(
|
|
|
2848
2993
|
e.postedReview
|
|
2849
2994
|
? "ℹ️ A changes-requested review was posted this round before the stop — its findings stand on the PR."
|
|
2850
2995
|
: undefined,
|
|
2851
|
-
reissue,
|
|
2996
|
+
s.input.idleDays && s.input.idleDays > 0 ? "Next step: reply in this thread to continue." : reissue,
|
|
2852
2997
|
]);
|
|
2853
2998
|
case "aborted":
|
|
2854
2999
|
if (!shows(verbosity, "verbose")) return `⚠️ Aborted after ${rounds}: ${e.reason}${prLine}`;
|
|
@@ -2870,7 +3015,9 @@ export function renderUnitReport(
|
|
|
2870
3015
|
if (!shows(verbosity, "verbose")) return "";
|
|
2871
3016
|
return join([
|
|
2872
3017
|
writeUpPointer(s, e.round.kind, e.runId),
|
|
2873
|
-
|
|
3018
|
+
s.input.idleDays && s.input.idleDays > 0
|
|
3019
|
+
? `🔁 The unit's budget ran out with the unit unfinished — ${e.line}. Next step: reply in this thread to continue; ${e.renewalsLeft} renewal${e.renewalsLeft === 1 ? "" : "s"} remain${e.spendUsd !== null ? `, $${e.spendUsd.toFixed(2)} spent so far` : ""}.`
|
|
3020
|
+
: `🔁 The unit's budget ran out with the unit unfinished — ${e.line}. A fresh ${s.input.caps.maxMinutes}-minute budget opens in this thread${e.from !== undefined ? ` from \`${e.from.slice(0, 7)}\`` : ""}, with the last run's write-up as its request; ${e.renewalsLeft} renewal${e.renewalsLeft === 1 ? "" : "s"} remain${e.spendUsd !== null ? `, $${e.spendUsd.toFixed(2)} spent so far` : ""}.`,
|
|
2874
3021
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
2875
3022
|
]);
|
|
2876
3023
|
case "transient":
|
|
@@ -2892,7 +3039,9 @@ export function renderUnitReport(
|
|
|
2892
3039
|
reissue,
|
|
2893
3040
|
]);
|
|
2894
3041
|
case "interrupted":
|
|
2895
|
-
return shipInterruptedNote(prUrl, e.cause);
|
|
3042
|
+
return join([shipInterruptedNote(prUrl, e.cause, (s.input.idleDays ?? 0) > 0), checkpointLine(e.checkpoint)]);
|
|
3043
|
+
case "idle_expired":
|
|
3044
|
+
return "⌛ Idle expired: no reply continued this unit before its idle window closed.";
|
|
2896
3045
|
case "refused":
|
|
2897
3046
|
return join([
|
|
2898
3047
|
`🚫 The ${presetOf(e.round.kind)} child of round ${e.round.index} was refused by the authorize stage (${e.refusal})${e.message ? `: ${e.message}` : ""} — every child is authorized as the requesting user, so the pipeline ends here.`,
|
|
@@ -18,6 +18,8 @@ import type { Handoff } from "./handoff.js";
|
|
|
18
18
|
export interface PushedHeadFact {
|
|
19
19
|
ref: string;
|
|
20
20
|
sha: string;
|
|
21
|
+
/** Mechanical WIP checkpoints are preserved work, not a completed agent push. */
|
|
22
|
+
by?: "push" | "salvage";
|
|
21
23
|
at?: number;
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -82,7 +84,10 @@ export interface RenewalInput {
|
|
|
82
84
|
/** Dollars spent under this request so far; null when a run's model had no
|
|
83
85
|
* price, since a total that left a model's tokens out would understate it. */
|
|
84
86
|
spendUsd: number | null;
|
|
85
|
-
|
|
87
|
+
/** Progress normally comes from the segment's row. An idle wake sets it
|
|
88
|
+
* aside because the person's reply is the new work to run; the count, cap
|
|
89
|
+
* and fit still decide whether another lease may open. */
|
|
90
|
+
progress: Progress | "set_aside";
|
|
86
91
|
/** The pipeline the next segment would run: its fit is re-asserted before every segment. */
|
|
87
92
|
pipeline: Pipeline;
|
|
88
93
|
}
|
|
@@ -101,7 +106,8 @@ const usd = (n: number): string => `$${n.toFixed(2)}`;
|
|
|
101
106
|
* the progress. */
|
|
102
107
|
export function renewalDecision(input: RenewalInput): RenewalDecision {
|
|
103
108
|
const renewalsLeft = Math.max(0, input.grant.renewals - input.renewalsSpent);
|
|
104
|
-
if (
|
|
109
|
+
if (input.progress !== "set_aside" && !input.progress.progressed)
|
|
110
|
+
return { renew: false, why: "no_progress", detail: input.progress.why, renewalsLeft };
|
|
105
111
|
if (renewalsLeft === 0) {
|
|
106
112
|
const detail =
|
|
107
113
|
input.grant.renewals === 0
|
|
@@ -139,7 +145,7 @@ export function renewalDecision(input: RenewalInput): RenewalDecision {
|
|
|
139
145
|
return {
|
|
140
146
|
renew: true,
|
|
141
147
|
segment: input.renewalsSpent + 2,
|
|
142
|
-
...(input.progress.by === "push" ? { from: input.progress.sha } : {}),
|
|
148
|
+
...(input.progress !== "set_aside" && input.progress.by === "push" ? { from: input.progress.sha } : {}),
|
|
143
149
|
renewalsLeft: renewalsLeft - 1,
|
|
144
150
|
};
|
|
145
151
|
}
|
|
@@ -149,22 +155,31 @@ export function renewalDecision(input: RenewalInput): RenewalDecision {
|
|
|
149
155
|
* names the clause and, when renewals remain, what actually spends one — a
|
|
150
156
|
* budget that made progress — and the honest recourse (re-issue the request).
|
|
151
157
|
* It teaches no keyword: the router has none (routing-and-config item 3). */
|
|
152
|
-
export function renderRenewal(
|
|
158
|
+
export function renderRenewal(
|
|
159
|
+
decision: RenewalDecision,
|
|
160
|
+
grant: Grant,
|
|
161
|
+
options: { idle?: boolean; senders?: readonly string[] } = {},
|
|
162
|
+
): string {
|
|
163
|
+
const recourse = options.idle ? "reply in this thread to continue" : "re-issue the request to try again";
|
|
164
|
+
const senders = options.senders?.length
|
|
165
|
+
? `, with ${options.senders.length} message${options.senders.length === 1 ? "" : "s"} from ${options.senders.join(", ")}`
|
|
166
|
+
: "";
|
|
153
167
|
if (decision.renew)
|
|
154
|
-
return `budget renewed, ${decision.segment - 1} of ${grant.renewals}, continues ${decision.from !== undefined ? decision.from.slice(0, 7) : "the branch's head"}`;
|
|
168
|
+
return `budget renewed, ${decision.segment - 1} of ${grant.renewals}, continues ${decision.from !== undefined ? decision.from.slice(0, 7) : "the branch's head"}${senders}`;
|
|
155
169
|
const holds =
|
|
156
170
|
decision.renewalsLeft > 0
|
|
157
171
|
? `${decision.renewalsLeft} renewal${decision.renewalsLeft === 1 ? "" : "s"} left`
|
|
158
172
|
: "no renewals left";
|
|
173
|
+
const idleRecourse = (line: string): string => (options.idle ? `${line}; ${recourse}` : line);
|
|
159
174
|
switch (decision.why) {
|
|
160
175
|
case "no_progress":
|
|
161
176
|
return decision.renewalsLeft > 0
|
|
162
|
-
? `no progress on the last budget; ${holds} unspent — a renewal is spent only by a budget that pushed to the unit's branch or moved its write-up;
|
|
177
|
+
? `no progress on the last budget; ${holds} unspent — a renewal is spent only by a budget that pushed to the unit's branch or moved its write-up; ${recourse}`
|
|
163
178
|
: `no progress on the last budget; ${holds}`;
|
|
164
179
|
case "cost_cap":
|
|
165
|
-
return decision.renewalsLeft > 0 ? `${decision.detail}; ${holds} unspent` : decision.detail;
|
|
180
|
+
return idleRecourse(decision.renewalsLeft > 0 ? `${decision.detail}; ${holds} unspent` : decision.detail);
|
|
166
181
|
case "grant_exhausted":
|
|
167
182
|
case "unfit":
|
|
168
|
-
return decision.detail;
|
|
183
|
+
return idleRecourse(decision.detail);
|
|
169
184
|
}
|
|
170
185
|
}
|