@coreplane/switchboard 1.243.0 → 1.245.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 +6 -3
- package/dist/assets/deploy/cloudflare/worker.ts +2 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +148 -0
- package/dist/assets/deploy/cloudflare-resident/gc.ts +8 -1
- package/dist/assets/deploy/cloudflare-resident/threadErr.ts +337 -0
- package/dist/assets/deploy/cloudflare-resident/tsconfig.json +1 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +258 -173
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +18 -12
- 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 +5 -5
- package/dist/assets/src/config/profile.ts +17 -1
- package/dist/assets/src/core/authz/types.ts +18 -0
- package/dist/assets/src/core/budgets.ts +61 -0
- package/dist/assets/src/core/coordinator/contract.ts +32 -0
- package/dist/assets/src/core/coordinator/driver.ts +75 -12
- package/dist/assets/src/core/delivery.ts +1 -1
- package/dist/assets/src/core/reviewVerdict.ts +101 -24
- package/dist/assets/src/core/runEvents.ts +76 -8
- package/dist/assets/src/core/runFriction.ts +2 -1
- package/dist/assets/src/core/runRecord.ts +65 -0
- package/dist/assets/src/core/ship/contract.ts +2 -1
- package/dist/assets/src/core/ship/coordinator.ts +235 -33
- package/dist/assets/src/core/ship/renewal.ts +155 -0
- package/dist/assets/src/core/trace/attrs.ts +11 -0
- package/dist/assets/src/core/trace/types.ts +4 -3
- package/dist/assets/src/execution/residentRefresh.ts +46 -7
- package/dist/assets/src/execution/residentText.ts +8 -3
- package/dist/assets/src/mcp/registry.ts +3 -0
- package/dist/assets/web/dist/.vite/manifest.json +491 -472
- package/dist/assets/web/dist/assets/AppShell-qqs_p96-.js +1 -0
- package/dist/assets/web/dist/assets/CostsPage-Dd9aXxxI.js +2 -0
- package/dist/assets/web/dist/assets/DeliveryPage-D90FTjYq.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-VLTFopnl.js +2 -0
- package/dist/assets/web/dist/assets/InputMenu-lQP3ia9D.js +1 -0
- package/dist/assets/web/dist/assets/NotFoundPage-8ViskpN3.js +1 -0
- package/dist/assets/web/dist/assets/PendingTurnRow-D2gHef5v.js +1 -0
- package/dist/assets/web/dist/assets/ResidentDetailPage-nWLN8BOd.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-D3Q48CJW.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-CHy_IfZG.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-C6kX5om4.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DjxGj1Q5.js +1 -0
- package/dist/assets/web/dist/assets/RunsTabs-WyeG5tKb.js +1 -0
- package/dist/assets/web/dist/assets/ScheduledPage-B8D5AU4d.js +1 -0
- package/dist/assets/web/dist/assets/SettingSelect-UTHsqUUS.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-BoW7Zjzt.js +1 -0
- package/dist/assets/web/dist/assets/StatusDot-BGHureCJ.js +1 -0
- package/dist/assets/web/dist/assets/Tooltip-CiP8qPd7.js +1 -0
- package/dist/assets/web/dist/assets/UnitRoutePage-2zEeRBP1.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-oBNfPJR0.js → angular-html-qXhb9ti8.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-BvNwsyWA.js → angular-ts-DzvdyoUZ.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-CNUdRlYf.js → apl-Ck7Ru2Sm.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-Zb0NriSe.js → astro-CwehvReb.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-qPRVheqq.js → blade-WSyVbSFK.js} +1 -1
- package/dist/assets/web/dist/assets/{c-D8Awx4YO.js → c-DMHI1wDz.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-Bt72Mhsx.js → chapel-BF8AYK8_.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-BOBacexg.js → cobol-UAfImuWu.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-E4u0liHW.js → coffee-D54hi96d.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-q2sLNlul.js → cpp-BRWfLgBc.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DyWqUnlb.js → crystal-CSqC5deT.js} +1 -1
- package/dist/assets/web/dist/assets/{css-CQY0hFsD.js → css-D4ZuRFGk.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-twkFmSUY.js → dist-73ZMn_Mg.js} +2 -2
- package/dist/assets/web/dist/assets/{durationTone-BobbycC-.js → durationTone-D51enaGR.js} +1 -1
- package/dist/assets/web/dist/assets/{edge-C1MwhJkX.js → edge-DDPtNbZc.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-Bb3YbHfn.js → elixir-BTQkQfsJ.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DAN9IGQw.js → elm-B_PkLzGZ.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-BEB8Xlsj.js → erb-DfjIMVM_.js} +1 -1
- package/dist/assets/web/dist/assets/{eventSource-tRprhqm7.js → eventSource-By0B_w4S.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-tqpjRfxO.js → git-rebase-DP8wmn7K.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-Ccbo65zR.js → glimmer-js-G12VK2gK.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-B6WBVMpE.js → glimmer-ts-D3XiSI8E.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-tRec3Fcu.js → glsl-DOKAhMj-.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-P8kbxT4F.js → graphql-CJB_fmON.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-H9Zhkagy.js → hack-BHqtRWap.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DtEnpn7Z.js → haml-DwotyarS.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-DkgPfoAz.js → handlebars-B6fDaA3U.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DwozLrEx.js → html-derivative-C5bpQmmI.js} +1 -1
- package/dist/assets/web/dist/assets/{html-D30RXpIs.js → html-s52taj36.js} +1 -1
- package/dist/assets/web/dist/assets/{http-DXuzBAPm.js → http-99zC935r.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-d1UUJIt_.js → hurl-Bgk-4-0x.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-B_s5tKyq.js → indexRow-BmK74Vp1.js} +1 -1
- package/dist/assets/web/dist/assets/{java-DL0gWf34.js → java-CuV_c5I0.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-Cl7vavnS.js → javascript-CZlOvSMQ.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-CfQOWMX9.js → jinja-BuXeZ4Tu.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-KdYirlqm.js → jison-DIuP0hkG.js} +1 -1
- package/dist/assets/web/dist/assets/{json-C9cDQ-Qj.js → json-CZ74vfoD.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CRx5NItd.js → jsx-BNjRs8Vj.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-CmsQsZQl.js → julia-OCE-fFOg.js} +1 -1
- package/dist/assets/web/dist/assets/{just-CsM3Q8TE.js → just-CtvD87WF.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BXCh5YRX.js → latex-C3kf86OR.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BF2vwK8p.js → liquid-B1WI--Kp.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-DcMBATrl.js → lua-DtoT0gKg.js} +1 -1
- package/dist/assets/web/dist/assets/main-ClluP-Qf.js +28 -0
- package/dist/assets/web/dist/assets/main-CpkwzHX6.css +1 -0
- package/dist/assets/web/dist/assets/{marko-88MndKvG.js → marko-BE2MC6Yt.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-DJ4kVAd8.js → mdc-BwO4weXJ.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-CP6mRgtV.js → nginx-BTuOgGVC.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-QQfj3fpF.js → nim-CiIUM-oV.js} +1 -1
- package/dist/assets/web/dist/assets/{org-Bke3eBzc.js → org-L78DEWB-.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-Bkh0N7Kz.js → perl-KqHOm4Ft.js} +1 -1
- package/dist/assets/web/dist/assets/{php-Dnv1Piya.js → php-C41XbEkn.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-D6peFFI7.js → pug-B12-4i-k.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-D8PEs-C-.js → qml--fw2Up5c.js} +1 -1
- package/dist/assets/web/dist/assets/{r-B1EL9b_j.js → r-Db1JBXrC.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BsMTIh2b.js → razor-BYpVBgbu.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-HhvC8spD.js → regexp-BMqGLSPc.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-D5paAxpg.js → rst-B63n7-Sa.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-BrQwhLrl.js → ruby-D3VI8UYv.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-afot2B1o.js → sas-DxRf8ZAR.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-Efm-mwuG.js → scss-DRRA6d6A.js} +1 -1
- package/dist/assets/web/dist/assets/seed-DFfT5S_N.js +6 -0
- package/dist/assets/web/dist/assets/{settingsApi-priOPiwu.js → settingsApi-Dm6eaQcu.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-BK0Vv5fT.js → shellscript-CoOBe3_y.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-BGqlMC7N.js → shellsession-DOV1HYxf.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-M0b4UwGM.js → soy-Acpmpp1W.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-sxe6IE9j.js → sql-DeT9N-fA.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-C9m_EB8J.js → sseReplay-DPXVxhL5.js} +5 -5
- package/dist/assets/web/dist/assets/{stata-nPF_ddLP.js → stata-CbxXAMO4.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-D_GrC6u7.js → surrealql-BdKbi4hc.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-DwL1AtNP.js → svelte-tAcjDM7R.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-7s7LTDkc.js → templ-CObINNJj.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-Bx-5fMxe.js → tex-BYwpZT1J.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-DUMJnke_.js → ts-tags-BciREL5R.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-BN8biPDe.js → tsx-CRiv3Pr3.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-8nIu84TN.js → twig-DuZZ1Ej0.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BooSPq_S.js → typescript-jkHz_5HZ.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-CTBiBsem.js → typst-DOac4Gal.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-C6Ft4Lea.js → vue-CgOX5wXA.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-Ba36dD5D.js → vue-html-Duu99VNw.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-NkFexVo2.js → vue-vine-Bmj_wvRJ.js} +1 -1
- package/dist/assets/web/dist/assets/wallClock-DD_fPuSY.js +1 -0
- package/dist/assets/web/dist/assets/{xml-C_THnHXZ.js → xml-BtUIVeWH.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-D8G5xqjY.js → xsl-wX71n1E5.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-jAMIJzge.js → yaml-CdLdXvmz.js} +1 -1
- package/dist/cli.js +4330 -1256
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-IDMGG6yi.js +0 -1
- package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +0 -2
- package/dist/assets/web/dist/assets/DeliveryPage-n1tz5I_v.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +0 -2
- package/dist/assets/web/dist/assets/NotFoundPage-C97EhJcQ.js +0 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BT9RhFZ7.js +0 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-DACalNVF.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-SHPdu6uZ.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +0 -1
- package/dist/assets/web/dist/assets/RunsTabs-BUfdk0lH.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DJ8HiCPt.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-DLiN5IgY.js +0 -1
- package/dist/assets/web/dist/assets/StatusDot-Dw0T1M-P.js +0 -1
- package/dist/assets/web/dist/assets/Tooltip-BbLuIAiS.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DicUG96U.js +0 -1
- package/dist/assets/web/dist/assets/main-B4kEF3Sg.css +0 -1
- package/dist/assets/web/dist/assets/main-d-w-tIKt.js +0 -28
- package/dist/assets/web/dist/assets/seed-DeKSHrt2.js +0 -6
- package/dist/assets/web/dist/assets/wallClock-61NpVkY6.js +0 -1
|
@@ -49,7 +49,11 @@ import {
|
|
|
49
49
|
import { envFromRequest } from "../../src/execution/sandboxEnv.js";
|
|
50
50
|
import { IdleGuard, type IdleGuardHost } from "../../src/execution/sandboxIdle.js";
|
|
51
51
|
import { StartGate, type StartGateHost, type StartingCause } from "../../src/execution/sandboxStart.js";
|
|
52
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
RUNTIME_REPLACEMENT_WORDING,
|
|
54
|
+
isRuntimeUnreachableSignal,
|
|
55
|
+
selfAndCauses,
|
|
56
|
+
} from "../../src/execution/residentRefresh.js";
|
|
53
57
|
import {
|
|
54
58
|
fleetBusyAnswer,
|
|
55
59
|
fleetBusyExecAnswer,
|
|
@@ -149,15 +153,6 @@ const RUNTIME_REPLACED_REASONS = new Set([
|
|
|
149
153
|
* closed (a runtime crash or stop), the socket failed, the upgrade failed. */
|
|
150
154
|
const RPC_TRANSPORT_LOSS_KINDS = new Set(["peer_closed", "connection_failed", "upgrade_failed", "session_disposed"]);
|
|
151
155
|
|
|
152
|
-
/** `err` and its `cause` chain, bounded like the SDK's own walk. */
|
|
153
|
-
function* selfAndCauses(err: unknown): Generator<unknown> {
|
|
154
|
-
let link: unknown = err;
|
|
155
|
-
for (let depth = 0; link !== null && link !== undefined && depth < 8; depth++) {
|
|
156
|
-
yield link;
|
|
157
|
-
link = typeof link === "object" ? (link as { cause?: unknown }).cause : undefined;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
156
|
/** Did the container's runtime change under the command? Typed first (the
|
|
162
157
|
* Durable Object sees the SDK's own classes, so `instanceof` holds here), the
|
|
163
158
|
* SDK's replacement wording second — the same list the resident Worker
|
|
@@ -588,9 +583,20 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
588
583
|
await Promise.race([settled, new Promise<void>((r) => setTimeout(r, maxMs))]);
|
|
589
584
|
}
|
|
590
585
|
|
|
591
|
-
/** `du -sk` over the paths, summed, in KiB; null when nothing could be
|
|
586
|
+
/** `du -sk` over the paths, summed, in KiB; null when nothing could be
|
|
587
|
+
* measured — including a `du` that did not answer inside its own limit
|
|
588
|
+
* (a mount mid-restore under load): the judge treats a sample the
|
|
589
|
+
* container could not take as no evidence either way, never as the
|
|
590
|
+
* restore's failure. The restore's own promise still reports a runtime
|
|
591
|
+
* that went away. */
|
|
592
592
|
private async duKiB(paths: string[]): Promise<number | null> {
|
|
593
|
-
|
|
593
|
+
let r: { stdout: string; exitCode: number };
|
|
594
|
+
try {
|
|
595
|
+
r = await this.runRoot(["du", "-sk", ...paths], 30_000);
|
|
596
|
+
} catch {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
if (r.exitCode === 124) return null;
|
|
594
600
|
let total = 0;
|
|
595
601
|
let seen = false;
|
|
596
602
|
for (const line of r.stdout.split("\n")) {
|
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
"workers": ["bot"],
|
|
17
17
|
"note": "Anthropic Console key the bot runs on. Third-party."
|
|
18
18
|
},
|
|
19
|
+
{
|
|
20
|
+
"name": "OPENAI_API_KEY",
|
|
21
|
+
"workers": ["bot"],
|
|
22
|
+
"optional": true,
|
|
23
|
+
"note": "OpenAI key for the example config's `openai` provider block (`apiKeyEnv: OPENAI_API_KEY`). Third-party. Optional: read at the first model call on that block, never at load; without it every other block runs and only a run whose model names `openai/…` fails, by this name."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "OPENROUTER_API_KEY",
|
|
27
|
+
"workers": ["bot"],
|
|
28
|
+
"optional": true,
|
|
29
|
+
"note": "OpenRouter key for the example config's `openrouter` provider block (`apiKeyEnv: OPENROUTER_API_KEY`). Third-party. Optional: read at the first model call on that block, never at load; without it every other block runs and only a run whose model names `openrouter/…` fails, by this name."
|
|
30
|
+
},
|
|
19
31
|
{
|
|
20
32
|
"name": "ANTHROPIC_ADMIN_KEY",
|
|
21
33
|
"workers": ["bot"],
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.245.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.245.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -20445,7 +20445,7 @@
|
|
|
20445
20445
|
},
|
|
20446
20446
|
"packages/switchboard": {
|
|
20447
20447
|
"name": "@coreplane/switchboard",
|
|
20448
|
-
"version": "1.
|
|
20448
|
+
"version": "1.245.0",
|
|
20449
20449
|
"license": "Apache-2.0",
|
|
20450
20450
|
"dependencies": {
|
|
20451
20451
|
"@earendil-works/pi-ai": "0.85.1",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.245.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/dist/assets/source.json
CHANGED
|
@@ -145,7 +145,7 @@ export function statusCardRule(examples = '"Implement the fix", "Run the test su
|
|
|
145
145
|
|
|
146
146
|
const PR_DESCRIPTION_TEMPLATE = `PR description — submit it with the submit_pr_description tool for EVERY PR (this is the default, not something to wait to be asked for). Switchboard renders the GitHub body from the object you submit, so never author PR-body markdown yourself. Before submitting, judge your title with the ${PR_TITLE_GUARD} gate — \`npm run check:pr-title -- "<title>"\` — and submit only a title it accepts; the same gate refuses the PR in CI. The body is a fixed-size MAP for the reader with everything for agents collapsed under it; every field is capped in visible characters (a link's URL is not counted) and the tool refuses an object over a cap naming the field and the count — cut and resubmit. Prose is unwrapped — no hard line breaks inside a paragraph. Always hyperlink the triggering issue/request. Never fabricate validation — state exactly what you ran and the real result. BEFORE authoring the pointers, load the \`pr-description\` skill with use_skill — it defines how to choose at most seven pointers, the mechanical anchor rules and what goes below the fold; follow it for every PR.
|
|
147
147
|
EVERY PR includes one that already exists when you push — opened by a person, by dependabot, or by an earlier run. After EVERY push to such a PR: read its current title and body (\`github_issue_get\` with the PR number works for pull requests; \`gh pr view\` where gh exists), judge them against the change as it now stands at the pushed head, and submit the object that describes the PR as it is NOW — carry forward what the existing body says that is still true (a dependency bump's release notes belong in why), add what you changed, and anchor the pointers at the new head. Switchboard replaces the PR's title and body with your rendering. A description that describes an earlier state of its branch is a bug; "it is someone else's PR" is never a reason to leave it.
|
|
148
|
-
- **title
|
|
148
|
+
- **title** (≤72 characters in all): the changelog line — \`type(scope): what a reader can now do or expect\`, one change, one clause, present tense. The type and the scope spend the same budget as the description, so the description is short; the body carries the rest. The tool refuses a longer title naming the count, and so does the gate in CI.
|
|
149
149
|
- **TL;DR** (\`tldr\`, rendered first, ≤300): two sentences for a reader with zero context — what this PR does and why it matters.
|
|
150
150
|
- **Why** (\`why\`, ≤400): the problem and the motivation, with the triggering issue/request, the record and the stack position hyperlinked. Why, never what: the diff shows what.
|
|
151
151
|
- **Where to look** (\`pointers\`, 1 to 7): the files a reviewer would open first, in reading order, each { label ≤60, text ≤160, optional risk ≤100, anchor } with the anchor a { path, from, to } line range at your pushed head, rendered as a link (never embedded code). One pointer per idea, never per file; when the change has more ideas than seven, keep the seven whose mistake would cost most.
|
|
@@ -225,7 +225,7 @@ Keep notes with the \`notes\` tool: one short document, replaced whole each time
|
|
|
225
225
|
// project's most expensive checks first; the rule is the runner's to hold, not
|
|
226
226
|
// a line every requester remembers to paste. Stack-agnostic on purpose — the
|
|
227
227
|
// classes are by duration, the project's own scripts and CI say which is which.
|
|
228
|
-
export const CHECKS_BY_COST = `CHECKS BY COST — push before the expensive ones. Every check you might run has a cost class: seconds (a formatter or a linter on the files you touched, one test file, a docs, link or spec check, the typecheck of one package) or minutes (the whole test suite, a build, a dependency install, an end-to-end or full verification). Know a command's class before you run it — from the project's own scripts and CI configuration, from how long it took last time, or by the class above when you have nothing better. Prove each change with the cheapest check that can prove it, matched to the change's scope: a documentation change gets the documentation checks, one module gets its own tests, a shared type gets the typecheck. As soon as the change exists and those checks pass, commit and push — the pushed branch is the deliverable, and an unpushed tree does not survive the run's end. Only then run the expensive checks, once, and fix forward with further commits and pushes. Never start an operation whose expected duration does not fit the time you have left minus what a commit, a push and the description need: push what there is and say plainly what is unverified instead. The description's validation names exactly what ran; what did not run is CI's to gate, and you say so.`;
|
|
228
|
+
export const CHECKS_BY_COST = `CHECKS BY COST — push before the expensive ones. Every check you might run has a cost class: seconds (a formatter or a linter on the files you touched, one test file, a docs, link or spec check, the typecheck of one package) or minutes (the whole test suite, a build, a dependency install, an end-to-end or full verification). Know a command's class before you run it — from the project's own scripts and CI configuration, from how long it took last time, or by the class above when you have nothing better. Prove each change with the cheapest check that can prove it, matched to the change's scope: a documentation change gets the documentation checks, one module gets its own tests, a shared type gets the typecheck. As soon as the change exists and those checks pass, commit and push — the pushed branch is the deliverable, and an unpushed tree does not survive the run's end. Only then run the expensive checks, once, and fix forward with further commits and pushes. Never start an operation whose expected duration does not fit the time you have left minus what a commit, a push and the description need: push what there is and say plainly what is unverified instead. At the wind-down note, commit and push what compiles, say what does not, then answer. The description's validation names exactly what ran; what did not run is CI's to gate, and you say so.`;
|
|
229
229
|
|
|
230
230
|
const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
|
|
231
231
|
|
|
@@ -357,9 +357,9 @@ Your final message is posted to Slack — keep it readable, lead with the outcom
|
|
|
357
357
|
|
|
358
358
|
// Both review prompts carry this verbatim. The findings contract
|
|
359
359
|
// (docs/reference/specs/agent-ship.md item 6) lives here once — stable ids, the severity
|
|
360
|
-
// vocabulary, the
|
|
361
|
-
// resident variants can never drift apart on it.
|
|
362
|
-
const REVIEW_VERDICT_INSTRUCTION = `VERDICT: before your final message, call the submit_verdict tool exactly once with \`approve\` (no
|
|
360
|
+
// vocabulary, the severity gate's downgrade (agent-review.md item 5a) — so the
|
|
361
|
+
// sandbox and resident variants can never drift apart on it.
|
|
362
|
+
const REVIEW_VERDICT_INSTRUCTION = `VERDICT: before your final message, call the submit_verdict tool exactly once with \`approve\` (no finding at or above the severity to address remains — the level in force for this run, \`minor\` by default: a major or a minor finding means \`request_changes\`; nits alone never block) or \`request_changes\`, a one-line summary, \`head\` = the output of \`git rev-parse HEAD\` in the checkout you reviewed, and \`findings\` — every issue you report as a structured entry with a stable id you assign in order (F1, F2, …), a severity of exactly blocking|major|minor|nit, the file (plus line when it points at one), and a one-line title. The findings array is the index of your review: the full explanation of each finding stays in your prose, keyed by the same ids. Switchboard writes the verdict as the first line of the GitHub comment itself and lists the findings under it; a review with no submitted verdict is posted as not approving, so never skip it. An \`approve\` carrying a finding at or above the severity to address is downgraded to \`request_changes\` and the tool's ack says so — approve only when every finding sits below the level. Do not write "LGTM" in your own text — the verdict line carries it.`;
|
|
363
363
|
|
|
364
364
|
// The diff-gated spec review (docs/reference/specs/agent-review.md item 14) and
|
|
365
365
|
// the test guard under it (item 16; specs-coverage.md item 6), one text for
|
|
@@ -231,7 +231,11 @@ export function boundedByParent(
|
|
|
231
231
|
* reply names. */
|
|
232
232
|
export type ProfileRefusal =
|
|
233
233
|
| { axis: "identity"; needs: Identity; cap: Identity; scope: BoundaryScope }
|
|
234
|
-
| { axis: "machine"; needs: MachineClass; allowed: MachineClass[]; scopes: BoundaryScope[] }
|
|
234
|
+
| { axis: "machine"; needs: MachineClass; allowed: MachineClass[]; scopes: BoundaryScope[] }
|
|
235
|
+
/** The minutes axis refuses only under a minimum the caller names (the
|
|
236
|
+
* preset's lease minimum, `leaseMinimum` in `src/core/budgets.ts`): a lease
|
|
237
|
+
* clipped below it would leave the loop no time at all. */
|
|
238
|
+
| { axis: "minutes"; needs: number; have: number; scope: BoundaryScope };
|
|
235
239
|
|
|
236
240
|
/** The effective profile, or the refusal the authorize stage turns into a
|
|
237
241
|
* named reply. Resolution is pure; the gate judges. */
|
|
@@ -252,6 +256,8 @@ export function effectiveProfile(
|
|
|
252
256
|
preset: Pick<AgentDef, "machine" | "identity" | "maxMinutes">,
|
|
253
257
|
directives: { budget?: number },
|
|
254
258
|
boundary: EffectiveBoundary | undefined,
|
|
259
|
+
/** The least lease the preset does anything under; absent, the minutes only clip. */
|
|
260
|
+
minimum?: number,
|
|
255
261
|
): ProfileResolution {
|
|
256
262
|
if (boundary?.maxIdentity && !identityWithin(preset.identity, boundary.maxIdentity.value)) {
|
|
257
263
|
return {
|
|
@@ -285,6 +291,16 @@ export function effectiveProfile(
|
|
|
285
291
|
minutes = boundary.maxMinutes.value;
|
|
286
292
|
boundedBy = boundary.maxMinutes.scope;
|
|
287
293
|
}
|
|
294
|
+
// Under the minimum the clip is a refusal, not a run: the write-up and the
|
|
295
|
+
// post-step take the whole lease and the loop never turns. Attributed to
|
|
296
|
+
// whatever clipped; a declared ask under its own minimum is the module's
|
|
297
|
+
// invariant to catch, named as the defaults' here so the switch is total.
|
|
298
|
+
if (minimum !== undefined && minutes < minimum) {
|
|
299
|
+
return {
|
|
300
|
+
kind: "refused",
|
|
301
|
+
refusal: { axis: "minutes", needs: minimum, have: minutes, scope: boundedBy ?? "defaults" },
|
|
302
|
+
};
|
|
303
|
+
}
|
|
288
304
|
return {
|
|
289
305
|
kind: "profile",
|
|
290
306
|
profile: {
|
|
@@ -66,6 +66,14 @@ export interface Actor {
|
|
|
66
66
|
* → nothing: fail-closed, exactly today's behaviour.
|
|
67
67
|
*/
|
|
68
68
|
readonly memberOf?: ReadonlySet<string>;
|
|
69
|
+
/**
|
|
70
|
+
* The person an admin's dashboard session is viewing as (record 0053): set
|
|
71
|
+
* beside `onBehalfOf` = that person's actor, so the effective grants are the
|
|
72
|
+
* intersection (the person's) and `self`/`memberOf` are the person's, while
|
|
73
|
+
* `id` stays the admin's. Identity for the audit line and the banner; the
|
|
74
|
+
* registry door refuses every non-read effect while it is set. No rule reads it.
|
|
75
|
+
*/
|
|
76
|
+
readonly viewingAs?: { readonly id: string; readonly name?: string };
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
/** `<group>:<read|write|exec>` plus the non-command actions. A plain
|
|
@@ -196,4 +204,14 @@ export interface ChannelDirectory {
|
|
|
196
204
|
/** Every channel the actor is in, platform-namespaced — what the resolver puts on
|
|
197
205
|
* `Actor.memberOf`; `unknown` when the adapter cannot say (fail-closed). */
|
|
198
206
|
channelsOf(actorId: string): Promise<ReadonlySet<string> | "unknown">;
|
|
207
|
+
/** Every channel the bot itself is in, with its visibility — the channels a person can
|
|
208
|
+
* pick a scope or an MCP tier for (settings-page.md item 7); `unknown` when the adapter
|
|
209
|
+
* cannot list them, and the surfaces fall back to the channels that already carry a scope. */
|
|
210
|
+
channels(): Promise<readonly ListedChannel[] | "unknown">;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** One channel the bot is in, as the directory lists it. */
|
|
214
|
+
export interface ListedChannel {
|
|
215
|
+
id: string;
|
|
216
|
+
visibility: ChannelVisibility;
|
|
199
217
|
}
|
|
@@ -18,6 +18,31 @@
|
|
|
18
18
|
|
|
19
19
|
export const MINUTE_MS = 60_000;
|
|
20
20
|
|
|
21
|
+
/** One calendar day in milliseconds: the unit the daily cost and delivery ranges
|
|
22
|
+
* step by (`dayOf`, the day count of a range). A day is not a lease, but it is
|
|
23
|
+
* a duration, and every duration is read from this table rather than written
|
|
24
|
+
* as a literal where it is used. */
|
|
25
|
+
export const DAY_MS = 24 * 60 * MINUTE_MS;
|
|
26
|
+
|
|
27
|
+
/** How long the confirmation a routed write is offered as stays pending
|
|
28
|
+
* (docs/decisions/0044; docs/reference/specs/routing-and-config.md item 25):
|
|
29
|
+
* the connect ticket's ten minutes. The bot passes it to the config object,
|
|
30
|
+
* which stamps the expiry on its own clock. */
|
|
31
|
+
export const CONFIRMATION_TTL_MS = 10 * MINUTE_MS;
|
|
32
|
+
|
|
33
|
+
/** How long a dispatch's FIRST attach to a resident — a fresh run's, or a
|
|
34
|
+
* resumed run's re-attach to its recorded worktree — waits for the resident
|
|
35
|
+
* to wake when the Worker typed its refusal as the platform's transient
|
|
36
|
+
* (docs/reference/specs/execution.md item 9): a Durable Object reset or lost
|
|
37
|
+
* under the attach clears in seconds, so a minute is generous, and far under
|
|
38
|
+
* the wake ceiling a mid-run re-attach may take — the card is silent while
|
|
39
|
+
* this wait runs, then names it. It bounds the PROBING: past it no further
|
|
40
|
+
* probe is made and, with nothing bound, a fresh run falls cold and a resumed
|
|
41
|
+
* run is refused, the wait named either way; a re-attach already opened
|
|
42
|
+
* inside it runs to the attach's own timeout. The run's own stop ends any of
|
|
43
|
+
* it at once. Read by the executor factory. */
|
|
44
|
+
export const FIRST_ATTACH_WAIT_MS = MINUTE_MS;
|
|
45
|
+
|
|
21
46
|
/** The presets that run the tool loop, and the one pipeline preset. */
|
|
22
47
|
export const LOOP_PRESETS = ["general", "coding", "review", "research", "explore", "conductor"] as const;
|
|
23
48
|
export type LoopPreset = (typeof LOOP_PRESETS)[number];
|
|
@@ -115,6 +140,18 @@ export function postStepLease(preset: string, remainingMs: number | undefined):
|
|
|
115
140
|
return Math.min(allowance, Math.max(1, remainingMs / MINUTE_MS));
|
|
116
141
|
}
|
|
117
142
|
|
|
143
|
+
/** The least lease under which a loop-running preset does anything: the
|
|
144
|
+
* write-up and the preset's post-step are held back from every lease
|
|
145
|
+
* (`loopClock`), so a lease of that sum leaves the loop nothing — the run
|
|
146
|
+
* goes straight to its write-up and reports a budget it never had. One minute
|
|
147
|
+
* above the sum is the least that runs a turn. A profile clipped under this by
|
|
148
|
+
* a directive, a boundary or a parent's remainder is refused at the gate
|
|
149
|
+
* naming it, the way a ship round is refused under its floor; the floors are
|
|
150
|
+
* the larger numbers a round needs to be worth dispatching. */
|
|
151
|
+
export function leaseMinimum(preset: string): number {
|
|
152
|
+
return ALLOWANCES.writeUp + postStepMinutes(preset) + 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
118
155
|
/** A follow-up turn's lease, in ms: the lesser of what it asks and what the
|
|
119
156
|
* run's lease still holds, never under a minute — the grace covers that floor
|
|
120
157
|
* when the turn starts by the lease's end; later, the bearer may expire under
|
|
@@ -311,3 +348,27 @@ export function fit(pipeline: Pipeline): { ok: boolean; need: number; have: numb
|
|
|
311
348
|
const need = ALLOWANCES.provision + ASKS.coding + reserveMinutes({ kind: "coding", index: 0 }, pipeline);
|
|
312
349
|
return { ok: pipeline.maxMinutes >= need, need, have: pipeline.maxMinutes };
|
|
313
350
|
}
|
|
351
|
+
|
|
352
|
+
// ---- the grant (decision 0046, Renewal: the grant decides, the lease continues) ----
|
|
353
|
+
|
|
354
|
+
/** What the request authorizes for the whole problem beyond one lease: a count
|
|
355
|
+
* of renewals and a cost cap in dollars. A lease is sized by the infrastructure
|
|
356
|
+
* that must outlive it; the grant is sized by the person — a scope's word or
|
|
357
|
+
* the request's `renewals:` directive — and the runner spends it one segment at
|
|
358
|
+
* a time. Zero renewals and no cap by default, so nothing renews until someone
|
|
359
|
+
* says so; `costCapUsd` absent is no cap, never zero. */
|
|
360
|
+
export interface Grant {
|
|
361
|
+
renewals: number;
|
|
362
|
+
costCapUsd?: number;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Which layer's word the grant is: the request's directive, the user's scope,
|
|
366
|
+
* the channel's, or the org's `ship.grant` — the default counts as the org's. */
|
|
367
|
+
export type GrantSource = "org" | "channel" | "user" | "run";
|
|
368
|
+
|
|
369
|
+
export const DEFAULT_GRANT: Grant = { renewals: 0 };
|
|
370
|
+
|
|
371
|
+
/** The most renewals one request may carry: thirteen segments of the ship
|
|
372
|
+
* preset's ask are a day, the longest problem a person hands over in one
|
|
373
|
+
* message before a plan should carry it. */
|
|
374
|
+
export const GRANT_RENEWALS_MAX = 12;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// parent record the spawn route reads the requester from, and the names the
|
|
9
9
|
// routes decide on.
|
|
10
10
|
//
|
|
11
|
+
import type { Grant, GrantSource } from "../budgets.js";
|
|
11
12
|
import type { AddressSeverity, AddressSeveritySource } from "../ship/coordinator.js";
|
|
12
13
|
|
|
13
14
|
// A coordinator is a Workflow instance in the shim Worker whose children are
|
|
@@ -181,6 +182,13 @@ export interface CoordinatorInstance {
|
|
|
181
182
|
* the machine reads one value; absent reads as the default (`minor`, org). */
|
|
182
183
|
addressSeverity?: AddressSeverity;
|
|
183
184
|
addressSeveritySource?: AddressSeveritySource;
|
|
185
|
+
/** The grant (decision 0046, the renewable lease): the renewals and cost cap
|
|
186
|
+
* the request carries, resolved once by the ship fork (directive count >
|
|
187
|
+
* user > channel > org) and written here beside `merge`; absent reads as
|
|
188
|
+
* zero renewals and no cap, the org's. Nothing renews until the renewal
|
|
189
|
+
* decision reads it. */
|
|
190
|
+
grant?: Grant;
|
|
191
|
+
grantSource?: GrantSource;
|
|
184
192
|
/** The pipeline's caps as the profile gate clipped them: the rounds cap and the wall clock per unit. */
|
|
185
193
|
caps?: { maxRounds: number; maxMinutes: number };
|
|
186
194
|
/** The status card in the requesting thread, when the channel has one — what
|
|
@@ -195,6 +203,13 @@ export interface CoordinatorInstance {
|
|
|
195
203
|
attempt?: number;
|
|
196
204
|
}
|
|
197
205
|
|
|
206
|
+
export interface UnitSegment {
|
|
207
|
+
index: number;
|
|
208
|
+
from?: string;
|
|
209
|
+
runId?: string;
|
|
210
|
+
at: number;
|
|
211
|
+
}
|
|
212
|
+
|
|
198
213
|
/** One unit of the plan an instance runs (a task string is a generated plan of
|
|
199
214
|
* one unit, `U1`): its branch, the units it waits on, and — as the runner
|
|
200
215
|
* reaches it — its thread, its pull request, the round boundaries the card
|
|
@@ -229,6 +244,13 @@ export interface CoordinatorUnit {
|
|
|
229
244
|
* opened or updated): the next attempt's pre-check starts at the review
|
|
230
245
|
* round when the open pull request still heads exactly here. */
|
|
231
246
|
lastPush?: string;
|
|
247
|
+
/** The renewals the unit spent (decision 0046, Renewal): one row per segment
|
|
248
|
+
* the grant opened after the first, keyed by the segment's index — written
|
|
249
|
+
* by `unit-end` on a `continued` ending before the segment runs, so a runner
|
|
250
|
+
* reclaimed between a segment's end and its renewal finds the row and never
|
|
251
|
+
* renews the same segment twice. `from` is the sha the segment continues
|
|
252
|
+
* from, `runId` the coding run whose write-up briefs it. */
|
|
253
|
+
segments?: UnitSegment[];
|
|
232
254
|
/** The round boundaries the coordinator reported, oldest first (the `ship_round` vocabulary). */
|
|
233
255
|
rounds: Array<{ index: number; agent: string; outcome: string; at: number }>;
|
|
234
256
|
/** How the unit ended: the ending's kind and the thread's report, when it has. */
|
|
@@ -281,6 +303,15 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
|
|
|
281
303
|
}
|
|
282
304
|
|
|
283
305
|
/** Structural check on a unit row from outside the process. */
|
|
306
|
+
const isSegment = (v: unknown): boolean =>
|
|
307
|
+
isObject(v) &&
|
|
308
|
+
typeof v.index === "number" &&
|
|
309
|
+
Number.isInteger(v.index) &&
|
|
310
|
+
v.index >= 2 &&
|
|
311
|
+
(v.from === undefined || isText(v.from)) &&
|
|
312
|
+
(v.runId === undefined || isText(v.runId)) &&
|
|
313
|
+
typeof v.at === "number";
|
|
314
|
+
|
|
284
315
|
export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
285
316
|
if (!isObject(v)) return false;
|
|
286
317
|
const r = v;
|
|
@@ -293,6 +324,7 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
|
293
324
|
if (r.pr !== undefined && !isPr(r.pr)) return false;
|
|
294
325
|
if (r.resume !== undefined && !isResume(r.resume)) return false;
|
|
295
326
|
if (r.lastPush !== undefined && !isText(r.lastPush)) return false;
|
|
327
|
+
if (r.segments !== undefined && (!Array.isArray(r.segments) || !r.segments.every(isSegment))) return false;
|
|
296
328
|
if (
|
|
297
329
|
!Array.isArray(r.rounds) ||
|
|
298
330
|
r.rounds.length > MAX_ROUNDS ||
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
// plan's re-issue. A task string's ship branch waits for a person. Node-free:
|
|
34
34
|
// the shim Worker imports this by relative path.
|
|
35
35
|
|
|
36
|
+
import { DEFAULT_GRANT, GRANT_RENEWALS_MAX, type Grant, type GrantSource } from "../budgets.js";
|
|
36
37
|
import {
|
|
37
38
|
applyReturn,
|
|
38
39
|
cursorFinished,
|
|
@@ -55,6 +56,8 @@ import {
|
|
|
55
56
|
type StepReturn,
|
|
56
57
|
type UnitEnding,
|
|
57
58
|
type UnitPipelineState,
|
|
59
|
+
stepPrefixOf,
|
|
60
|
+
type UnitSession,
|
|
58
61
|
} from "../ship/coordinator.js";
|
|
59
62
|
import { checksSettledEventType, isCoordinatorUnit, runFinishedEventType, type CoordinatorUnit } from "./contract.js";
|
|
60
63
|
|
|
@@ -164,6 +167,9 @@ interface PlanFacts {
|
|
|
164
167
|
/** The severity to address, beside `merge`: the level an approve's findings are held to, and which layer set it. */
|
|
165
168
|
addressSeverity: AddressSeverity;
|
|
166
169
|
addressSeveritySource: AddressSeveritySource;
|
|
170
|
+
/** The grant beside them (decision 0046): what a renewal could spend, and which layer granted it. */
|
|
171
|
+
grant: Grant;
|
|
172
|
+
grantSource: GrantSource;
|
|
167
173
|
/** The instance's mark as the plan route answers it: a generated one-unit plan (a `plan` with no `path`). */
|
|
168
174
|
generated: boolean;
|
|
169
175
|
repo: string;
|
|
@@ -176,6 +182,21 @@ const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === "obj
|
|
|
176
182
|
const isMinutes = (v: unknown): v is Record<string, number> =>
|
|
177
183
|
isRecord(v) && Object.values(v).every((n) => typeof n === "number" && Number.isFinite(n));
|
|
178
184
|
|
|
185
|
+
/** The grant as the plan route answers it; anything unreadable — a count
|
|
186
|
+
* outside the module's ceiling included — is the default: nothing renews on a guess. */
|
|
187
|
+
function readGrant(raw: unknown): Grant {
|
|
188
|
+
if (
|
|
189
|
+
!isRecord(raw) ||
|
|
190
|
+
typeof raw.renewals !== "number" ||
|
|
191
|
+
!Number.isInteger(raw.renewals) ||
|
|
192
|
+
raw.renewals < 0 ||
|
|
193
|
+
raw.renewals > GRANT_RENEWALS_MAX
|
|
194
|
+
)
|
|
195
|
+
return DEFAULT_GRANT;
|
|
196
|
+
const cap = raw.costCapUsd;
|
|
197
|
+
return { renewals: raw.renewals, ...(typeof cap === "number" && cap > 0 ? { costCapUsd: cap } : {}) };
|
|
198
|
+
}
|
|
199
|
+
|
|
179
200
|
function readPlan(a: BotAnswer): PlanFacts {
|
|
180
201
|
const b = a.body;
|
|
181
202
|
if (b.ok !== true) throw new UnreadableAnswer("plan", a, "not ok");
|
|
@@ -192,6 +213,9 @@ function readPlan(a: BotAnswer): PlanFacts {
|
|
|
192
213
|
b.addressSeveritySource === "run" || b.addressSeveritySource === "user" || b.addressSeveritySource === "channel"
|
|
193
214
|
? b.addressSeveritySource
|
|
194
215
|
: "org",
|
|
216
|
+
grant: readGrant(b.grant),
|
|
217
|
+
grantSource:
|
|
218
|
+
b.grantSource === "run" || b.grantSource === "user" || b.grantSource === "channel" ? b.grantSource : "org",
|
|
195
219
|
generated: b.generated === true,
|
|
196
220
|
repo: b.repo,
|
|
197
221
|
base: b.base,
|
|
@@ -252,6 +276,10 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
252
276
|
reviewHead,
|
|
253
277
|
dispositions,
|
|
254
278
|
handoff,
|
|
279
|
+
pushed,
|
|
280
|
+
leaseStartedAt,
|
|
281
|
+
costUsd,
|
|
282
|
+
handoffLists,
|
|
255
283
|
} = facts;
|
|
256
284
|
return {
|
|
257
285
|
type: "read-record",
|
|
@@ -269,6 +297,11 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
269
297
|
...(reviewHead !== undefined ? { reviewHead } : {}),
|
|
270
298
|
...(dispositions !== undefined ? { dispositions } : {}),
|
|
271
299
|
...(handoff !== undefined ? { handoff } : {}),
|
|
300
|
+
// The renewal's facts (decision 0046), as the record carries them.
|
|
301
|
+
...(Array.isArray(pushed) ? { pushed } : {}),
|
|
302
|
+
...(typeof leaseStartedAt === "number" ? { leaseStartedAt } : {}),
|
|
303
|
+
...(typeof costUsd === "number" || costUsd === null ? { costUsd } : {}),
|
|
304
|
+
...(handoffLists !== undefined ? { handoffLists } : {}),
|
|
272
305
|
},
|
|
273
306
|
at: a.body.at,
|
|
274
307
|
};
|
|
@@ -457,11 +490,16 @@ async function runUnit(
|
|
|
457
490
|
instanceId: string,
|
|
458
491
|
node: PlanUnitNode,
|
|
459
492
|
plan: PlanFacts,
|
|
493
|
+
session?: UnitSession,
|
|
460
494
|
): Promise<UnitEnding> {
|
|
461
495
|
const unit = node.id;
|
|
496
|
+
// A renewal's segment names its steps under the segment (`U10/s2/…`), so
|
|
497
|
+
// the Workflow's durable step cache never answers segment two with segment
|
|
498
|
+
// one's results (decision 0046).
|
|
499
|
+
const prefix = stepPrefixOf(unit, session);
|
|
462
500
|
const tag = { parentInstanceId: instanceId, unit };
|
|
463
501
|
const start = readUnitStart(
|
|
464
|
-
answerOf("unit-start", await step.do(`${
|
|
502
|
+
answerOf("unit-start", await step.do(`${prefix}/start`, STEP_CONFIG, () => call(bot, "unit-start", tag))),
|
|
465
503
|
);
|
|
466
504
|
// A resume at review (agent-ship item 10) rides the unit's row: the pull
|
|
467
505
|
// request of ship's own the requester named opens the pipeline at its first
|
|
@@ -484,9 +522,12 @@ async function runUnit(
|
|
|
484
522
|
merge: plan.merge,
|
|
485
523
|
addressSeverity: plan.addressSeverity,
|
|
486
524
|
addressSeveritySource: plan.addressSeveritySource,
|
|
525
|
+
grant: plan.grant,
|
|
526
|
+
grantSource: plan.grantSource,
|
|
487
527
|
generated: plan.generated,
|
|
488
528
|
...(resume !== undefined ? { resume } : {}),
|
|
489
529
|
...(lastPush !== undefined ? { lastPush } : {}),
|
|
530
|
+
...(session !== undefined ? { session } : {}),
|
|
490
531
|
},
|
|
491
532
|
start.at,
|
|
492
533
|
);
|
|
@@ -499,7 +540,7 @@ async function runUnit(
|
|
|
499
540
|
for (const note of transition.notes) {
|
|
500
541
|
if (note.type === "round") {
|
|
501
542
|
const body = { ...tag, index: note.index, agent: note.agent, outcome: note.outcome };
|
|
502
|
-
await step.do(`${
|
|
543
|
+
await step.do(`${prefix}/note/${++notes}`, STEP_CONFIG, () => call(bot, "round", body));
|
|
503
544
|
} else {
|
|
504
545
|
// A merge_ready ending names the pull request as it is at the APPROVED
|
|
505
546
|
// head (agent-ship item 9): one more pr-check reads the facts fresh —
|
|
@@ -511,10 +552,10 @@ async function runUnit(
|
|
|
511
552
|
if (note.ending.kind === "merge_ready") {
|
|
512
553
|
try {
|
|
513
554
|
const check = prCheckReturn(
|
|
514
|
-
`${
|
|
555
|
+
`${prefix}/end/pr-facts`,
|
|
515
556
|
answerOf(
|
|
516
557
|
"pr-check",
|
|
517
|
-
await step.do(`${
|
|
558
|
+
await step.do(`${prefix}/end/pr-facts`, STEP_CONFIG, () => call(bot, "pr-check", tag)),
|
|
518
559
|
),
|
|
519
560
|
);
|
|
520
561
|
if (check.type === "pr-check" && check.pr.state === "merged")
|
|
@@ -538,8 +579,20 @@ async function runUnit(
|
|
|
538
579
|
? { headSha: note.ending.headSha }
|
|
539
580
|
: {}),
|
|
540
581
|
...(state.lastCodingRunId !== undefined ? { codingRunId: state.lastCodingRunId } : {}),
|
|
582
|
+
// A continued ending is a segment's end, not the unit's: the bot
|
|
583
|
+
// writes the renewal as a row keyed by the next segment's index
|
|
584
|
+
// (decision 0046), so a runner reclaimed here never renews twice.
|
|
585
|
+
...(note.ending.kind === "continued"
|
|
586
|
+
? {
|
|
587
|
+
segment: {
|
|
588
|
+
index: note.ending.segment,
|
|
589
|
+
...(note.ending.from !== undefined ? { from: note.ending.from } : {}),
|
|
590
|
+
runId: note.ending.runId,
|
|
591
|
+
},
|
|
592
|
+
}
|
|
593
|
+
: {}),
|
|
541
594
|
};
|
|
542
|
-
await step.do(`${
|
|
595
|
+
await step.do(`${prefix}/end`, STEP_CONFIG, () => call(bot, "unit-end", body));
|
|
543
596
|
}
|
|
544
597
|
}
|
|
545
598
|
}
|
|
@@ -573,13 +626,23 @@ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string):
|
|
|
573
626
|
const [next] = readyUnits(graph, cursor);
|
|
574
627
|
if (next === undefined) break;
|
|
575
628
|
cursor = startUnit(graph, cursor, next);
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
)
|
|
629
|
+
const node = graph.units.find((u) => u.id === next)!;
|
|
630
|
+
let ending = await runUnit(step, bot, instanceId, node, plan);
|
|
631
|
+
// The lease continues while the grant renews (decision 0046): each
|
|
632
|
+
// `continued` ending opens the next segment of the same unit — a fresh
|
|
633
|
+
// pipeline under a fresh lease, from the recorded sha, briefed with the
|
|
634
|
+
// previous segment's write-up — until the unit ends some other way.
|
|
635
|
+
while (ending.kind === "continued") {
|
|
636
|
+
const c = ending;
|
|
637
|
+
ending = await runUnit(step, bot, instanceId, node, plan, {
|
|
638
|
+
segment: c.segment,
|
|
639
|
+
renewalsSpent: c.segment - 1,
|
|
640
|
+
spendUsd: c.spendUsd,
|
|
641
|
+
...(c.from !== undefined ? { continueFrom: c.from } : {}),
|
|
642
|
+
previousRunId: c.runId,
|
|
643
|
+
...(c.handoff !== undefined ? { previousHandoff: c.handoff } : {}),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
583
646
|
endings[next] = ending.kind;
|
|
584
647
|
cursor = settleUnit(graph, cursor, next, ending.kind === "merged" ? "done" : "failed");
|
|
585
648
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { snapshotAgeText } from "./snapshotAge.js";
|
|
2
|
+
import { DAY_MS } from "./budgets.js";
|
|
2
3
|
import { systemClock } from "./trace/clock.js";
|
|
3
4
|
|
|
4
5
|
// Delivery indicators: what the run history and the pull requests' own facts
|
|
@@ -225,7 +226,6 @@ export function weekIncomplete(report: Pick<DeliveryReport, "truncated" | "compl
|
|
|
225
226
|
|
|
226
227
|
// ---- the arithmetic ---------------------------------------------------------------------
|
|
227
228
|
|
|
228
|
-
const DAY_MS = 86_400_000;
|
|
229
229
|
const HOUR_MS = 3_600_000;
|
|
230
230
|
|
|
231
231
|
/** Add `n` calendar days to a UTC date (`YYYY-MM-DD`). */
|