@coreplane/switchboard 1.245.0 → 1.246.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/deploy/cloudflare-resident/worker.ts +9 -10
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +17 -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 +31 -7
- package/dist/assets/src/core/budgets.ts +18 -0
- package/dist/assets/src/core/coordinator/contract.ts +29 -4
- package/dist/assets/src/core/coordinator/driver.ts +7 -1
- package/dist/assets/src/core/reviewVerdict.ts +138 -6
- package/dist/assets/src/core/runEvents.ts +29 -1
- package/dist/assets/src/core/runRecord.ts +38 -16
- package/dist/assets/src/core/ship/coordinator.ts +23 -2
- package/dist/assets/src/core/ship/renewal.ts +10 -3
- package/dist/assets/src/execution/bashTimeout.ts +46 -2
- package/dist/assets/src/execution/residentRefresh.ts +9 -5
- package/dist/assets/src/execution/sandboxErrors.ts +9 -4
- package/dist/assets/web/dist/.vite/manifest.json +468 -468
- package/dist/assets/web/dist/assets/{AppShell-qqs_p96-.js → AppShell-B4EUiSwP.js} +1 -1
- package/dist/assets/web/dist/assets/{CostsPage-Dd9aXxxI.js → CostsPage-D8VjAtuH.js} +1 -1
- package/dist/assets/web/dist/assets/DeliveryPage-DpQi5XUI.js +1 -0
- package/dist/assets/web/dist/assets/{HomePage-VLTFopnl.js → HomePage-CvKuIGqg.js} +1 -1
- package/dist/assets/web/dist/assets/{InputMenu-lQP3ia9D.js → InputMenu-DmqAxkn4.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-8ViskpN3.js → NotFoundPage-DDogyiXA.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-D2gHef5v.js → PendingTurnRow-BoSLlR6j.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-nWLN8BOd.js → ResidentDetailPage-CiBdbzkE.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-D3Q48CJW.js → ResidentsIndexPage-DgtHtd9K.js} +1 -1
- package/dist/assets/web/dist/assets/{RunFoldRow-CHy_IfZG.js → RunFoldRow-IF4xbz-3.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-C6kX5om4.js → RunRoutePage-DHYa7rHK.js} +3 -3
- package/dist/assets/web/dist/assets/RunsIndexPage-aWhNTv8r.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-WyeG5tKb.js → RunsTabs-CY_tdGTU.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-B8D5AU4d.js → ScheduledPage-gPEBZjcx.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingSelect-UTHsqUUS.js → SettingSelect-VDanrTZi.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-BoW7Zjzt.js → SettingsPage-CFoltSRD.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-BGHureCJ.js → StatusDot-DA3eTVrT.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-CiP8qPd7.js → Tooltip-BgbCkM12.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-2zEeRBP1.js → UnitRoutePage-DibegaJJ.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-html-qXhb9ti8.js → angular-html-D_3slZqf.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-DzvdyoUZ.js → angular-ts-D1Fv2cpN.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-Ck7Ru2Sm.js → apl-60Nm_trN.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-CwehvReb.js → astro-CvyaM3Q9.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-WSyVbSFK.js → blade-yan3VBls.js} +1 -1
- package/dist/assets/web/dist/assets/{c-DMHI1wDz.js → c-Ddgepap2.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-BF8AYK8_.js → chapel-CgNY6dxC.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-UAfImuWu.js → cobol-C7q82cdP.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-D54hi96d.js → coffee-DgRMF_ni.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-BRWfLgBc.js → cpp-B8Lf-aeo.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CSqC5deT.js → crystal-CkA_dhEO.js} +1 -1
- package/dist/assets/web/dist/assets/{css-D4ZuRFGk.js → css-Cyu-C_oA.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-73ZMn_Mg.js → dist-DS-gMjLZ.js} +2 -2
- package/dist/assets/web/dist/assets/{durationTone-D51enaGR.js → durationTone-l6aVrHb8.js} +1 -1
- package/dist/assets/web/dist/assets/{edge-DDPtNbZc.js → edge-BxV3rKNI.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-BTQkQfsJ.js → elixir-pNSEK39d.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-B_PkLzGZ.js → elm-zgwWE33W.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-DfjIMVM_.js → erb-yzcYAHp_.js} +1 -1
- package/dist/assets/web/dist/assets/{eventSource-By0B_w4S.js → eventSource-Ce94ho6M.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-DP8wmn7K.js → git-rebase-Dg7Ous1-.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-G12VK2gK.js → glimmer-js-CbZEwX27.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-D3XiSI8E.js → glimmer-ts-D3NIqALy.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-DOKAhMj-.js → glsl-y4-3WrI2.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-CJB_fmON.js → graphql-D5d0al7K.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BHqtRWap.js → hack-D8fD6kEl.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DwotyarS.js → haml-BWeFFc-V.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-B6fDaA3U.js → handlebars-gIm5FeE_.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-C5bpQmmI.js → html-derivative-B3ySSBrQ.js} +1 -1
- package/dist/assets/web/dist/assets/{html-s52taj36.js → html-fXJTVLSM.js} +1 -1
- package/dist/assets/web/dist/assets/{http-99zC935r.js → http-DrJfQUmw.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-Bgk-4-0x.js → hurl-Bvbnsf55.js} +1 -1
- package/dist/assets/web/dist/assets/{java-CuV_c5I0.js → java-CUSpb-i-.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-CZlOvSMQ.js → javascript-BWRH5fbY.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-BuXeZ4Tu.js → jinja-D3Ymci8y.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DIuP0hkG.js → jison-Gt9SO7ZC.js} +1 -1
- package/dist/assets/web/dist/assets/{json-CZ74vfoD.js → json-Cfcblhgn.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-BNjRs8Vj.js → jsx-C0366fFM.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-OCE-fFOg.js → julia-CyBOTbxS.js} +1 -1
- package/dist/assets/web/dist/assets/{just-CtvD87WF.js → just-FPXZcIOd.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-C3kf86OR.js → latex-Bd-c88v0.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-B1WI--Kp.js → liquid-5tu9Hc0k.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-DtoT0gKg.js → lua-BZ5fdPP8.js} +1 -1
- package/dist/assets/web/dist/assets/{main-ClluP-Qf.js → main-B4A6x82d.js} +3 -3
- package/dist/assets/web/dist/assets/{marko-BE2MC6Yt.js → marko-C_mmj4p0.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-BwO4weXJ.js → mdc-CtaNqWjY.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-BTuOgGVC.js → nginx-Ddm5Mb8J.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-CiIUM-oV.js → nim-COhfroYB.js} +1 -1
- package/dist/assets/web/dist/assets/{org-L78DEWB-.js → org-CCHFgmTb.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-KqHOm4Ft.js → perl-CXzOE-Z-.js} +1 -1
- package/dist/assets/web/dist/assets/{php-C41XbEkn.js → php-1V6jAIhU.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-B12-4i-k.js → pug-uS37efts.js} +1 -1
- package/dist/assets/web/dist/assets/{qml--fw2Up5c.js → qml-Bj1A0Ben.js} +1 -1
- package/dist/assets/web/dist/assets/{r-Db1JBXrC.js → r-CMbKbLsS.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BYpVBgbu.js → razor-DVRq_Ndk.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-BMqGLSPc.js → regexp-AbaOr4oB.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-B63n7-Sa.js → rst-Dq_QWgOF.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-D3VI8UYv.js → ruby-CVSsAj7T.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-DxRf8ZAR.js → sas-C-dGfxUi.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-DRRA6d6A.js → scss-DxALxYPA.js} +1 -1
- package/dist/assets/web/dist/assets/{seed-DFfT5S_N.js → seed-BhxEVasY.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-CoOBe3_y.js → shellscript--qXcvQ5L.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-DOV1HYxf.js → shellsession-DeqnW8Ux.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-Acpmpp1W.js → soy-DreoUIMt.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-DeT9N-fA.js → sql-o3i2VTG6.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-DPXVxhL5.js → sseReplay-qF-gpbAY.js} +1 -1
- package/dist/assets/web/dist/assets/{stata-CbxXAMO4.js → stata-CGAT41H7.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-BdKbi4hc.js → surrealql-CBU0QuGV.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-tAcjDM7R.js → svelte-CfHaLZiG.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-CObINNJj.js → templ-D02MUcma.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BYwpZT1J.js → tex-DjprHYXl.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-BciREL5R.js → ts-tags-DftDOzP-.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-CRiv3Pr3.js → tsx-BOdvPohJ.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-DuZZ1Ej0.js → twig-B_yLS56B.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-jkHz_5HZ.js → typescript-CdPCa4nU.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-DOac4Gal.js → typst-C1oIFsUl.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CgOX5wXA.js → vue-CcszJEX8.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-Duu99VNw.js → vue-html-DcpxPcuT.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-Bmj_wvRJ.js → vue-vine-D26bIKGj.js} +1 -1
- package/dist/assets/web/dist/assets/{wallClock-DD_fPuSY.js → wallClock-DPTuP2FD.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-BtUIVeWH.js → xml-D8zl6W5P.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-wX71n1E5.js → xsl-D86Bii28.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-CdLdXvmz.js → yaml-g6Px4KFU.js} +1 -1
- package/dist/cli.js +1750 -566
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/DeliveryPage-D90FTjYq.js +0 -1
- package/dist/assets/web/dist/assets/RunsIndexPage-DjxGj1Q5.js +0 -1
|
@@ -8906,19 +8906,18 @@ async function handleOp(env: Env, body: Record<string, unknown>, traceparent?: s
|
|
|
8906
8906
|
return streamOp(stub.runOp(op as "test" | "build", ref, traceparent));
|
|
8907
8907
|
}
|
|
8908
8908
|
|
|
8909
|
-
/** /op's payload mapping: results pass through; a
|
|
8910
|
-
*
|
|
8911
|
-
*
|
|
8912
|
-
*
|
|
8909
|
+
/** /op's payload mapping: results pass through; a failure streams as the typed
|
|
8910
|
+
* document it is — the named error with its `status` IN the body, a pending
|
|
8911
|
+
* op that rejected as the typed 500 (`catchAllErr`, so `transient` rides
|
|
8912
|
+
* beside the words) — exactly as `/exec`, `/attach` and `/await-restore`
|
|
8913
|
+
* stream theirs, so the client reads the status off the document by the one
|
|
8914
|
+
* rule (`answeredStatus`) and judges `transient` by the one rule too
|
|
8915
|
+
* (`isTransientRefusal`: a 5xx carrying it), never by the field on a 200. */
|
|
8913
8916
|
function streamOp(pending: Promise<Awaited<ReturnType<ResidentDO["runOp"]>>>): Response {
|
|
8914
|
-
const shedStatus = (failure: ThreadErr): object => {
|
|
8915
|
-
const { status: _status, ...rest } = failure;
|
|
8916
|
-
return rest;
|
|
8917
|
-
};
|
|
8918
8917
|
return streamHeartbeatJson(
|
|
8919
8918
|
pending,
|
|
8920
|
-
(result) =>
|
|
8921
|
-
(err) =>
|
|
8919
|
+
(result) => result,
|
|
8920
|
+
(err) => catchAllErr(err, "op-failed"),
|
|
8922
8921
|
);
|
|
8923
8922
|
}
|
|
8924
8923
|
|
|
@@ -111,6 +111,13 @@ const SDK_PIN: string = sandboxPkg.dependencies["@cloudflare/sandbox"];
|
|
|
111
111
|
|
|
112
112
|
const WORKDIR = "/workspace";
|
|
113
113
|
|
|
114
|
+
/** How long the SDK waits for the platform to grant a container instance
|
|
115
|
+
* before its start fails (docs/reference/specs/execution.md item 23). The
|
|
116
|
+
* SDK's default is 30 s and it retries, so a burst that the platform would
|
|
117
|
+
* not admit took 63 s to say so; at 10 s (the SDK allows 5 s to 300 s) the
|
|
118
|
+
* refusal reaches the start gate — and the executor's wait — in seconds. */
|
|
119
|
+
const INSTANCE_GET_TIMEOUT_MS = 10_000;
|
|
120
|
+
|
|
114
121
|
// Default per-command time limit, enforced by coreutils `timeout` inside the
|
|
115
122
|
// sandbox. The tuned 280s applies when the body carries no timeoutMs (an older
|
|
116
123
|
// bot); a caller-supplied timeoutMs is clamped server-side to the shared
|
|
@@ -806,7 +813,16 @@ export default {
|
|
|
806
813
|
// One sandbox per thread; the DO name is the thread key. The stub's own
|
|
807
814
|
// methods (`runCommand`, `readText`, …) are what the routes call — the
|
|
808
815
|
// work happens in the Durable Object, the data comes back over RPC.
|
|
809
|
-
const sandbox = getSandbox(env.Sandbox, threadKey
|
|
816
|
+
const sandbox = getSandbox(env.Sandbox, threadKey, {
|
|
817
|
+
containerTimeouts: {
|
|
818
|
+
// A refused instance grant is learned in seconds, not after the SDK's
|
|
819
|
+
// 30 s default and its retries (63 s to "no container instance" under
|
|
820
|
+
// a burst, docs/reference/specs/execution.md item 23): the gate turns
|
|
821
|
+
// it into the wait token and the executor re-sends. A grant that is
|
|
822
|
+
// merely slow is re-asked on the next send, which finds it made.
|
|
823
|
+
instanceGetTimeoutMS: INSTANCE_GET_TIMEOUT_MS,
|
|
824
|
+
},
|
|
825
|
+
});
|
|
810
826
|
|
|
811
827
|
const url = new URL(request.url);
|
|
812
828
|
const body = (await request.json().catch(() => ({}))) as Record<string, unknown>;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.246.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.246.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.246.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.246.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
|
@@ -209,6 +209,14 @@ Text stays in your message; do not attach what you can say.`;
|
|
|
209
209
|
// belongs in the agent's notes for the thread, and why — the one thing sure to
|
|
210
210
|
// survive a compaction and reach the next run there — beside the reach `recall`
|
|
211
211
|
// gives into every earlier turn. Said once so the prompts cannot drift on it.
|
|
212
|
+
/** The one rule every preset carries about the length of what it says: the
|
|
213
|
+
* answer is read in a chat thread, so it leads with the outcome and stops
|
|
214
|
+
* when the outcome is said. Spelled the same in every prompt; the registry
|
|
215
|
+
* test pins it. A review's verdict and findings are rendered by code from the
|
|
216
|
+
* typed verdict, so there the rule governs the write-up alone. */
|
|
217
|
+
export const BREVITY_RULE =
|
|
218
|
+
"SAY LESS. Your answer is read in a chat thread, often on a phone: lead with the answer or the outcome in one sentence, then only what the reader needs to act on it. Never restate the question or recap what was asked, never describe your method or list what you checked (that is your notes' job), never list options you are not recommending, never end with an offer or a question about what to do next — when the answer is complete, stop. One idea per sentence; a bullet only for items that are truly parallel, one line each; no headings. A question gets a few sentences; a refusal gets one sentence and the nearest thing you can do; a report gets its findings and nothing around them. Length is right only when the person asked for depth or the content is the list they asked for (a table, a diff, a plan).";
|
|
219
|
+
|
|
212
220
|
/** The one rule every preset carries about text it did not receive from the
|
|
213
221
|
* person (record 0037): a linked thread, a stored record, a page someone
|
|
214
222
|
* else wrote, arrives inside the untrusted fence and is quoted data. Spelled
|
|
@@ -265,6 +273,7 @@ ${statusCardRule('"Clone repo and read the diff", "Run the test suite"')}
|
|
|
265
273
|
|
|
266
274
|
If the request doesn't name a repository and you can't infer it, ask for it instead of guessing.
|
|
267
275
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
276
|
+
${BREVITY_RULE}
|
|
268
277
|
${FENCED_CONTENT_RULE}
|
|
269
278
|
Your final message is posted to Slack — keep it readable, lead with the outcome.`;
|
|
270
279
|
|
|
@@ -310,6 +319,7 @@ ${NOTEPAD}
|
|
|
310
319
|
${statusCardRule()}
|
|
311
320
|
|
|
312
321
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
322
|
+
${BREVITY_RULE}
|
|
313
323
|
${FENCED_CONTENT_RULE}
|
|
314
324
|
Your final message is posted to Slack — keep it readable, lead with the outcome.`;
|
|
315
325
|
|
|
@@ -352,6 +362,7 @@ ${NOTEPAD}
|
|
|
352
362
|
${statusCardRule()}
|
|
353
363
|
|
|
354
364
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
365
|
+
${BREVITY_RULE}
|
|
355
366
|
${FENCED_CONTENT_RULE}
|
|
356
367
|
Your final message is posted to Slack — keep it readable, lead with the outcome.`;
|
|
357
368
|
|
|
@@ -359,7 +370,13 @@ Your final message is posted to Slack — keep it readable, lead with the outcom
|
|
|
359
370
|
// (docs/reference/specs/agent-ship.md item 6) lives here once — stable ids, the severity
|
|
360
371
|
// vocabulary, the severity gate's downgrade (agent-review.md item 5a) — so the
|
|
361
372
|
// sandbox and resident variants can never drift apart on it.
|
|
362
|
-
|
|
373
|
+
// The write-up is the review's text alone (docs/reference/specs/agent-review.md
|
|
374
|
+
// item 5b): the verdict line and the findings list are rendered by code from
|
|
375
|
+
// the submit_verdict call — on GitHub as the comment's head, in Slack as the
|
|
376
|
+
// whole reply — so the prose never repeats them and never pads around them.
|
|
377
|
+
const REVIEW_FINAL_MESSAGE = `YOUR FINAL MESSAGE IS THE REVIEW'S TEXT, NOTHING ELSE. Switchboard renders the verdict line and the findings list from your submit_verdict call — on GitHub as the head of the comment, in Slack as the whole reply — and folds your final message under them on GitHub as the full review. So write only what the list cannot carry: one short paragraph per finding, keyed by its id (what is wrong, the concrete failure, the fix). Do not restate the verdict or the findings, do not summarize what you read, do not list what you verified clean, do not describe your method — what you checked belongs in your notes, which the run page shows. A change with no findings needs one sentence, not a tour.`;
|
|
378
|
+
|
|
379
|
+
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, lists the findings under it and folds your text below them as the full review; 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
380
|
|
|
364
381
|
// The diff-gated spec review (docs/reference/specs/agent-review.md item 14) and
|
|
365
382
|
// the test guard under it (item 16; specs-coverage.md item 6), one text for
|
|
@@ -416,8 +433,9 @@ ${NOTEPAD}
|
|
|
416
433
|
|
|
417
434
|
${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
|
|
418
435
|
|
|
436
|
+
${BREVITY_RULE}
|
|
419
437
|
${FENCED_CONTENT_RULE}
|
|
420
|
-
|
|
438
|
+
${REVIEW_FINAL_MESSAGE}`;
|
|
421
439
|
|
|
422
440
|
// Resident-path variant for review (docs/reference/specs/resident-repos.md): same
|
|
423
441
|
// gather-once discipline, but against the ready worktree with git — the
|
|
@@ -449,8 +467,9 @@ ${NOTEPAD}
|
|
|
449
467
|
|
|
450
468
|
${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
|
|
451
469
|
|
|
470
|
+
${BREVITY_RULE}
|
|
452
471
|
${FENCED_CONTENT_RULE}
|
|
453
|
-
|
|
472
|
+
${REVIEW_FINAL_MESSAGE}`;
|
|
454
473
|
|
|
455
474
|
// Seeded-sandbox variant for review (docs/reference/specs/execution.md item 26):
|
|
456
475
|
// the gather-once discipline against a checkout that is already at the PR
|
|
@@ -483,8 +502,9 @@ ${NOTEPAD}
|
|
|
483
502
|
|
|
484
503
|
${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
|
|
485
504
|
|
|
505
|
+
${BREVITY_RULE}
|
|
486
506
|
${FENCED_CONTENT_RULE}
|
|
487
|
-
|
|
507
|
+
${REVIEW_FINAL_MESSAGE}`;
|
|
488
508
|
|
|
489
509
|
// Research agent: no repo, no workspace — just web search + URL
|
|
490
510
|
// reading, so a user can drop a link or ask a research question and get an
|
|
@@ -492,7 +512,7 @@ Your final message is posted to Slack. Lead with a one-line verdict, then the fi
|
|
|
492
512
|
// fast and tool-less.
|
|
493
513
|
const RESEARCH_SYSTEM = `You are Switchboard's research agent, answering a request from Slack.
|
|
494
514
|
|
|
495
|
-
You have no workspace and cannot run commands or clone repos. Your tools: \`web_search\` (find sources), \`web_fetch\` (read a public URL — pages as text; image and PDF links come back as the image/document itself), and the GitHub tools — \`github_repos\` (the org repositories you can reach, private ones included), \`github_tree\` / \`github_file\` (browse and read their files at any ref), \`github_search_code\`,
|
|
515
|
+
You have no workspace and cannot run commands or clone repos. Your tools: \`web_search\` (find sources), \`web_fetch\` (read a public URL — pages as text; image and PDF links come back as the image/document itself), and the GitHub tools — \`github_repos\` (the org repositories you can reach, private ones included), \`github_tree\` / \`github_file\` (browse and read their files at any ref), \`github_search_code\`, \`github_issue_list\` / \`github_issue_get\`, and \`github_actions_run\` / \`github_actions_job_log\` (a GitHub Actions run, its jobs, a job's errors and log tail). They use Switchboard's own GitHub credential, so a private repo of ours is readable — never conclude a repo is inaccessible from a public-web 404; use the GitHub tools.
|
|
496
516
|
|
|
497
517
|
How to work:
|
|
498
518
|
1. If the user gave a URL, read it first — a github.com URL to one of our repos with github_file/github_tree (web_fetch cannot see private repos), anything else with web_fetch. If they asked about Switchboard or one of our repos, read the repo (README, AGENTS.md, \`docs/reference/specs/*.md\` specs, the code) with github_tree / github_file / github_search_code before answering. For a general question, web_search for good sources, then web_fetch the most promising 1-3 to read the actual content — don't answer from snippets alone when the page is readable.
|
|
@@ -501,6 +521,7 @@ How to work:
|
|
|
501
521
|
|
|
502
522
|
${statusCardRule('"Search the sources", "Write the answer"')}
|
|
503
523
|
|
|
524
|
+
${BREVITY_RULE}
|
|
504
525
|
${FENCED_CONTENT_RULE}
|
|
505
526
|
Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks). Your final message is posted to Slack — lead with the answer, then supporting detail and sources.`;
|
|
506
527
|
|
|
@@ -510,10 +531,11 @@ Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks
|
|
|
510
531
|
// everyday asks ("open an issue on X", "what does our resident system do?",
|
|
511
532
|
// "what's in that link?") are answered here instead of bounced to a directive.
|
|
512
533
|
const GENERAL_SYSTEM = `You are Switchboard, a helpful assistant answering requests from Slack.
|
|
534
|
+
${BREVITY_RULE}
|
|
513
535
|
${FENCED_CONTENT_RULE}
|
|
514
536
|
Answer directly and concisely. Use Slack-friendly formatting (no markdown headers; use *bold*, bullets, and code blocks).
|
|
515
537
|
|
|
516
|
-
Your tools work without a workspace: the GitHub tools — \`github_repos\` (the org repositories you can reach), \`github_tree\` / \`github_file\` / \`github_search_code\` (browse, read, search their code and docs, private repos included), \`github_issue_list\` / \`github_issue_get\` (read issues), \`github_issue_create\` / \`github_issue_update\` / \`github_issue_comment\` / \`github_issue_delete\` (act on issues) — and \`web_fetch\` (read a public URL). Use them: when the user names a repo loosely ("the switchboard app"), resolve it with github_repos (or the thread) rather than asking; when asked about one of our repos, read it before answering. Report exactly what a tool did (issue number + URL) — never claim an action you did not perform, and never fabricate file contents, URLs, or command output.
|
|
538
|
+
Your tools work without a workspace: the GitHub tools — \`github_repos\` (the org repositories you can reach), \`github_tree\` / \`github_file\` / \`github_search_code\` (browse, read, search their code and docs, private repos included), \`github_issue_list\` / \`github_issue_get\` (read issues), \`github_actions_run\` / \`github_actions_job_log\` (a GitHub Actions run, its jobs, and a failed job's errors and log tail — "why did this run fail?"), \`github_issue_create\` / \`github_issue_update\` / \`github_issue_comment\` / \`github_issue_delete\` (act on issues) — and \`web_fetch\` (read a public URL). Use them: when the user names a repo loosely ("the switchboard app"), resolve it with github_repos (or the thread) rather than asking; when asked about one of our repos, read it before answering. Report exactly what a tool did (issue number + URL) — never claim an action you did not perform, and never fabricate file contents, URLs, or command output.
|
|
517
539
|
|
|
518
540
|
${statusCardRule('"Read the issue and its thread", "Post the comment"')} A one-step answer needs no checklist; post one when the request has steps the person would wait on.
|
|
519
541
|
|
|
@@ -548,6 +570,7 @@ ${statusCardRule('"Clone and install", "Time the full suite"')}
|
|
|
548
570
|
|
|
549
571
|
${NOTEPAD}
|
|
550
572
|
|
|
573
|
+
${BREVITY_RULE}
|
|
551
574
|
${FENCED_CONTENT_RULE}
|
|
552
575
|
Report outcomes faithfully: a check you could not run is "could not check", never a guess. Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks — render the claim table as aligned rows inside a code block). Your final message is posted to Slack: lead with the overall verdict in one line, then the claim table, then what a follow-up should do.`;
|
|
553
576
|
|
|
@@ -576,7 +599,7 @@ function conductorSystem(siblings: readonly AgentDef[]): string {
|
|
|
576
599
|
);
|
|
577
600
|
return `You are Switchboard's conductor: you coordinate other runs instead of doing the work yourself, answering a request from Slack.
|
|
578
601
|
|
|
579
|
-
You have no workspace and no shell. Your tools: \`spawn_run\` (start a child run), \`send_to_run\` (steer a live child: your text reaches it as a follow-up at its next step), \`await_runs\` (wait for your children to end and get each end — its status and final reply — back as data), \`list_runs\` (the runs you may see — your own children by default), \`get_run_status\` (one run: whether it is running, what it is doing, and its final reply once it finished), the GitHub reads — \`github_repos\`, \`github_tree\` / \`github_file\` (browse and read our repositories), \`github_search_code\`, \`github_issue_list\` / \`github_issue_get\` — \`web_fetch\` (read a public URL), and \`update_status\`.
|
|
602
|
+
You have no workspace and no shell. Your tools: \`spawn_run\` (start a child run), \`send_to_run\` (steer a live child: your text reaches it as a follow-up at its next step), \`await_runs\` (wait for your children to end and get each end — its status and final reply — back as data), \`list_runs\` (the runs you may see — your own children by default), \`get_run_status\` (one run: whether it is running, what it is doing, and its final reply once it finished), the GitHub reads — \`github_repos\`, \`github_tree\` / \`github_file\` (browse and read our repositories), \`github_search_code\`, \`github_issue_list\` / \`github_issue_get\`, \`github_actions_run\` / \`github_actions_job_log\` (an Actions run, its jobs, a job's log) — \`web_fetch\` (read a public URL), and \`update_status\`.
|
|
580
603
|
|
|
581
604
|
WHAT A CHILD IS. A child is an ordinary Switchboard run started as the person who asked you — exactly the run they could start by hand with \`agent:<preset>\` — in a thread of its own in this channel, visible to everyone there, with its own status card and run page, and under their permissions: a preset they may not run, a repository they may not use, or a profile a boundary caps is refused in the child's thread, and the refusal comes back to you as the tool result naming the gate. Children cannot spawn children. You may have a few live at once (the deployment's \`spawn.maxChildren\`, three by default); a spawn past the cap is refused until one finishes. A child's wall clock is capped by what is left of yours.
|
|
582
605
|
|
|
@@ -593,6 +616,7 @@ A CHILD IS ITS THREAD. People can reply in a child's thread. While the child run
|
|
|
593
616
|
|
|
594
617
|
Maintain the user-facing status card with the update_status tool: one item per child (○ pending, ✱ running, ✓ finished — only once await_runs or get_run_status said so).
|
|
595
618
|
|
|
619
|
+
${BREVITY_RULE}
|
|
596
620
|
${FENCED_CONTENT_RULE}
|
|
597
621
|
Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks). Your final message is posted to Slack: lead with the outcome, then one line per child — its preset, its thread, its status and its result in a sentence — and what is still running, if anything.`;
|
|
598
622
|
}
|
|
@@ -42,6 +42,24 @@ export const CONFIRMATION_TTL_MS = 10 * MINUTE_MS;
|
|
|
42
42
|
* inside it runs to the attach's own timeout. The run's own stop ends any of
|
|
43
43
|
* it at once. Read by the executor factory. */
|
|
44
44
|
export const FIRST_ATTACH_WAIT_MS = MINUTE_MS;
|
|
45
|
+
/** The least an attach REQUEST is opened with (docs/reference/specs/execution.md
|
|
46
|
+
* item 9): a re-attach that recreates the worktree clones from the resident's
|
|
47
|
+
* local mirror and may install deps, so a bound under this could not finish
|
|
48
|
+
* and a request cut mid-clone is struck as a rollout — the strike this floor
|
|
49
|
+
* exists to stop counting. A run with less than this left past its write-up
|
|
50
|
+
* reserve opens no attach at all (`attachBoundWithinRun`: `exhausted`). Half
|
|
51
|
+
* a minute: the one-second floor a COMMAND keeps (`BASH_TIMEOUT_MIN_MS`) is
|
|
52
|
+
* the model's to choose; an attach is opened on the run's behalf. */
|
|
53
|
+
export const ATTACH_REQUEST_MIN_MS = 30_000;
|
|
54
|
+
/** How long a harness's one more command waits for a container that is down
|
|
55
|
+
* under a live run to answer (docs/reference/specs/harness-pi.md item 16):
|
|
56
|
+
* the platform rebuilt a replaced resident container in about a minute, and
|
|
57
|
+
* a run with work in flight should not hang on a container that is not coming
|
|
58
|
+
* back for as long as a fresh run may wait for its first container
|
|
59
|
+
* (`SANDBOX_START_WAIT_MAX_MS`, ten minutes, execution.md item 23 — a wait
|
|
60
|
+
* before anything ran). A wait that runs out decides nothing; the failure
|
|
61
|
+
* that opened the question stands. */
|
|
62
|
+
export const HARNESS_PROBE_WAIT_MS = 5 * MINUTE_MS;
|
|
45
63
|
|
|
46
64
|
/** The presets that run the tool loop, and the one pipeline preset. */
|
|
47
65
|
export const LOOP_PRESETS = ["general", "coding", "review", "research", "explore", "conductor"] as const;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// routes decide on.
|
|
10
10
|
//
|
|
11
11
|
import type { Grant, GrantSource } from "../budgets.js";
|
|
12
|
-
import type
|
|
12
|
+
import { isAddressSeverity, type AddressSeverity, type AddressSeveritySource } from "../ship/coordinator.js";
|
|
13
13
|
|
|
14
14
|
// A coordinator is a Workflow instance in the shim Worker whose children are
|
|
15
15
|
// ordinary `dispatch()` runs as the requesting user. It holds no credential of
|
|
@@ -215,6 +215,12 @@ export interface UnitSegment {
|
|
|
215
215
|
* reaches it — its thread, its pull request, the round boundaries the card
|
|
216
216
|
* drew and how it ended. One row a person can read for "what happened to this
|
|
217
217
|
* unit". */
|
|
218
|
+
/** What the severity gate caught on a round: the level in force and the gated findings as `id (severity)`. */
|
|
219
|
+
export interface RoundGate {
|
|
220
|
+
level: AddressSeverity;
|
|
221
|
+
findings: string[];
|
|
222
|
+
}
|
|
223
|
+
|
|
218
224
|
export interface CoordinatorUnit {
|
|
219
225
|
instanceId: string;
|
|
220
226
|
/** `U<n>` as the plan spells it. */
|
|
@@ -251,8 +257,12 @@ export interface CoordinatorUnit {
|
|
|
251
257
|
* renews the same segment twice. `from` is the sha the segment continues
|
|
252
258
|
* from, `runId` the coding run whose write-up briefs it. */
|
|
253
259
|
segments?: UnitSegment[];
|
|
254
|
-
/** The round boundaries the coordinator reported, oldest first (the `ship_round`
|
|
255
|
-
|
|
260
|
+
/** The round boundaries the coordinator reported, oldest first (the `ship_round`
|
|
261
|
+
* vocabulary). `gate` rides an approve the machine's severity check caught
|
|
262
|
+
* carrying a finding at or above the level in force ([agent-ship](../../../docs/reference/specs/agent-ship.md)
|
|
263
|
+
* item 9) — a mismatch to be seen, since the child's parser holds an approve
|
|
264
|
+
* to the same level. */
|
|
265
|
+
rounds: Array<{ index: number; agent: string; outcome: string; at: number; gate?: RoundGate }>;
|
|
256
266
|
/** How the unit ended: the ending's kind and the thread's report, when it has. */
|
|
257
267
|
ending?: { kind: string; report: string; at: number };
|
|
258
268
|
startedAt?: number;
|
|
@@ -303,6 +313,13 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
|
|
|
303
313
|
}
|
|
304
314
|
|
|
305
315
|
/** Structural check on a unit row from outside the process. */
|
|
316
|
+
/** A round's gate: a level on the ladder and string findings. */
|
|
317
|
+
const isRoundGate = (v: unknown): boolean =>
|
|
318
|
+
isObject(v) &&
|
|
319
|
+
isAddressSeverity(v.level) &&
|
|
320
|
+
Array.isArray(v.findings) &&
|
|
321
|
+
v.findings.every((f) => typeof f === "string");
|
|
322
|
+
|
|
306
323
|
const isSegment = (v: unknown): boolean =>
|
|
307
324
|
isObject(v) &&
|
|
308
325
|
typeof v.index === "number" &&
|
|
@@ -328,7 +345,15 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
|
328
345
|
if (
|
|
329
346
|
!Array.isArray(r.rounds) ||
|
|
330
347
|
r.rounds.length > MAX_ROUNDS ||
|
|
331
|
-
!r.rounds.every(
|
|
348
|
+
!r.rounds.every(
|
|
349
|
+
(x) =>
|
|
350
|
+
isObject(x) &&
|
|
351
|
+
isFinite(x.index) &&
|
|
352
|
+
isText(x.agent) &&
|
|
353
|
+
isText(x.outcome) &&
|
|
354
|
+
isFinite(x.at) &&
|
|
355
|
+
(x.gate === undefined || isRoundGate(x.gate)),
|
|
356
|
+
)
|
|
332
357
|
)
|
|
333
358
|
return false;
|
|
334
359
|
if (
|
|
@@ -539,7 +539,13 @@ async function runUnit(
|
|
|
539
539
|
state = transition.state;
|
|
540
540
|
for (const note of transition.notes) {
|
|
541
541
|
if (note.type === "round") {
|
|
542
|
-
const body = {
|
|
542
|
+
const body = {
|
|
543
|
+
...tag,
|
|
544
|
+
index: note.index,
|
|
545
|
+
agent: note.agent,
|
|
546
|
+
outcome: note.outcome,
|
|
547
|
+
...(note.gate !== undefined ? { gate: note.gate } : {}),
|
|
548
|
+
};
|
|
543
549
|
await step.do(`${prefix}/note/${++notes}`, STEP_CONFIG, () => call(bot, "round", body));
|
|
544
550
|
} else {
|
|
545
551
|
// A merge_ready ending names the pull request as it is at the APPROVED
|
|
@@ -247,14 +247,146 @@ export function formatDisposition(d: FindingDisposition): string {
|
|
|
247
247
|
return `${d.findingId}: ${d.disposition}${d.note ? ` — ${d.note}` : ""}`;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/** Where the posted body's file links point: the PR's repository and the
|
|
251
|
+
* head the review is pinned to (the post-step's `commitId`). */
|
|
252
|
+
export interface ReviewBodyTarget {
|
|
253
|
+
/** `owner/name` */
|
|
254
|
+
repo: string;
|
|
255
|
+
/** The pinned head, full sha. */
|
|
256
|
+
head: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The verdict word the callout carries, per verdict kind (or its absence). */
|
|
260
|
+
function verdictWord(verdict: ReviewVerdict | undefined): string {
|
|
261
|
+
if (!verdict) return "No verdict";
|
|
262
|
+
return verdict.verdict === "approve" ? "Approved" : "Changes requested";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** GitHub's alert callout kind per verdict: an approve is a note, changes
|
|
266
|
+
* requested a warning, a missing verdict a caution. */
|
|
267
|
+
function calloutKind(verdict: ReviewVerdict | undefined): "NOTE" | "WARNING" | "CAUTION" {
|
|
268
|
+
if (!verdict) return "CAUTION";
|
|
269
|
+
return verdict.verdict === "approve" ? "NOTE" : "WARNING";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** `2 findings: 1 minor, 1 nit` — counted on the ladder, most severe first;
|
|
273
|
+
* an empty array is `no findings`, no array `findings not itemized`. */
|
|
274
|
+
function findingCounts(findings: readonly Finding[] | undefined): string {
|
|
275
|
+
if (findings === undefined) return "findings not itemized";
|
|
276
|
+
if (findings.length === 0) return "no findings";
|
|
277
|
+
const by = FINDING_SEVERITIES.map((sev) => [sev, findings.filter((f) => f.severity === sev).length] as const)
|
|
278
|
+
.filter(([, n]) => n > 0)
|
|
279
|
+
.map(([sev, n]) => `${n} ${sev}`)
|
|
280
|
+
.join(", ");
|
|
281
|
+
return `${findings.length} finding${findings.length === 1 ? "" : "s"}: ${by}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** A table cell: pipes escaped so the row holds, already one line. */
|
|
285
|
+
const cell = (text: string): string => oneLine(text).replace(/\|/g, "\\|");
|
|
286
|
+
|
|
287
|
+
/** Whether a finding's `file` is a path a blob URL can point at: no
|
|
288
|
+
* whitespace, no backtick, not a URL. */
|
|
289
|
+
const isRepoPath = (file: string): boolean => /^[^\s`]+$/.test(file) && !/^[a-z][a-z0-9+.-]*:\/\//i.test(file);
|
|
290
|
+
|
|
291
|
+
/** The `Where` cell: the location in code, linked at the pinned head when the
|
|
292
|
+
* file is a path and a target is known. */
|
|
293
|
+
function whereCell(f: Finding, target: ReviewBodyTarget | undefined): string {
|
|
294
|
+
const location = f.line !== undefined ? `${f.file}:${f.line}` : f.file;
|
|
295
|
+
const label = `\`${cell(location)}\``;
|
|
296
|
+
if (!target || !isRepoPath(f.file)) return label;
|
|
297
|
+
const path = encodeURI(f.file).replace(/#/g, "%23").replace(/\?/g, "%3F");
|
|
298
|
+
const url = `https://github.com/${target.repo}/blob/${target.head}/${path}${f.line !== undefined ? `#L${f.line}` : ""}`;
|
|
299
|
+
return `[${label}](${url})`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The machine-readable marker the body ends with — the verdict, the head and
|
|
303
|
+
* the findings index as JSON inside an HTML comment, for a scanner that would
|
|
304
|
+
* otherwise parse the token line. `-->` can never occur inside it. */
|
|
305
|
+
function verdictMarker(verdict: ReviewVerdict | undefined, target: ReviewBodyTarget | undefined): string {
|
|
306
|
+
const head = target?.head ?? verdict?.head;
|
|
307
|
+
const payload = {
|
|
308
|
+
verdict: verdict?.verdict ?? "none",
|
|
309
|
+
...(head ? { head } : {}),
|
|
310
|
+
...(verdict?.findings
|
|
311
|
+
? {
|
|
312
|
+
findings: verdict.findings.map((f) => ({
|
|
313
|
+
id: f.id,
|
|
314
|
+
severity: f.severity,
|
|
315
|
+
file: f.file,
|
|
316
|
+
...(f.line !== undefined ? { line: f.line } : {}),
|
|
317
|
+
})),
|
|
318
|
+
}
|
|
319
|
+
: {}),
|
|
320
|
+
};
|
|
321
|
+
return `<!-- switchboard:verdict ${JSON.stringify(payload).replace(/-->/g, "--\\u003e")} -->`;
|
|
322
|
+
}
|
|
323
|
+
|
|
250
324
|
/**
|
|
251
|
-
* Build the body posted to GitHub
|
|
252
|
-
*
|
|
253
|
-
*
|
|
325
|
+
* Build the body posted to GitHub, rendered from the typed verdict
|
|
326
|
+
* (docs/reference/specs/agent-review.md item 5b): the deterministic verdict
|
|
327
|
+
* line first (the auto-approve contract — never `LGTM:` unless the verdict is
|
|
328
|
+
* `approve`), a GitHub alert callout with the verdict word, the pinned head
|
|
329
|
+
* and the finding counts, the findings as a table (severity, id + title, the
|
|
330
|
+
* file linked at the head), the model's text folded under `Full review`, and
|
|
331
|
+
* the machine-readable marker last. The prose decides nothing above the fold.
|
|
254
332
|
*/
|
|
255
|
-
export function buildReviewPostBody(
|
|
256
|
-
|
|
257
|
-
|
|
333
|
+
export function buildReviewPostBody(
|
|
334
|
+
answer: string,
|
|
335
|
+
verdict: ReviewVerdict | undefined,
|
|
336
|
+
target?: ReviewBodyTarget,
|
|
337
|
+
): string {
|
|
338
|
+
const facts = [
|
|
339
|
+
`**${verdictWord(verdict)}**`,
|
|
340
|
+
...(target ? [`head \`${target.head.slice(0, 7)}\``] : []),
|
|
341
|
+
verdict ? findingCounts(verdict.findings) : "the run ended without a submit_verdict call",
|
|
342
|
+
];
|
|
343
|
+
const parts: string[] = [verdictLine(verdict), `> [!${calloutKind(verdict)}]\n> ${facts.join(" · ")}`];
|
|
344
|
+
const findings = verdict?.findings ?? [];
|
|
345
|
+
if (findings.length > 0) {
|
|
346
|
+
parts.push(
|
|
347
|
+
[
|
|
348
|
+
"| Severity | Finding | Where |",
|
|
349
|
+
"| --- | --- | --- |",
|
|
350
|
+
...findings.map((f) => `| ${f.severity} | **${cell(f.id)}** ${cell(f.title)} | ${whereCell(f, target)} |`),
|
|
351
|
+
].join("\n"),
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
const prose = answer.trim();
|
|
355
|
+
if (prose) parts.push(`<details>\n<summary>Full review</summary>\n\n${prose}\n\n</details>`);
|
|
356
|
+
parts.push(verdictMarker(verdict, target));
|
|
357
|
+
return parts.join("\n\n");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* The channel reply for a review run, rendered from the typed verdict
|
|
362
|
+
* (agent-review.md item 5b): the verdict line, one bullet per finding, then
|
|
363
|
+
* where the review was posted and the run link. The model's write-up rides
|
|
364
|
+
* along only when it landed nowhere else — no GitHub post (Slack-only, an
|
|
365
|
+
* opt-out, a guard refusal) or findings not itemized (a list that says
|
|
366
|
+
* nothing is no substitute) — so the review's text is always somewhere a
|
|
367
|
+
* person reads it. No verdict → the bare answer with the link, as before.
|
|
368
|
+
*/
|
|
369
|
+
export function buildReviewChannelReply(input: {
|
|
370
|
+
answer: string;
|
|
371
|
+
verdict: ReviewVerdict | undefined;
|
|
372
|
+
/** The PR the post landed on, or undefined when nothing was posted. */
|
|
373
|
+
posted: { repo: string; number: number } | undefined;
|
|
374
|
+
liveUrl: string | undefined;
|
|
375
|
+
}): string {
|
|
376
|
+
const { answer, verdict, posted, liveUrl } = input;
|
|
377
|
+
const tail = [
|
|
378
|
+
...(posted && verdict ? [`Posted to ${posted.repo}#${posted.number}`] : []),
|
|
379
|
+
...(liveUrl ? [`[Live run](${liveUrl})`] : []),
|
|
380
|
+
].join(" · ");
|
|
381
|
+
const blocks: string[] = [];
|
|
382
|
+
if (!verdict) blocks.push(answer);
|
|
383
|
+
else {
|
|
384
|
+
blocks.push([verdictLine(verdict), ...(verdict.findings ?? []).map((f) => `- ${formatFinding(f)}`)].join("\n"));
|
|
385
|
+
const compact = posted !== undefined && verdict.findings !== undefined;
|
|
386
|
+
if (!compact && answer.trim()) blocks.push(answer.trim());
|
|
387
|
+
}
|
|
388
|
+
if (tail) blocks.push(tail);
|
|
389
|
+
return blocks.join("\n\n");
|
|
258
390
|
}
|
|
259
391
|
|
|
260
392
|
// --- Dispositions (the coding side's answer to findings) -------------------
|
|
@@ -94,6 +94,10 @@ export type RunNoteKind =
|
|
|
94
94
|
| "sandbox_restarted"
|
|
95
95
|
| "stop_requested"
|
|
96
96
|
| "stopped"
|
|
97
|
+
/** A stop the loop or turn had to ask pi for again reached it: the series a
|
|
98
|
+
* failed abort's write opened (its `harness_error` line) closes here with
|
|
99
|
+
* the count of re-asks (harness-pi item 16). Written by the pi harness. */
|
|
100
|
+
| "stop_landed"
|
|
97
101
|
/** Setup spans the request's stream sink had to drop before this run was
|
|
98
102
|
* bound (docs/reference/specs/tracing.md): `summary` says how many, `from`/`to` the
|
|
99
103
|
* interval, which the partition reports as not recorded. */
|
|
@@ -142,6 +146,16 @@ export type RunNoteKind =
|
|
|
142
146
|
* after the attach, before the first turn, so the run page explains a
|
|
143
147
|
* sandbox run that shows resident steps. */
|
|
144
148
|
| "cold_sandbox"
|
|
149
|
+
/** The run ledger would not track this run (docs/reference/specs/run-history.md
|
|
150
|
+
* item 54): its reservation met the row of a run this process was closing
|
|
151
|
+
* (a restart from its request, or the fresh turn for its follow-ups), waited
|
|
152
|
+
* for that finish, and the row still stood — the finish failed or was
|
|
153
|
+
* refused — so no handoff, resume or reclaim reaches this run, and its
|
|
154
|
+
* record reaches the store when it finishes. The summary names that run and
|
|
155
|
+
* how its finish ended; published by the dispatcher right after the
|
|
156
|
+
* reservation, before the attach, and carried on the card's label — the bot
|
|
157
|
+
* log's warning is not the only witness. */
|
|
158
|
+
| "ledger_untracked"
|
|
145
159
|
/** The resident kept this thread's binding where it was instead of moving
|
|
146
160
|
* it onto the branch the thread's own run opened a pull request on
|
|
147
161
|
* (docs/reference/specs/resident-repos.md item 16): the summary names the
|
|
@@ -248,6 +262,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
248
262
|
"sandbox_restarted",
|
|
249
263
|
"stop_requested",
|
|
250
264
|
"stopped",
|
|
265
|
+
"stop_landed",
|
|
251
266
|
"spans_dropped",
|
|
252
267
|
"head_moved",
|
|
253
268
|
"run_failed",
|
|
@@ -258,6 +273,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
258
273
|
"description_turn",
|
|
259
274
|
"verdict_turn",
|
|
260
275
|
"cold_sandbox",
|
|
276
|
+
"ledger_untracked",
|
|
261
277
|
"rebind_refused",
|
|
262
278
|
"work_left_behind",
|
|
263
279
|
"workspace_torn_down",
|
|
@@ -373,6 +389,7 @@ export function isHeadMaterial(event: RunEvent): boolean {
|
|
|
373
389
|
event.kind === "mcp_unavailable" ||
|
|
374
390
|
event.kind === "spans_dropped" ||
|
|
375
391
|
event.kind === "cold_sandbox" ||
|
|
392
|
+
event.kind === "ledger_untracked" ||
|
|
376
393
|
event.kind === "rebind_refused"
|
|
377
394
|
);
|
|
378
395
|
case "span_start":
|
|
@@ -794,7 +811,18 @@ export type RunEvent =
|
|
|
794
811
|
* round and its fix round share an index. Published by the ship pipeline
|
|
795
812
|
* straight to the registry (like `pr_opened`), never through the runner.
|
|
796
813
|
* Additive: unknown → ignored. */
|
|
797
|
-
| {
|
|
814
|
+
| {
|
|
815
|
+
type: "ship_round";
|
|
816
|
+
index: number;
|
|
817
|
+
agent: string;
|
|
818
|
+
outcome: ShipRoundOutcome;
|
|
819
|
+
/** The severity gate fired on this approve (agent-ship item 9): the level
|
|
820
|
+
* in force and the gated findings as `id (severity)` — a mismatch the
|
|
821
|
+
* child's own parser should have made impossible, kept visible. */
|
|
822
|
+
gate?: { level: string; findings: string[] };
|
|
823
|
+
seq?: number;
|
|
824
|
+
at?: number;
|
|
825
|
+
}
|
|
798
826
|
/** The request router's decision (docs/reference/specs/routing-and-config.md
|
|
799
827
|
* item 21): the preset a plain message was routed to, the one-line reason
|
|
800
828
|
* the router gave (redacted, capped — the same text the card's `routed:`
|