@coreplane/switchboard 1.244.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/config/config.example.yaml +6 -3
- package/dist/assets/deploy/cloudflare/worker.ts +2 -0
- 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 +40 -272
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +22 -11
- 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 +34 -10
- package/dist/assets/src/config/profile.ts +17 -1
- package/dist/assets/src/core/authz/types.ts +8 -0
- package/dist/assets/src/core/budgets.ts +43 -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 +239 -30
- package/dist/assets/src/core/runEvents.ts +55 -2
- package/dist/assets/src/core/runRecord.ts +56 -0
- package/dist/assets/src/core/ship/coordinator.ts +48 -23
- 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 +34 -11
- package/dist/assets/src/execution/sandboxErrors.ts +9 -4
- package/dist/assets/web/dist/.vite/manifest.json +481 -462
- package/dist/assets/web/dist/assets/AppShell-B4EUiSwP.js +1 -0
- package/dist/assets/web/dist/assets/CostsPage-D8VjAtuH.js +2 -0
- package/dist/assets/web/dist/assets/DeliveryPage-DpQi5XUI.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-CvKuIGqg.js +2 -0
- package/dist/assets/web/dist/assets/InputMenu-DmqAxkn4.js +1 -0
- package/dist/assets/web/dist/assets/NotFoundPage-DDogyiXA.js +1 -0
- package/dist/assets/web/dist/assets/PendingTurnRow-BoSLlR6j.js +1 -0
- package/dist/assets/web/dist/assets/ResidentDetailPage-CiBdbzkE.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-DgtHtd9K.js +1 -0
- package/dist/assets/web/dist/assets/{RunFoldRow-BPMQxTcl.js → RunFoldRow-IF4xbz-3.js} +1 -1
- package/dist/assets/web/dist/assets/RunRoutePage-DHYa7rHK.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-aWhNTv8r.js +1 -0
- package/dist/assets/web/dist/assets/RunsTabs-CY_tdGTU.js +1 -0
- package/dist/assets/web/dist/assets/ScheduledPage-gPEBZjcx.js +1 -0
- package/dist/assets/web/dist/assets/SettingSelect-VDanrTZi.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-CFoltSRD.js +1 -0
- package/dist/assets/web/dist/assets/StatusDot-DA3eTVrT.js +1 -0
- package/dist/assets/web/dist/assets/Tooltip-BgbCkM12.js +1 -0
- package/dist/assets/web/dist/assets/UnitRoutePage-DibegaJJ.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-Dail07rr.js → angular-html-D_3slZqf.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-BReExio8.js → angular-ts-D1Fv2cpN.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-DhNO6Yyx.js → apl-60Nm_trN.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-BYotTY4C.js → astro-CvyaM3Q9.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-mUxTtD1X.js → blade-yan3VBls.js} +1 -1
- package/dist/assets/web/dist/assets/{c-DhuiwcSQ.js → c-Ddgepap2.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-GD2vFtRi.js → chapel-CgNY6dxC.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-BhFDwkes.js → cobol-C7q82cdP.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-DWUgbE0r.js → coffee-DgRMF_ni.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-CO9XFjGW.js → cpp-B8Lf-aeo.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-B9pVurpn.js → crystal-CkA_dhEO.js} +1 -1
- package/dist/assets/web/dist/assets/{css-CFVVV6lI.js → css-Cyu-C_oA.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BtPs2AkQ.js → dist-DS-gMjLZ.js} +2 -2
- package/dist/assets/web/dist/assets/{durationTone-BobbycC-.js → durationTone-l6aVrHb8.js} +1 -1
- package/dist/assets/web/dist/assets/{edge-BDhIDQIb.js → edge-BxV3rKNI.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-DG3J8WoD.js → elixir-pNSEK39d.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-FiOdiaOE.js → elm-zgwWE33W.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-vxbtW503.js → erb-yzcYAHp_.js} +1 -1
- package/dist/assets/web/dist/assets/{eventSource-tRprhqm7.js → eventSource-Ce94ho6M.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-DINz8M3H.js → git-rebase-Dg7Ous1-.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-yann6r9i.js → glimmer-js-CbZEwX27.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CXTbrZn_.js → glimmer-ts-D3NIqALy.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-DNYC-hq0.js → glsl-y4-3WrI2.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-CwzhHzi9.js → graphql-D5d0al7K.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BBXF1cwR.js → hack-D8fD6kEl.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-oEqBcsTy.js → haml-BWeFFc-V.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-YE5thkT1.js → handlebars-gIm5FeE_.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DCT1GYtQ.js → html-derivative-B3ySSBrQ.js} +1 -1
- package/dist/assets/web/dist/assets/{html-Dp4jRQ1j.js → html-fXJTVLSM.js} +1 -1
- package/dist/assets/web/dist/assets/{http-BPcGbmzJ.js → http-DrJfQUmw.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-CQnNHwyd.js → hurl-Bvbnsf55.js} +1 -1
- package/dist/assets/web/dist/assets/{java-Dm8Ovj6W.js → java-CUSpb-i-.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-Btm9oJK9.js → javascript-BWRH5fbY.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-VZYjWF_U.js → jinja-D3Ymci8y.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-Dcmjxa9x.js → jison-Gt9SO7ZC.js} +1 -1
- package/dist/assets/web/dist/assets/{json-DoWiOGZ8.js → json-Cfcblhgn.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-Dvmo55ma.js → jsx-C0366fFM.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-d8AtfV26.js → julia-CyBOTbxS.js} +1 -1
- package/dist/assets/web/dist/assets/{just-Cc5EStA-.js → just-FPXZcIOd.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-B5fKKIH7.js → latex-Bd-c88v0.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-DUYI9JND.js → liquid-5tu9Hc0k.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-BhMwKWpw.js → lua-BZ5fdPP8.js} +1 -1
- package/dist/assets/web/dist/assets/main-B4A6x82d.js +28 -0
- package/dist/assets/web/dist/assets/main-CpkwzHX6.css +1 -0
- package/dist/assets/web/dist/assets/{marko-Cnf2cPAi.js → marko-C_mmj4p0.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-FDDXoir9.js → mdc-CtaNqWjY.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-q-8U_1jR.js → nginx-Ddm5Mb8J.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-DIA0TUWs.js → nim-COhfroYB.js} +1 -1
- package/dist/assets/web/dist/assets/{org-CR88hy7S.js → org-CCHFgmTb.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-Bkd8N7ja.js → perl-CXzOE-Z-.js} +1 -1
- package/dist/assets/web/dist/assets/{php-BOs84mvy.js → php-1V6jAIhU.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-BbpZBb3w.js → pug-uS37efts.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-D2jQpjFE.js → qml-Bj1A0Ben.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DKzcAQ22.js → r-CMbKbLsS.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-SOQMWcC9.js → razor-DVRq_Ndk.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-B6KK4W8d.js → regexp-AbaOr4oB.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-kPSg-NPf.js → rst-Dq_QWgOF.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-cKS-JYLH.js → ruby-CVSsAj7T.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-DNxzE5gs.js → sas-C-dGfxUi.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-DGcHQ5OO.js → scss-DxALxYPA.js} +1 -1
- package/dist/assets/web/dist/assets/seed-BhxEVasY.js +6 -0
- package/dist/assets/web/dist/assets/{settingsApi-priOPiwu.js → settingsApi-Dm6eaQcu.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-CRZeZBa5.js → shellscript--qXcvQ5L.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-DSNEL0HX.js → shellsession-DeqnW8Ux.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-zisNbkDO.js → soy-DreoUIMt.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-MG06KMMb.js → sql-o3i2VTG6.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-DaRtc7nI.js → sseReplay-qF-gpbAY.js} +3 -3
- package/dist/assets/web/dist/assets/{stata-CB_wKwv9.js → stata-CGAT41H7.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-DYdSt7re.js → surrealql-CBU0QuGV.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-DHO-F7c7.js → svelte-CfHaLZiG.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-CUL5L4Lr.js → templ-D02MUcma.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-_JNEKjVW.js → tex-DjprHYXl.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags--9EpEV_8.js → ts-tags-DftDOzP-.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-BcNq2x2u.js → tsx-BOdvPohJ.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-EsY5Dk_D.js → twig-B_yLS56B.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-CyA_HiUH.js → typescript-CdPCa4nU.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-SzaHlDt4.js → typst-C1oIFsUl.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-D3cn5acV.js → vue-CcszJEX8.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-6MxhUxOa.js → vue-html-DcpxPcuT.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-qJnqhgo9.js → vue-vine-D26bIKGj.js} +1 -1
- package/dist/assets/web/dist/assets/wallClock-DPTuP2FD.js +1 -0
- package/dist/assets/web/dist/assets/{xml-DyRpC7xv.js → xml-D8zl6W5P.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-BXMRPAsg.js → xsl-D86Bii28.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BNmPCtAN.js → yaml-g6Px4KFU.js} +1 -1
- package/dist/cli.js +3023 -1049
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-Ck4hSZDi.js +0 -1
- package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +0 -2
- package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +0 -2
- package/dist/assets/web/dist/assets/NotFoundPage-CiGrVOyv.js +0 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BHCSpsPf.js +0 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-C5USV_8P.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CSKisWnX.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +0 -1
- package/dist/assets/web/dist/assets/RunsTabs-BWRHzhtT.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DBaChcJZ.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +0 -1
- package/dist/assets/web/dist/assets/StatusDot-_Q_bmReh.js +0 -1
- package/dist/assets/web/dist/assets/Tooltip-Cafsgd4m.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CdoBZwtf.js +0 -1
- package/dist/assets/web/dist/assets/main-C4366PZF.css +0 -1
- package/dist/assets/web/dist/assets/main-DlTNAC6c.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
|
@@ -19,19 +19,83 @@
|
|
|
19
19
|
// a fix round can reference each finding by its stable id and answer it with
|
|
20
20
|
// a typed disposition (`parseDispositionsInput` below). Findings validate
|
|
21
21
|
// fail-closed PER finding — a malformed finding drops with a note, a
|
|
22
|
-
// malformed array drops the whole field
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
22
|
+
// malformed array drops the whole field.
|
|
23
|
+
//
|
|
24
|
+
// THE SEVERITY GATE (docs/reference/specs/agent-review.md item 5a): an
|
|
25
|
+
// `approve` carrying a finding at or above the severity to address — the
|
|
26
|
+
// level in force for the run, `minor` by default — is downgraded to
|
|
27
|
+
// `request_changes` HERE, where the verdict is parsed, so the posted body can
|
|
28
|
+
// never begin with `LGTM:` over a finding the loop would have to act on. The
|
|
29
|
+
// gate is keyed on the RAW entries' declared severity, so a finding that
|
|
30
|
+
// fails validation for another reason still poisons the approve. The same
|
|
31
|
+
// ladder is what ship's coordinator holds a posted approve to (agent-ship.md
|
|
32
|
+
// item 9) — that check is now defense in depth behind this one.
|
|
26
33
|
|
|
27
34
|
import { normalizeHead } from "./reviewedHead.js";
|
|
28
35
|
import { redactSecrets } from "./redact.js";
|
|
29
36
|
|
|
30
37
|
export type ReviewVerdictKind = "approve" | "request_changes";
|
|
31
38
|
|
|
39
|
+
/** The severity ladder, most to least severe. One list serves the findings a
|
|
40
|
+
* reviewer submits and the level the loop addresses: a finding's severity is
|
|
41
|
+
* compared to the level in force on this ladder, so nothing outside it can
|
|
42
|
+
* reach a gate — the parser drops an entry with any other severity (`fyi`,
|
|
43
|
+
* none) with a note. */
|
|
32
44
|
export const FINDING_SEVERITIES = ["blocking", "major", "minor", "nit"] as const;
|
|
33
45
|
export type FindingSeverity = (typeof FINDING_SEVERITIES)[number];
|
|
34
46
|
|
|
47
|
+
/** The severity to address (the loop's gate on a round): an approve carrying a
|
|
48
|
+
* finding at or above this level is not an approve. Resolved once per run —
|
|
49
|
+
* the request's `severity:` directive over the user's scope over the
|
|
50
|
+
* channel's over the org's `review.addressSeverity` — and handed to the
|
|
51
|
+
* verdict parser and ship's coordinator alike. */
|
|
52
|
+
export const ADDRESS_SEVERITIES = FINDING_SEVERITIES;
|
|
53
|
+
export type AddressSeverity = FindingSeverity;
|
|
54
|
+
/** Where the level in force came from, most specific wins: a `severity:`
|
|
55
|
+
* directive on the request (`run`), the user's or the channel's config scope,
|
|
56
|
+
* the org's `review.addressSeverity` (or its default). */
|
|
57
|
+
export type AddressSeveritySource = "org" | "channel" | "user" | "run";
|
|
58
|
+
export const DEFAULT_ADDRESS_SEVERITY: AddressSeverity = "minor";
|
|
59
|
+
export const isAddressSeverity = (v: unknown): v is AddressSeverity =>
|
|
60
|
+
(ADDRESS_SEVERITIES as readonly unknown[]).includes(v);
|
|
61
|
+
|
|
62
|
+
const severityRank = (s: AddressSeverity): number => ADDRESS_SEVERITIES.indexOf(s);
|
|
63
|
+
/** Whether a declared severity sits at or above the level in force on the
|
|
64
|
+
* ladder — the one comparison every gate makes. A value outside the ladder is
|
|
65
|
+
* never at or above anything. */
|
|
66
|
+
export function severityAtOrAbove(severity: unknown, level: AddressSeverity): severity is AddressSeverity {
|
|
67
|
+
return isAddressSeverity(severity) && severityRank(severity) <= severityRank(level);
|
|
68
|
+
}
|
|
69
|
+
/** The findings a gate acts on at `level`: at or above it. */
|
|
70
|
+
export function findingsAtOrAbove(findings: readonly Finding[], level: AddressSeverity): Finding[] {
|
|
71
|
+
return findings.filter((f) => severityAtOrAbove(f.severity, level));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The level in force and the layer that set it: the request's
|
|
75
|
+
* `severity:` directive wins, then the user's scope, the channel's, the org's
|
|
76
|
+
* `review.addressSeverity` — the default counts as the org's. Resolved once
|
|
77
|
+
* per run by the dispatcher (every review's verdict parser reads it) and once
|
|
78
|
+
* per ship request by the hand-off, which writes it on the instance beside
|
|
79
|
+
* `merge` and hands it to each review child as its `severity:` directive. */
|
|
80
|
+
export function resolveAddressSeverity(layers: {
|
|
81
|
+
org?: AddressSeverity;
|
|
82
|
+
channel?: AddressSeverity;
|
|
83
|
+
user?: AddressSeverity;
|
|
84
|
+
run?: AddressSeverity;
|
|
85
|
+
}): { level: AddressSeverity; source: AddressSeveritySource } {
|
|
86
|
+
if (layers.run !== undefined) return { level: layers.run, source: "run" };
|
|
87
|
+
if (layers.user !== undefined) return { level: layers.user, source: "user" };
|
|
88
|
+
if (layers.channel !== undefined) return { level: layers.channel, source: "channel" };
|
|
89
|
+
return { level: layers.org ?? DEFAULT_ADDRESS_SEVERITY, source: "org" };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The gate's one sentence — `finding(s) <id> (<severity>)[, …] at or above
|
|
93
|
+
* <level>, the severity to address` — as the downgraded summary carries it into
|
|
94
|
+
* the posted body and the tool's ack repeats it to the model. */
|
|
95
|
+
export function downgradeNote(d: { findings: readonly string[]; level: AddressSeverity }): string {
|
|
96
|
+
return `finding${d.findings.length === 1 ? "" : "s"} ${d.findings.join(", ")} at or above ${d.level}, the severity to address`;
|
|
97
|
+
}
|
|
98
|
+
|
|
35
99
|
export interface Finding {
|
|
36
100
|
/** Stable id the reviewer assigned in order ("F1", "F2", …) — dispositions
|
|
37
101
|
* reference findings by this id. */
|
|
@@ -63,50 +127,63 @@ export interface ReviewVerdict {
|
|
|
63
127
|
* reasons) — surfaced in the tool ack so the model can resubmit, never
|
|
64
128
|
* rendered into the posted body. */
|
|
65
129
|
droppedFindings?: string[];
|
|
130
|
+
/** Parse note: the approve was downgraded by the severity gate — the gated
|
|
131
|
+
* findings as `id (severity)` and the level in force. Surfaced in the tool
|
|
132
|
+
* ack so the model knows its verdict changed; the summary carries the same
|
|
133
|
+
* words into the posted body, so this never rides a record. */
|
|
134
|
+
downgraded?: { findings: string[]; level: AddressSeverity };
|
|
66
135
|
}
|
|
67
136
|
|
|
68
137
|
export const LGTM_TOKEN = "LGTM:";
|
|
69
138
|
export const CHANGES_TOKEN = "Changes requested:";
|
|
70
139
|
export const NO_VERDICT_LINE = "No verdict submitted — not approving.";
|
|
71
140
|
|
|
72
|
-
/** Parse an arbitrary tool input into a verdict, or null when it is not one.
|
|
73
|
-
|
|
141
|
+
/** Parse an arbitrary tool input into a verdict, or null when it is not one.
|
|
142
|
+
* `addressSeverity` is the level in force for the run (default `minor`): an
|
|
143
|
+
* approve carrying a finding at or above it is parsed as `request_changes`. */
|
|
144
|
+
export function parseVerdictInput(
|
|
145
|
+
input: Record<string, unknown>,
|
|
146
|
+
opts: { addressSeverity?: AddressSeverity } = {},
|
|
147
|
+
): ReviewVerdict | null {
|
|
74
148
|
const verdict = input.verdict;
|
|
75
149
|
if (verdict !== "approve" && verdict !== "request_changes") return null;
|
|
150
|
+
const level = opts.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY;
|
|
76
151
|
const summary = typeof input.summary === "string" ? oneLine(input.summary) : "";
|
|
77
152
|
const head = normalizeHead(input.head);
|
|
78
153
|
const out: ReviewVerdict = { verdict, summary };
|
|
79
154
|
if (head) out.head = head;
|
|
80
155
|
if (input.findings !== undefined) {
|
|
81
|
-
const parsed = parseFindings(input.findings);
|
|
156
|
+
const parsed = parseFindings(input.findings, level);
|
|
82
157
|
if (parsed.findings) out.findings = parsed.findings;
|
|
83
158
|
if (parsed.dropped.length) out.droppedFindings = parsed.dropped;
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// `LGTM:` over a
|
|
87
|
-
// declared severity, not the validated survivors — a
|
|
88
|
-
// itself failed validation and dropped still poisons the approve.
|
|
89
|
-
if (out.verdict === "approve" && parsed.
|
|
159
|
+
// The severity gate, fail-closed: an approve carrying a finding at or
|
|
160
|
+
// above the level in force downgrades, so the posted body can never start
|
|
161
|
+
// with `LGTM:` over a finding the loop has to address. Keyed on the RAW
|
|
162
|
+
// entries' declared severity, not the validated survivors — a gated entry
|
|
163
|
+
// that itself failed validation and dropped still poisons the approve.
|
|
164
|
+
if (out.verdict === "approve" && parsed.gated.length > 0) {
|
|
90
165
|
out.verdict = "request_changes";
|
|
91
|
-
out.
|
|
92
|
-
|
|
93
|
-
);
|
|
166
|
+
out.downgraded = { findings: parsed.gated, level };
|
|
167
|
+
out.summary = oneLine(`${out.summary} [downgraded from approve: ${downgradeNote(out.downgraded)}]`);
|
|
94
168
|
}
|
|
95
169
|
}
|
|
96
170
|
return out;
|
|
97
171
|
}
|
|
98
172
|
|
|
99
173
|
/** Fail-closed per finding: each malformed entry drops with a note naming its
|
|
100
|
-
* index (and id when readable); a non-array drops the whole field.
|
|
101
|
-
*
|
|
102
|
-
* valid or not — for the approve downgrade above. */
|
|
103
|
-
function parseFindings(
|
|
174
|
+
* index (and id when readable); a non-array drops the whole field. `gated`
|
|
175
|
+
* labels every raw entry whose declared severity sits at or above `level` —
|
|
176
|
+
* valid or not — as `id (severity)`, for the approve downgrade above. */
|
|
177
|
+
function parseFindings(
|
|
178
|
+
value: unknown,
|
|
179
|
+
level: AddressSeverity,
|
|
180
|
+
): { findings?: Finding[]; dropped: string[]; gated: string[] } {
|
|
104
181
|
if (!Array.isArray(value)) {
|
|
105
|
-
return { dropped: ["findings: dropped — not an array"],
|
|
182
|
+
return { dropped: ["findings: dropped — not an array"], gated: [] };
|
|
106
183
|
}
|
|
107
184
|
const findings: Finding[] = [];
|
|
108
185
|
const dropped: string[] = [];
|
|
109
|
-
const
|
|
186
|
+
const gated: string[] = [];
|
|
110
187
|
value.forEach((raw, i) => {
|
|
111
188
|
const parsed = parseFinding(raw);
|
|
112
189
|
if (parsed.finding) {
|
|
@@ -116,9 +193,9 @@ function parseFindings(value: unknown): { findings?: Finding[]; dropped: string[
|
|
|
116
193
|
}
|
|
117
194
|
const label = parsed.finding?.id ?? parsed.id ?? `findings[${i}]`;
|
|
118
195
|
const r = typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : undefined;
|
|
119
|
-
if (r?.severity
|
|
196
|
+
if (severityAtOrAbove(r?.severity, level)) gated.push(`${label} (${r?.severity})`);
|
|
120
197
|
});
|
|
121
|
-
return { findings, dropped,
|
|
198
|
+
return { findings, dropped, gated };
|
|
122
199
|
}
|
|
123
200
|
|
|
124
201
|
function parseFinding(
|
|
@@ -170,14 +247,146 @@ export function formatDisposition(d: FindingDisposition): string {
|
|
|
170
247
|
return `${d.findingId}: ${d.disposition}${d.note ? ` — ${d.note}` : ""}`;
|
|
171
248
|
}
|
|
172
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
|
+
|
|
324
|
+
/**
|
|
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.
|
|
332
|
+
*/
|
|
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
|
+
|
|
173
360
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* review
|
|
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.
|
|
177
368
|
*/
|
|
178
|
-
export function
|
|
179
|
-
|
|
180
|
-
|
|
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");
|
|
181
390
|
}
|
|
182
391
|
|
|
183
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
|
|
@@ -158,6 +172,13 @@ export type RunNoteKind =
|
|
|
158
172
|
* after the model's last turn — the release itself runs after the record
|
|
159
173
|
* is sealed — and set on the card's label too, so the loss is never silent. */
|
|
160
174
|
| "work_left_behind"
|
|
175
|
+
/** The run's ending may have left a command running in its workspace — a
|
|
176
|
+
* call cut by the ending's abort or interrupt, or open when the run failed —
|
|
177
|
+
* so the release tears the workspace down rather than pair it for the
|
|
178
|
+
* thread's next run (harness.md item 13). The summary names the calls.
|
|
179
|
+
* Written by the run loop once the harness session has ended and before the
|
|
180
|
+
* record is sealed; the release itself runs after. */
|
|
181
|
+
| "workspace_torn_down"
|
|
161
182
|
/** A coding run submitted a PR description but the post-step opened no
|
|
162
183
|
* pull request because the branch it observed IS the base the pull
|
|
163
184
|
* request would target (docs/reference/specs/pr-description.md item 5) —
|
|
@@ -210,11 +231,20 @@ export type RunNoteKind =
|
|
|
210
231
|
* names the call and the step. Information, not a failure: the tool ran
|
|
211
232
|
* under the gate's decision either way. Published by the OpenCode bridge. */
|
|
212
233
|
| "tool_unnamed"
|
|
234
|
+
/** An `external_directory` ask answered once its call's line was already on
|
|
235
|
+
* the record (harness.md item 13): the line cannot be amended, so the
|
|
236
|
+
* directory the call reached is said here, naming the call. Published by
|
|
237
|
+
* the OpenCode bridge. */
|
|
238
|
+
| "directory_reached"
|
|
213
239
|
/** A ship coding child's budget ended with work still in the tree: the run
|
|
214
240
|
* loop committed and pushed it to the unit's branch (or says plainly that
|
|
215
241
|
* there was nothing to push), so a re-issue starts from the partial work
|
|
216
242
|
* (docs/reference/specs/agent-ship.md item 8). */
|
|
217
243
|
| "budget_salvage"
|
|
244
|
+
/** The loop's end found a tool call in flight and ended it, so the write-up
|
|
245
|
+
* keeps its allowance (decision 0046, unit seven; harness-pi item 6). The
|
|
246
|
+
* summary names the tools. Published by the harness beside the budget note. */
|
|
247
|
+
| "tool_cut"
|
|
218
248
|
/** The native loop's stuck-loop guard: the same tool call failed identically
|
|
219
249
|
* six times in a row and the run was forced into its write-up. Written by
|
|
220
250
|
* no loop since that loop's deletion; a record from before it may carry it. */
|
|
@@ -225,12 +255,14 @@ export type RunNoteKind =
|
|
|
225
255
|
export const RUN_NOTE_KINDS = [
|
|
226
256
|
"wrap_up",
|
|
227
257
|
"time_budget_exhausted",
|
|
258
|
+
"tool_cut",
|
|
228
259
|
"turn_budget_exhausted",
|
|
229
260
|
"sandbox_dead",
|
|
230
261
|
"fleet_busy",
|
|
231
262
|
"sandbox_restarted",
|
|
232
263
|
"stop_requested",
|
|
233
264
|
"stopped",
|
|
265
|
+
"stop_landed",
|
|
234
266
|
"spans_dropped",
|
|
235
267
|
"head_moved",
|
|
236
268
|
"run_failed",
|
|
@@ -241,8 +273,10 @@ export const RUN_NOTE_KINDS = [
|
|
|
241
273
|
"description_turn",
|
|
242
274
|
"verdict_turn",
|
|
243
275
|
"cold_sandbox",
|
|
276
|
+
"ledger_untracked",
|
|
244
277
|
"rebind_refused",
|
|
245
278
|
"work_left_behind",
|
|
279
|
+
"workspace_torn_down",
|
|
246
280
|
"pr_not_opened",
|
|
247
281
|
"review_not_posted",
|
|
248
282
|
"compacted",
|
|
@@ -251,6 +285,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
251
285
|
"tool_refused",
|
|
252
286
|
"settle_set_aside",
|
|
253
287
|
"tool_unnamed",
|
|
288
|
+
"directory_reached",
|
|
254
289
|
"budget_salvage",
|
|
255
290
|
"stuck_loop",
|
|
256
291
|
] as const satisfies readonly RunNoteKind[];
|
|
@@ -354,6 +389,7 @@ export function isHeadMaterial(event: RunEvent): boolean {
|
|
|
354
389
|
event.kind === "mcp_unavailable" ||
|
|
355
390
|
event.kind === "spans_dropped" ||
|
|
356
391
|
event.kind === "cold_sandbox" ||
|
|
392
|
+
event.kind === "ledger_untracked" ||
|
|
357
393
|
event.kind === "rebind_refused"
|
|
358
394
|
);
|
|
359
395
|
case "span_start":
|
|
@@ -460,7 +496,12 @@ export type RunEvent =
|
|
|
460
496
|
* executors' shared `exit N:` prefix, 0 for a clean run; absent when the code
|
|
461
497
|
* was not numeric). `output` is the tool's text — control-stripped, redacted,
|
|
462
498
|
* capped at TOOL_OUTPUT_CAP — for the run page's expandable card; the status
|
|
463
|
-
* card and the friction analyzer keep reading `summary`.
|
|
499
|
+
* card and the friction analyzer keep reading `summary`. `cut` marks the
|
|
500
|
+
* result of a call the run's ending cut rather than settled — pi's abort (the
|
|
501
|
+
* tool's own end after it), OpenCode's interrupt (a call still open when its
|
|
502
|
+
* loop left), a session's end (`closeOpenSpans`): the command behind it may
|
|
503
|
+
* still be running, and the workspace's release reads it so (`callsInFlight`,
|
|
504
|
+
* harness.md item 13). */
|
|
464
505
|
| {
|
|
465
506
|
type: "tool_result";
|
|
466
507
|
tool: string;
|
|
@@ -470,6 +511,7 @@ export type RunEvent =
|
|
|
470
511
|
exitCode?: number;
|
|
471
512
|
output?: string;
|
|
472
513
|
infra?: true;
|
|
514
|
+
cut?: true;
|
|
473
515
|
spanId?: string;
|
|
474
516
|
logIndex?: number;
|
|
475
517
|
seq?: number;
|
|
@@ -769,7 +811,18 @@ export type RunEvent =
|
|
|
769
811
|
* round and its fix round share an index. Published by the ship pipeline
|
|
770
812
|
* straight to the registry (like `pr_opened`), never through the runner.
|
|
771
813
|
* Additive: unknown → ignored. */
|
|
772
|
-
| {
|
|
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
|
+
}
|
|
773
826
|
/** The request router's decision (docs/reference/specs/routing-and-config.md
|
|
774
827
|
* item 21): the preset a plain message was routed to, the one-line reason
|
|
775
828
|
* the router gave (redacted, capped — the same text the card's `routed:`
|
|
@@ -291,6 +291,62 @@ export function pushedBranchesOf(events: readonly RunEvent[]): PushedBranch[] {
|
|
|
291
291
|
return [...byRef].map(([ref, pr]) => ({ ref, pr }));
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/** A call a run's ending may have left running in its workspace (harness.md
|
|
295
|
+
* item 13): the id the record pairs by, the tool, and the line the
|
|
296
|
+
* `workspace_torn_down` note names it by — the `tool_call`'s own summary
|
|
297
|
+
* while the record holds it, else the tool and the id. */
|
|
298
|
+
export interface CallInFlight {
|
|
299
|
+
callId: string;
|
|
300
|
+
tool: string;
|
|
301
|
+
summary: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The tool calls a run's ending may have left running in its workspace, in
|
|
305
|
+
* the record's order — what the workspace's release reads to tear the
|
|
306
|
+
* workspace down rather than wait for an idle the command would refuse, and
|
|
307
|
+
* what its note names (harness.md item 13). A call whose result is marked
|
|
308
|
+
* `cut` — pi's abort, OpenCode's interrupt, a session's end — was ended, not
|
|
309
|
+
* settled, whatever the run's status, and stays cut whatever lands for it
|
|
310
|
+
* later — an unmarked result (a post-turn's bridge reads the earlier
|
|
311
|
+
* execution's aborted settle as its own), or the call announced again under
|
|
312
|
+
* the same id; a call with no result at all was left open by a run that
|
|
313
|
+
* failed, was interrupted or hard-stopped, while on a run that completed or
|
|
314
|
+
* stopped softly it is a relayed tool that ran in the bot or a result lost to
|
|
315
|
+
* a transport gap, nothing running. Calls and results pair by id. The events
|
|
316
|
+
* are the registry's bounded backlog (`runRegistry/backlog.ts` drops the
|
|
317
|
+
* oldest past its count or byte bound), so on a long run the line of a
|
|
318
|
+
* command that hung early may be gone while its cut result — the ending's,
|
|
319
|
+
* landing last — survives: a cut result for a call the record no longer
|
|
320
|
+
* holds is the harness's own word that the call was ended and not settled,
|
|
321
|
+
* and counts by itself, named by its tool and id; an unmarked result for a
|
|
322
|
+
* call never opened counts for nothing. */
|
|
323
|
+
export function callsInFlight(events: readonly RunEvent[], status: RunStatus): CallInFlight[] {
|
|
324
|
+
const calls = new Map<string, { call: CallInFlight; state: "open" | "cut" | "settled" }>();
|
|
325
|
+
for (const e of events) {
|
|
326
|
+
if (e.type === "tool_call" && e.callId !== undefined) {
|
|
327
|
+
const seen = calls.get(e.callId);
|
|
328
|
+
if (seen?.state === "cut") {
|
|
329
|
+
// Announced again after its cut — or after an orphan cut, the line the
|
|
330
|
+
// backlog evicted now written again: the line is the record's, the
|
|
331
|
+
// state stays the cut's.
|
|
332
|
+
seen.call.tool = e.tool;
|
|
333
|
+
seen.call.summary = e.summary;
|
|
334
|
+
} else calls.set(e.callId, { call: { callId: e.callId, tool: e.tool, summary: e.summary }, state: "open" });
|
|
335
|
+
} else if (e.type === "tool_result" && e.callId !== undefined) {
|
|
336
|
+
const seen = calls.get(e.callId);
|
|
337
|
+
if (seen === undefined) {
|
|
338
|
+
if (e.cut === true)
|
|
339
|
+
calls.set(e.callId, {
|
|
340
|
+
call: { callId: e.callId, tool: e.tool, summary: `${e.tool} (call ${e.callId})` },
|
|
341
|
+
state: "cut",
|
|
342
|
+
});
|
|
343
|
+
} else if (seen.state !== "cut") seen.state = e.cut === true ? "cut" : "settled";
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const orderly = status === "completed" || status === "stopped_soft";
|
|
347
|
+
return [...calls.values()].filter((c) => c.state === "cut" || (c.state === "open" && !orderly)).map((c) => c.call);
|
|
348
|
+
}
|
|
349
|
+
|
|
294
350
|
/** The router's decision as a record carries it — the same fields the
|
|
295
351
|
* `route` event and the ledger row's `meta.route` carry (routing-and-config
|
|
296
352
|
* item 21). */
|
|
@@ -34,7 +34,18 @@ import type { Handoff } from "./handoff.js";
|
|
|
34
34
|
import { progressOf, renderRenewal, renewalDecision, type PushedHeadFact, type RenewalDecision } from "./renewal.js";
|
|
35
35
|
import type { RunStatus } from "../runRecord.js";
|
|
36
36
|
import { normalizeHead, sameCommit } from "../reviewedHead.js";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
ADDRESS_SEVERITIES,
|
|
39
|
+
DEFAULT_ADDRESS_SEVERITY,
|
|
40
|
+
findingsAtOrAbove,
|
|
41
|
+
formatFinding,
|
|
42
|
+
isAddressSeverity,
|
|
43
|
+
type AddressSeverity,
|
|
44
|
+
type AddressSeveritySource,
|
|
45
|
+
type Finding,
|
|
46
|
+
type FindingDisposition,
|
|
47
|
+
type ReviewVerdictKind,
|
|
48
|
+
} from "../reviewVerdict.js";
|
|
38
49
|
import { parsePlanUnit, planUnitIds } from "./contract.js";
|
|
39
50
|
import {
|
|
40
51
|
carve,
|
|
@@ -589,8 +600,22 @@ export type UnitEnding =
|
|
|
589
600
|
/** What a transition tells the driver beyond the next action: a round boundary
|
|
590
601
|
* the card draws (`shipRoundHeader`) and the run stream records, and the end. */
|
|
591
602
|
export type CoordinatorNote =
|
|
592
|
-
| {
|
|
603
|
+
| {
|
|
604
|
+
type: "round";
|
|
605
|
+
index: number;
|
|
606
|
+
agent: ChildPreset;
|
|
607
|
+
outcome: ShipRoundOutcome;
|
|
608
|
+
/** The severity gate fired on this approve (agent-ship item 9): the
|
|
609
|
+
* findings at or above the level in force, as `id (severity)`. The
|
|
610
|
+
* child's own verdict parser holds an approve to the same level
|
|
611
|
+
* (agent-review item 5a), so this should never be set — when it is, the
|
|
612
|
+
* verdict was parsed at another level (a lost directive, an older
|
|
613
|
+
* record, a harness around `submit_verdict`) and the row, the card and
|
|
614
|
+
* the log say so instead of routing silently into the findings step. */
|
|
615
|
+
gate?: { level: AddressSeverity; findings: string[] };
|
|
616
|
+
}
|
|
593
617
|
| { type: "ended"; ending: UnitEnding };
|
|
618
|
+
type RoundNote = Extract<CoordinatorNote, { type: "round" }>;
|
|
594
619
|
|
|
595
620
|
/** How the pipeline's budget went, in ms: the coding rounds' (round 0 and the
|
|
596
621
|
* findings steps), the review rounds', and everything else (branching,
|
|
@@ -598,26 +623,19 @@ export type CoordinatorNote =
|
|
|
598
623
|
* person can see whether the cap or the child is the problem. */
|
|
599
624
|
export type ShipBudgetSpent = Readonly<Record<"coding" | "review" | "waiting", number>>;
|
|
600
625
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
export
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
/** The findings the gate acts on at `level`: at or above it. Every parsed
|
|
615
|
-
* finding carries one of the ladder's four levels — the verdict parser
|
|
616
|
-
* (`parseVerdictInput`) drops an entry with any other severity, `fyi` or none,
|
|
617
|
-
* with a note — so nothing outside the ladder ever reaches the gate. */
|
|
618
|
-
export function findingsAtOrAbove(findings: readonly Finding[], level: AddressSeverity): Finding[] {
|
|
619
|
-
return findings.filter((f) => severityRank(f.severity) <= severityRank(level));
|
|
620
|
-
}
|
|
626
|
+
// The severity to address (agent-ship item 9's gate) is the verdict parser's
|
|
627
|
+
// ladder (src/core/reviewVerdict.ts): the same level the parser holds a
|
|
628
|
+
// submitted approve to, so a posted approve can carry a gated finding only
|
|
629
|
+
// when it was parsed at a different level — the check below is defense in
|
|
630
|
+
// depth behind the parser's. Re-exported here for the runner's callers.
|
|
631
|
+
export {
|
|
632
|
+
ADDRESS_SEVERITIES,
|
|
633
|
+
DEFAULT_ADDRESS_SEVERITY,
|
|
634
|
+
findingsAtOrAbove,
|
|
635
|
+
isAddressSeverity,
|
|
636
|
+
type AddressSeverity,
|
|
637
|
+
type AddressSeveritySource,
|
|
638
|
+
};
|
|
621
639
|
|
|
622
640
|
export interface UnitSession {
|
|
623
641
|
segment: number;
|
|
@@ -935,7 +953,10 @@ function end(s: UnitPipelineState, ending: UnitEnding, notes: CoordinatorNote[]
|
|
|
935
953
|
return { state: { ...s, phase: ENDED, ending }, notes: [...notes, { type: "ended", ending }] };
|
|
936
954
|
}
|
|
937
955
|
|
|
938
|
-
|
|
956
|
+
/** A gated finding as the gate note names it: `F1 (minor)`. */
|
|
957
|
+
const gateLabel = (f: Finding): string => `${f.id} (${f.severity})`;
|
|
958
|
+
|
|
959
|
+
const roundNote = (round: RoundRef, outcome: ShipRoundOutcome): RoundNote => ({
|
|
939
960
|
type: "round",
|
|
940
961
|
index: round.index,
|
|
941
962
|
agent: presetOf(round.kind),
|
|
@@ -1110,6 +1131,10 @@ function settleReview(
|
|
|
1110
1131
|
const level = next.input.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY;
|
|
1111
1132
|
const gated = findingsAtOrAbove(verdict.findings, level);
|
|
1112
1133
|
if (gated.length > 0) {
|
|
1134
|
+
// The gate fired — which the child's parser should have made impossible
|
|
1135
|
+
// (agent-review item 5a): the round note carries what it caught, so the
|
|
1136
|
+
// mismatch is seen and not just routed around.
|
|
1137
|
+
notes[0] = { ...roundNote(round, verdict.verdict), gate: { level, findings: gated.map(gateLabel) } };
|
|
1113
1138
|
if (mode !== undefined)
|
|
1114
1139
|
return end(
|
|
1115
1140
|
next,
|