@coreplane/switchboard 1.267.0 → 1.267.2
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/gc.ts +1 -0
- package/dist/assets/deploy/cloudflare-resident/keyedAsyncLock.ts +20 -0
- package/dist/assets/deploy/cloudflare-resident/legacyCredentials.ts +57 -0
- package/dist/assets/deploy/cloudflare-resident/orphanThreadUsers.ts +14 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +646 -204
- package/dist/assets/package-lock.json +51 -4
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/budgets.ts +10 -0
- package/dist/assets/src/core/coordinator/contract.ts +29 -0
- package/dist/assets/src/core/coordinator/driver.ts +15 -2
- package/dist/assets/src/core/publicationPush.ts +31 -9
- package/dist/assets/src/core/runRecord.ts +32 -0
- package/dist/assets/src/core/ship/coordinator.ts +53 -5
- package/dist/assets/src/execution/githubMintScope.ts +49 -0
- package/dist/assets/src/execution/residentCredentials.ts +9 -37
- package/dist/assets/src/execution/residentDestroyGate.ts +12 -0
- package/dist/assets/src/execution/residentPoolSpends.ts +96 -0
- package/dist/assets/src/execution/residentRecreateAdmission.ts +41 -0
- package/dist/assets/src/execution/residentSteps.ts +1 -0
- package/dist/assets/web/dist/.vite/manifest.json +505 -505
- package/dist/assets/web/dist/assets/AppShell-CCPwPqvc.js +1 -0
- package/dist/assets/web/dist/assets/{CostChart-4TeDdbvJ.js → CostChart-Y5BqJKLA.js} +2 -2
- package/dist/assets/web/dist/assets/CostsPage-RRggVvIx.js +1 -0
- package/dist/assets/web/dist/assets/DeliveryPage-CuIBLfup.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-BQmun9kS.js +1 -0
- package/dist/assets/web/dist/assets/InputMenu-ymuqElZB.js +1 -0
- package/dist/assets/web/dist/assets/{MetricsPage-BQBuIuYU.js → MetricsPage-D2YGsI8C.js} +1 -1
- package/dist/assets/web/dist/assets/NotFoundPage-DZ6ysTXd.js +1 -0
- package/dist/assets/web/dist/assets/PendingTurnRow-BPyJ2f-i.js +1 -0
- package/dist/assets/web/dist/assets/PlanePage-C_gVx0Dg.js +1 -0
- package/dist/assets/web/dist/assets/ResidentDetailPage-CT6v9INM.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CWqPFf40.js +1 -0
- package/dist/assets/web/dist/assets/{RunFoldRow-DrfFauYJ.js → RunFoldRow-C1w-mwch.js} +1 -1
- package/dist/assets/web/dist/assets/RunRoutePage-zWMHTzla.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DF16D2Gg.js +1 -0
- package/dist/assets/web/dist/assets/RunsTabs-D7rsC4GR.js +1 -0
- package/dist/assets/web/dist/assets/ScheduledPage-CyRs0fXN.js +1 -0
- package/dist/assets/web/dist/assets/SettingSelect-CEkCLVWd.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-BRuMCHSj.js +1 -0
- package/dist/assets/web/dist/assets/SilentTurn-D4MMDjWO.js +2 -0
- package/dist/assets/web/dist/assets/SlackMark-DjJTK2tD.js +1 -0
- package/dist/assets/web/dist/assets/StatusDot-BIAQN7iD.js +1 -0
- package/dist/assets/web/dist/assets/Tooltip-CxiQ7luL.js +1 -0
- package/dist/assets/web/dist/assets/UnitRoutePage-BQB5mAAl.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-BRRhsdQE.js → angular-html-DR_aiq_z.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-BFj-mqCm.js → angular-ts-DdAdPC99.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-CsBuy1IL.js → apl-CFsVwJXV.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-cVaqxBZq.js → astro-CV6XNX3T.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-MEP5mWha.js → blade-BGvEqTv7.js} +1 -1
- package/dist/assets/web/dist/assets/browser-BWnzWwaT.js +6 -0
- package/dist/assets/web/dist/assets/budgets-CJHiTZmM.js +1 -0
- package/dist/assets/web/dist/assets/{c-D4NGQ_Pd.js → c-ViUYuFAx.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-BkWImsIy.js → chapel-CZ7_YbfP.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-RPDSPSET.js → cobol-Dc8kYvS6.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-DZ5z7oVX.js → coffee-C0musOvn.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-CrBtIaRZ.js → cpp-ToPQGcGd.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DJesYi6f.js → crystal-DUJIaO8W.js} +1 -1
- package/dist/assets/web/dist/assets/{css-DO9mjau-.js → css-BTcEgWyQ.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-v3Py3_QR.js → dist-I2OEmPqz.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-C3VzbSW0.js → edge-D8sh1uAj.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-BkD_Vhfe.js → elixir-D5vOtHrB.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-KIM0mFPk.js → elm-B_5xqTuz.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-D2ruCqb2.js → erb-DBEct7_l.js} +1 -1
- package/dist/assets/web/dist/assets/{eventSource-Cv0AFlMi.js → eventSource-Ds6_gjPD.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-CnfbQTxA.js → git-rebase-lWoTL0Qh.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-CbiwFXWH.js → glimmer-js-DNfSP4GT.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-DV8ii0Pv.js → glimmer-ts-DpPeWMLs.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-CgmznWkA.js → glsl-m6Ziz8q0.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-BK-bBYfH.js → graphql-shD9521e.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-DnwTTP5n.js → hack-D_Mf_2gv.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DAVJsWwT.js → haml-BuEAF47S.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-voOVfxFL.js → handlebars-D2aiSYtv.js} +1 -1
- package/dist/assets/web/dist/assets/{html-3FTqwaPk.js → html-CRjXJZot.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-CSlhLDQn.js → html-derivative-CYpQX_KK.js} +1 -1
- package/dist/assets/web/dist/assets/{http-Vx4ilPfS.js → http-D6vXMpc_.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-DxyZDE3R.js → hurl-B634cMJT.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-BsWcHHVh.js → indexRow-BGjjLnN5.js} +1 -1
- package/dist/assets/web/dist/assets/{java-nWqdjNJG.js → java-BlINdAve.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-Dr82HOwp.js → javascript-BBEMmUOu.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-BhjLrPOt.js → jinja-BIA8TYHJ.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DilGIUa8.js → jison-aepfTbzH.js} +1 -1
- package/dist/assets/web/dist/assets/{json-CrOcvmhJ.js → json-DmYZ1boG.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-BW_RX-fo.js → jsx-GkaaF4_W.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-CWH071DB.js → julia-DRtMztOz.js} +1 -1
- package/dist/assets/web/dist/assets/{just-DkhhoWBy.js → just-PvoeEWoy.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-cuHJ4TUT.js → latex-Bujuo3AZ.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-DG6afYQN.js → liquid-B0h0HkS3.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-C5CMnCPS.js → lua-6O-O1CSO.js} +1 -1
- package/dist/assets/web/dist/assets/main-DYwKyLY5.js +28 -0
- package/dist/assets/web/dist/assets/{marko-Dy2_DuC4.js → marko-XS_tpV0N.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-BMKb_EeA.js → mdc-DxULuTS1.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-og0RgMKf.js → nginx-n2RAfCKE.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-CQxTQQP0.js → nim-DJxdqD8U.js} +1 -1
- package/dist/assets/web/dist/assets/{org-b5lkq_Pl.js → org-R9yxwZ2v.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-D9X5_i_J.js → perl-zKAUtl73.js} +1 -1
- package/dist/assets/web/dist/assets/{php-CwUxvLX0.js → php-Dk50x1WE.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-uOIOExr0.js → pug-C3TSg8JJ.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-g5RoADf-.js → qml-CL1nqvCt.js} +1 -1
- package/dist/assets/web/dist/assets/{r-5LdAopD3.js → r-Bo44Yl7y.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-DwrvFLYQ.js → razor-k2Dt2qXz.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-dGtAaxC-.js → regexp-Uy_HNe6G.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-wFdW0brr.js → rst-U0zcRJ60.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-DPglN6I9.js → ruby-DUyqwP37.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-CUbCs40d.js → sas-CxC8bRy1.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-B-DUiI1c.js → scss-DaqXj6RQ.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-lGOV7Ram.js → shellscript-CI49IoFh.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-BDWtAAjr.js → shellsession-DJxotIOK.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-zod_dcOy.js → soy-BjVAVuCU.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-BG0HSSoM.js → sql-CzxBKQd1.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-DuALaBPq.js → sseReplay-BDVyc8HX.js} +4 -4
- package/dist/assets/web/dist/assets/{stata-BVUMLp1W.js → stata-DLMv_9_X.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-DiSUbHKF.js → surrealql-BzbVEZbO.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-DZenT45Y.js → svelte-DTSfopUt.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-BWKRNM0-.js → templ-BPxFlKQj.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-CbMzBlkK.js → tex-B_KndQwK.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-CZsVorDN.js → ts-tags-CG7tgvBl.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-R1r3mH4W.js → tsx-CN9eqIdJ.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-BjPMyjKH.js → twig-CKPbB5ca.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-Bx-66VN_.js → typescript-BQAXVjHJ.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-fW037Eo6.js → typst-D3EaP9Ui.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-D1V_GNxW.js → vue-DuQwQ__D.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-C-4pTZ-s.js → vue-html-UEJn5cLD.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-C-QsX3hR.js → vue-vine-DDdijZY2.js} +1 -1
- package/dist/assets/web/dist/assets/{wallClock-x0hjr0J_.js → wallClock-DaewxNMY.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-BJTJXPgC.js → xml-BezNDynk.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-Xc9lukqe.js → xsl-Bzb2zIr8.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-Dw-FMsQa.js → yaml-DOccVRRG.js} +1 -1
- package/dist/cli.js +1002 -366
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-DPJVhUGP.js +0 -1
- package/dist/assets/web/dist/assets/CostsPage-8HDgvuWB.js +0 -1
- package/dist/assets/web/dist/assets/DeliveryPage-BUJckSuW.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-BfpIrKDX.js +0 -1
- package/dist/assets/web/dist/assets/InputMenu-BRV35UWP.js +0 -1
- package/dist/assets/web/dist/assets/NotFoundPage-CDjMfCcj.js +0 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-C6hroa00.js +0 -1
- package/dist/assets/web/dist/assets/PlanePage-C2Cb1mkT.js +0 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-Bd9_Q87l.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-C8eS7hxp.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-B_eXfNAF.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-BpQu36Dm.js +0 -1
- package/dist/assets/web/dist/assets/RunsTabs-C7Ua3XX5.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DGHcjcSt.js +0 -1
- package/dist/assets/web/dist/assets/SettingSelect-CEeA4iuC.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-IfQ71QjN.js +0 -1
- package/dist/assets/web/dist/assets/SilentTurn-D9ON15a8.js +0 -2
- package/dist/assets/web/dist/assets/SlackMark-DB0MGiF5.js +0 -1
- package/dist/assets/web/dist/assets/StatusDot-DyFjGg4V.js +0 -1
- package/dist/assets/web/dist/assets/Tooltip-BvPkV4kZ.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-BNe3nBIG.js +0 -1
- package/dist/assets/web/dist/assets/browser-CCImen5O.js +0 -6
- package/dist/assets/web/dist/assets/budgets-BQgcCyvb.js +0 -1
- package/dist/assets/web/dist/assets/main-ks3kBojV.js +0 -28
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.267.
|
|
3
|
+
"version": "1.267.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.267.
|
|
9
|
+
"version": "1.267.2",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -21023,7 +21023,7 @@
|
|
|
21023
21023
|
},
|
|
21024
21024
|
"packages/switchboard": {
|
|
21025
21025
|
"name": "@coreplane/switchboard",
|
|
21026
|
-
"version": "1.267.
|
|
21026
|
+
"version": "1.267.2",
|
|
21027
21027
|
"license": "Apache-2.0",
|
|
21028
21028
|
"dependencies": {
|
|
21029
21029
|
"@earendil-works/pi-ai": "0.85.1",
|
|
@@ -21053,7 +21053,7 @@
|
|
|
21053
21053
|
"dependencies": {
|
|
21054
21054
|
"@nuxt/ui": "^4.11.0",
|
|
21055
21055
|
"@pierre/diffs": "1.4.2",
|
|
21056
|
-
"@vueuse/core": "^
|
|
21056
|
+
"@vueuse/core": "^15.0.0",
|
|
21057
21057
|
"vue": "^3.5.42",
|
|
21058
21058
|
"vue-router": "^5.3.1"
|
|
21059
21059
|
},
|
|
@@ -21076,6 +21076,53 @@
|
|
|
21076
21076
|
"engines": {
|
|
21077
21077
|
"node": ">=22"
|
|
21078
21078
|
}
|
|
21079
|
+
},
|
|
21080
|
+
"web/node_modules/@vueuse/core": {
|
|
21081
|
+
"version": "15.0.0",
|
|
21082
|
+
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-15.0.0.tgz",
|
|
21083
|
+
"integrity": "sha512-Zv67h5mrk7YHDJsRObx+DXaCBz6RKBUpxqLesJ+3xZNPG6yuQs7ja7kg3veFrrouYLWAqydQ7CwGEbLVxB43Ng==",
|
|
21084
|
+
"license": "MIT",
|
|
21085
|
+
"dependencies": {
|
|
21086
|
+
"@types/web-bluetooth": "^0.0.21",
|
|
21087
|
+
"@vueuse/metadata": "15.0.0",
|
|
21088
|
+
"@vueuse/shared": "15.0.0"
|
|
21089
|
+
},
|
|
21090
|
+
"engines": {
|
|
21091
|
+
"node": ">=22"
|
|
21092
|
+
},
|
|
21093
|
+
"funding": {
|
|
21094
|
+
"url": "https://github.com/sponsors/antfu"
|
|
21095
|
+
},
|
|
21096
|
+
"peerDependencies": {
|
|
21097
|
+
"vue": "^3.5.0"
|
|
21098
|
+
}
|
|
21099
|
+
},
|
|
21100
|
+
"web/node_modules/@vueuse/core/node_modules/@vueuse/shared": {
|
|
21101
|
+
"version": "15.0.0",
|
|
21102
|
+
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-15.0.0.tgz",
|
|
21103
|
+
"integrity": "sha512-vbNXhGrbV53xPOMSyOzMHQRvhMgZf7oNDw2RChUetVxwTIEkInQ1joZ9JbT0G3z2n2AJNjvmCWeM9ySYrnvogA==",
|
|
21104
|
+
"license": "MIT",
|
|
21105
|
+
"engines": {
|
|
21106
|
+
"node": ">=22"
|
|
21107
|
+
},
|
|
21108
|
+
"funding": {
|
|
21109
|
+
"url": "https://github.com/sponsors/antfu"
|
|
21110
|
+
},
|
|
21111
|
+
"peerDependencies": {
|
|
21112
|
+
"vue": "^3.5.0"
|
|
21113
|
+
}
|
|
21114
|
+
},
|
|
21115
|
+
"web/node_modules/@vueuse/metadata": {
|
|
21116
|
+
"version": "15.0.0",
|
|
21117
|
+
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-15.0.0.tgz",
|
|
21118
|
+
"integrity": "sha512-sfWE5LADAKJ0KnjtGliMdSN8Ij/L7uP6rXIZlgPloAI4zvvefbw6H5Lt7KJA6RqQOYgVqWF5Guj50HHMIzygBg==",
|
|
21119
|
+
"license": "MIT",
|
|
21120
|
+
"engines": {
|
|
21121
|
+
"node": ">=22"
|
|
21122
|
+
},
|
|
21123
|
+
"funding": {
|
|
21124
|
+
"url": "https://github.com/sponsors/antfu"
|
|
21125
|
+
}
|
|
21079
21126
|
}
|
|
21080
21127
|
}
|
|
21081
21128
|
}
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.267.
|
|
3
|
+
"version": "1.267.2",
|
|
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
|
@@ -64,6 +64,16 @@ export const QUESTION_TTL_MS = DAY_MS;
|
|
|
64
64
|
* that FAILED is never cached — the next read asks again. */
|
|
65
65
|
export const AUTHOR_BINDING_TTL_MS = 15 * MINUTE_MS;
|
|
66
66
|
|
|
67
|
+
/** Maximum local lifetime of any Git receive-pack forward. The bounded
|
|
68
|
+
* request prevents a write connection outliving its run indefinitely after
|
|
69
|
+
* revocation. An aborted remote write has an unknown outcome; existing-PR
|
|
70
|
+
* writes retain their owned pending intent for authoritative reconciliation. */
|
|
71
|
+
export const GIT_RECEIVE_PACK_FORWARD_TIMEOUT_MS = 5 * MINUTE_MS;
|
|
72
|
+
/** The run's final record waits for that one forwarded report, plus a small
|
|
73
|
+
* durable-receipt margin, before sealing. If it still has no answer, the
|
|
74
|
+
* pending intent stays as an uncertain write and no retry is authorized. */
|
|
75
|
+
export const GIT_PUBLICATION_SETTLE_TIMEOUT_MS = GIT_RECEIVE_PACK_FORWARD_TIMEOUT_MS + 10 * SECOND_MS;
|
|
76
|
+
|
|
67
77
|
/** How long a dispatch's FIRST attach to a resident — a fresh run's, or a
|
|
68
78
|
* resumed run's re-attach to its recorded worktree — waits for the resident
|
|
69
79
|
* to wake when the Worker typed its refusal as the platform's transient
|
|
@@ -493,6 +493,15 @@ export interface RoundGate {
|
|
|
493
493
|
findings: string[];
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
+
/** A later human review is evidence to investigate, never typed findings. */
|
|
497
|
+
export interface RecoveryReviewEvidence {
|
|
498
|
+
id: number;
|
|
499
|
+
reviewer: { login: string; id: number };
|
|
500
|
+
headSha: string;
|
|
501
|
+
submittedAt: number;
|
|
502
|
+
body: string;
|
|
503
|
+
}
|
|
504
|
+
|
|
496
505
|
/** The original grant and historically priced children carried through a recovery checkpoint. */
|
|
497
506
|
export interface RecoveryAccounting {
|
|
498
507
|
spendUsd: number;
|
|
@@ -506,6 +515,7 @@ export interface RecoveryAccounting {
|
|
|
506
515
|
* the original instance/unit idempotency namespace. */
|
|
507
516
|
export interface OriginalUnitRecovery {
|
|
508
517
|
kind: "findings" | "review";
|
|
518
|
+
externalReview?: RecoveryReviewEvidence;
|
|
509
519
|
accounting?: RecoveryAccounting;
|
|
510
520
|
round: number;
|
|
511
521
|
expectedHeadSha: string;
|
|
@@ -540,6 +550,7 @@ export interface OriginalUnitRecovery {
|
|
|
540
550
|
}
|
|
541
551
|
|
|
542
552
|
export interface OriginalUnitRecoveryReceipt {
|
|
553
|
+
externalReview?: RecoveryReviewEvidence;
|
|
543
554
|
accounting?: RecoveryAccounting;
|
|
544
555
|
reviewRunId: string;
|
|
545
556
|
workflowId: string;
|
|
@@ -767,7 +778,18 @@ export const isUnitWakeAnswer = (v: unknown): v is UnitWakeAnswer => {
|
|
|
767
778
|
);
|
|
768
779
|
};
|
|
769
780
|
|
|
781
|
+
const isPositiveId = (v: unknown): v is number => typeof v === "number" && Number.isSafeInteger(v) && v > 0;
|
|
770
782
|
const isDollars = (v: unknown): v is number => isFinite(v) && v >= 0;
|
|
783
|
+
const isRecoveryReview = (v: unknown): v is RecoveryReviewEvidence =>
|
|
784
|
+
isObject(v) &&
|
|
785
|
+
isPositiveId(v.id) &&
|
|
786
|
+
isObject(v.reviewer) &&
|
|
787
|
+
isText(v.reviewer.login) &&
|
|
788
|
+
isPositiveId(v.reviewer.id) &&
|
|
789
|
+
typeof v.headSha === "string" &&
|
|
790
|
+
/^[0-9a-f]{40}$/i.test(v.headSha) &&
|
|
791
|
+
isFinite(v.submittedAt) &&
|
|
792
|
+
typeof v.body === "string";
|
|
771
793
|
const isRecoveryAccounting = (v: unknown): v is RecoveryAccounting =>
|
|
772
794
|
isObject(v) &&
|
|
773
795
|
isDollars(v.spendUsd) &&
|
|
@@ -839,6 +861,12 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
|
839
861
|
STEP_NAME_PATTERN.test(r.recovery.step) &&
|
|
840
862
|
isText(r.recovery.reviewRunId) &&
|
|
841
863
|
(r.recovery.accounting === undefined || isRecoveryAccounting(r.recovery.accounting)) &&
|
|
864
|
+
(r.recovery.externalReview === undefined ||
|
|
865
|
+
(isRecoveryReview(r.recovery.externalReview) &&
|
|
866
|
+
r.recovery.kind === "review" &&
|
|
867
|
+
isRecoveryAccounting(r.recovery.accounting) &&
|
|
868
|
+
r.recovery.findings === undefined &&
|
|
869
|
+
r.recovery.findingsRunId === undefined)) &&
|
|
842
870
|
(r.recovery.findingsRunId === undefined || isText(r.recovery.findingsRunId)) &&
|
|
843
871
|
(r.recovery.findingsKey === undefined || isText(r.recovery.findingsKey)) &&
|
|
844
872
|
((r.recovery.findingsRunId === undefined && r.recovery.findingsKey === undefined) ||
|
|
@@ -869,6 +897,7 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
|
869
897
|
r.recoveryReceipt !== undefined &&
|
|
870
898
|
(!isObject(r.recoveryReceipt) ||
|
|
871
899
|
!isText(r.recoveryReceipt.reviewRunId) ||
|
|
900
|
+
(r.recoveryReceipt.externalReview !== undefined && !isRecoveryReview(r.recoveryReceipt.externalReview)) ||
|
|
872
901
|
(r.recoveryReceipt.accounting !== undefined && !isRecoveryAccounting(r.recoveryReceipt.accounting)) ||
|
|
873
902
|
typeof r.recoveryReceipt.workflowId !== "string" ||
|
|
874
903
|
!INSTANCE_ID_PATTERN.test(r.recoveryReceipt.workflowId) ||
|
|
@@ -687,6 +687,18 @@ function isOpaqueNotFound(reply: BotReply): boolean {
|
|
|
687
687
|
}
|
|
688
688
|
}
|
|
689
689
|
|
|
690
|
+
/** A finished, addressed child whose final record is still being sealed.
|
|
691
|
+
* Unlike an opaque 404, this answer already establishes the child's identity. */
|
|
692
|
+
function isRecordPending(reply: BotReply): boolean {
|
|
693
|
+
if (reply.status !== 409) return false;
|
|
694
|
+
try {
|
|
695
|
+
const body = JSON.parse(reply.text) as { ok?: unknown; error?: unknown };
|
|
696
|
+
return body?.ok === false && body.error === "record_pending";
|
|
697
|
+
} catch {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
690
702
|
async function readRecordStep(
|
|
691
703
|
step: StepRunner,
|
|
692
704
|
bot: CoordinatorBot,
|
|
@@ -696,8 +708,9 @@ async function readRecordStep(
|
|
|
696
708
|
for (let retry = 0; ; retry++) {
|
|
697
709
|
const name = retry === 0 ? action.step : `${action.step}/record-read/${retry}`;
|
|
698
710
|
const reply = await step.do(name, STEP_CONFIG, () => call(bot, "read-record", body, true));
|
|
699
|
-
|
|
700
|
-
|
|
711
|
+
const pending = isRecordPending(reply);
|
|
712
|
+
const eventQualifiedMissing = action.finishedObserved === true && isOpaqueNotFound(reply);
|
|
713
|
+
if ((!pending && !eventQualifiedMissing) || retry >= SHIP_RECORD_VISIBILITY.retries) return reply;
|
|
701
714
|
await step.sleep(`${action.step}/record-visible/${retry + 1}`, SHIP_RECORD_VISIBILITY.retryMs);
|
|
702
715
|
}
|
|
703
716
|
}
|
|
@@ -15,14 +15,19 @@ const refName = (s: string): boolean =>
|
|
|
15
15
|
/^[a-zA-Z0-9_/-]+(?:\.[a-zA-Z0-9_/-]+)*$/.test(s) && !s.includes("..") && !s.startsWith("-");
|
|
16
16
|
const shortRef = (s: string): string => s.replace(/^refs\/heads\//, "");
|
|
17
17
|
|
|
18
|
+
/** Broad enough to count competing Git config pushes; leasedPushCommand
|
|
19
|
+
* separately proves whether one selected call is authorized evidence. */
|
|
20
|
+
export const mentionsGitPush = (command: string): boolean => /\bgit(?:\s+-c\s+\S+)*\s+push\b/.test(command);
|
|
21
|
+
|
|
18
22
|
/** Deliberately not a shell interpreter. Only a standalone literal push can
|
|
19
23
|
* attest its result: pipelines, scripts, substitutions and compound commands
|
|
20
24
|
* cannot prove which operation produced stdout or the successful exit. */
|
|
21
25
|
export function leasedPushCommand(command: string): { ref: string; expectedHeadSha: string } | undefined {
|
|
22
26
|
if (command.length >= COMMAND_CAP) return;
|
|
23
27
|
const literal = command.replace(/[ \t]+2>&1[ \t]*$/, "");
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
const prefix = /^git(?: -c http\.postBuffer=[0-9]+)? push\s/.exec(literal);
|
|
29
|
+
if (!prefix || /[\n\r'"`$;|<>\\&]/.test(literal)) return;
|
|
30
|
+
const words = literal.slice(prefix[0].length).trim().split(/\s+/);
|
|
26
31
|
const leases = words.filter((w) => w.startsWith("--force-with-lease="));
|
|
27
32
|
if (leases.length !== 1) return;
|
|
28
33
|
const lease = /^--force-with-lease=refs\/heads\/([^:]+):([a-f0-9]{40})$/.exec(leases[0]!);
|
|
@@ -42,13 +47,14 @@ export function pairedPublicationPush(
|
|
|
42
47
|
binding: ExistingPrPublicationBinding,
|
|
43
48
|
head: string,
|
|
44
49
|
callId?: string,
|
|
50
|
+
gitDoorBaseUrl?: string,
|
|
45
51
|
): PushEvent | undefined {
|
|
46
52
|
if (!fullSha(head) || !fullSha(binding.expectedHeadSha) || head === binding.expectedHeadSha) return;
|
|
47
53
|
const calls = events.filter(
|
|
48
54
|
(e): e is Extract<RunEvent, { type: "tool_call" }> =>
|
|
49
55
|
e.type === "tool_call" &&
|
|
50
56
|
e.tool === "bash" &&
|
|
51
|
-
(callId === undefined ?
|
|
57
|
+
(callId === undefined ? mentionsGitPush(e.command ?? e.summary) : e.callId === callId),
|
|
52
58
|
);
|
|
53
59
|
if (calls.length !== 1) return;
|
|
54
60
|
const call = calls[0]!;
|
|
@@ -76,20 +82,36 @@ export function pairedPublicationPush(
|
|
|
76
82
|
)
|
|
77
83
|
return;
|
|
78
84
|
let lines = result.output.trim().split(/\r?\n/);
|
|
79
|
-
const tracking =
|
|
85
|
+
const tracking = [
|
|
86
|
+
`branch '${binding.publicationRef}' set up to track 'origin/${binding.publicationRef}'.`,
|
|
87
|
+
`Branch '${binding.publicationRef}' set up to track remote branch '${binding.publicationRef}' from 'origin'.`,
|
|
88
|
+
];
|
|
80
89
|
if (
|
|
81
90
|
/(?:^|\s)(?:-u|--set-upstream)(?:\s|$)/.test(call.command) &&
|
|
82
|
-
lines.filter((line) => line
|
|
91
|
+
lines.filter((line) => tracking.includes(line)).length === 1
|
|
83
92
|
)
|
|
84
|
-
lines = lines.filter((line) => line
|
|
93
|
+
lines = lines.filter((line) => !tracking.includes(line));
|
|
85
94
|
if (lines.length !== 2) return;
|
|
86
|
-
const
|
|
95
|
+
const githubRemote = /^To (?:https:\/\/github\.com\/|git@github\.com:|github\.com:)([^\s]+?)(?:\.git)?$/.exec(
|
|
96
|
+
lines[0]!,
|
|
97
|
+
);
|
|
98
|
+
let doorRemote: string | undefined;
|
|
99
|
+
if (gitDoorBaseUrl !== undefined) {
|
|
100
|
+
try {
|
|
101
|
+
doorRemote = `${new URL(gitDoorBaseUrl).origin}/git/${binding.repo}`;
|
|
102
|
+
} catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const remoteMatches =
|
|
107
|
+
doorRemote === undefined
|
|
108
|
+
? githubRemote?.[1]?.toLowerCase() === binding.repo.toLowerCase()
|
|
109
|
+
: lines[0] === `To ${doorRemote}` || lines[0] === `To ${doorRemote}.git`;
|
|
87
110
|
const update = /^\s*\+?\s*([a-f0-9]{7,40})(\.{2,3})([a-f0-9]{7,40})\s+(\S+) -> (\S+)( \(forced update\))?$/.exec(
|
|
88
111
|
lines[1]!,
|
|
89
112
|
);
|
|
90
113
|
if (
|
|
91
|
-
!
|
|
92
|
-
remote[1]!.toLowerCase() !== binding.repo.toLowerCase() ||
|
|
114
|
+
!remoteMatches ||
|
|
93
115
|
!update ||
|
|
94
116
|
!binding.expectedHeadSha.startsWith(update[1]!) ||
|
|
95
117
|
!head.startsWith(update[3]!) ||
|
|
@@ -279,6 +279,17 @@ export interface RunRecord {
|
|
|
279
279
|
* last `pushed_head` event's sha winning — the fact a renewal reads to know
|
|
280
280
|
* the run made progress. Present only on a run that pushed. */
|
|
281
281
|
pushed?: PushedHead[];
|
|
282
|
+
/** A Git-door write whose remote outcome was still uncertain at seal. The
|
|
283
|
+
* live ledger row is deleted at finish, so recovery retains this exact
|
|
284
|
+
* ref transition until head reconciliation. A PR number exists only for a
|
|
285
|
+
* verified existing-PR publication. */
|
|
286
|
+
doorPublicationPending?: {
|
|
287
|
+
id: string;
|
|
288
|
+
repo: string;
|
|
289
|
+
pr?: number;
|
|
290
|
+
owner?: { instanceId: string; unit: string };
|
|
291
|
+
update: { ref: string; old: string; next: string };
|
|
292
|
+
};
|
|
282
293
|
/** What the run cost in tokens, per model, summed from its `model.turn`
|
|
283
294
|
* spans at finish (`usageOfEvents`; docs/reference/specs/costs.md, cost by user).
|
|
284
295
|
* Every record written since carries it (zero turns included); one written
|
|
@@ -1124,6 +1135,27 @@ export function isRunRecord(v: unknown): v is RunRecord {
|
|
|
1124
1135
|
)
|
|
1125
1136
|
)
|
|
1126
1137
|
return false;
|
|
1138
|
+
if (r.doorPublicationPending !== undefined) {
|
|
1139
|
+
const pending = r.doorPublicationPending;
|
|
1140
|
+
if (typeof pending !== "object" || pending === null) return false;
|
|
1141
|
+
const { id, repo, pr, owner, update } = pending as Record<string, unknown>;
|
|
1142
|
+
if (
|
|
1143
|
+
typeof id !== "string" ||
|
|
1144
|
+
!id ||
|
|
1145
|
+
typeof repo !== "string" ||
|
|
1146
|
+
!repo ||
|
|
1147
|
+
(pr !== undefined && (!Number.isInteger(pr) || (pr as number) < 1))
|
|
1148
|
+
)
|
|
1149
|
+
return false;
|
|
1150
|
+
if (owner !== undefined && (typeof owner !== "object" || owner === null)) return false;
|
|
1151
|
+
if (typeof update !== "object" || update === null) return false;
|
|
1152
|
+
const o = owner as Record<string, unknown> | undefined;
|
|
1153
|
+
const u = update as Record<string, unknown>;
|
|
1154
|
+
if (o !== undefined && (typeof o.instanceId !== "string" || typeof o.unit !== "string")) return false;
|
|
1155
|
+
if (typeof u.ref !== "string" || !u.ref.startsWith("refs/heads/")) return false;
|
|
1156
|
+
if (typeof u.old !== "string" || !/^[0-9a-f]{40}$/.test(u.old)) return false;
|
|
1157
|
+
if (typeof u.next !== "string" || !/^[0-9a-f]{40}$/.test(u.next)) return false;
|
|
1158
|
+
}
|
|
1127
1159
|
// A parent is named by a run id (item 46): the same shape as the record's own.
|
|
1128
1160
|
if (r.parentRunId !== undefined && (typeof r.parentRunId !== "string" || !RUN_ID_PATTERN.test(r.parentRunId)))
|
|
1129
1161
|
return false;
|
|
@@ -1243,6 +1243,9 @@ type Phase =
|
|
|
1243
1243
|
* must independently prove an open branch and pull request at exactly
|
|
1244
1244
|
* `childHead`; neither a push record nor this flag is remote evidence. */
|
|
1245
1245
|
findingsReady?: true;
|
|
1246
|
+
/** A capped recovery must verify this pushed head before ending, but
|
|
1247
|
+
* missing findings outputs still forbid review or merge-ready success. */
|
|
1248
|
+
findingsIncomplete?: string[];
|
|
1246
1249
|
/** The coding child died (`failed` or `interrupted`) after it may have
|
|
1247
1250
|
* pushed: the pr-check recovers a pushed branch by opening its pull
|
|
1248
1251
|
* request; with nothing pushed the unit ends with the child's own reason. */
|
|
@@ -1648,7 +1651,7 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
1648
1651
|
return {
|
|
1649
1652
|
type: "pr-check",
|
|
1650
1653
|
step: `${roundStep(s, p.round)}/pr-check`,
|
|
1651
|
-
...(p.dead !== undefined || p.recover === true || p.findingsReady === true
|
|
1654
|
+
...(p.dead !== undefined || p.recover === true || p.findingsReady === true || p.findingsIncomplete !== undefined
|
|
1652
1655
|
? { recover: { runId: p.runId } }
|
|
1653
1656
|
: {}),
|
|
1654
1657
|
// The adopted pull request rides the check so the bot can follow it
|
|
@@ -1884,7 +1887,8 @@ function capEnding(s: UnitPipelineState, round?: RoundRef, refused?: Carve): Uni
|
|
|
1884
1887
|
};
|
|
1885
1888
|
}
|
|
1886
1889
|
|
|
1887
|
-
/**
|
|
1890
|
+
/** Recovery cannot buy another child (or claim readiness) after the original
|
|
1891
|
+
* dollar cap is spent. Unknown cost is not zero and never resets that cap. */
|
|
1888
1892
|
function recoveryCostEnding(s: UnitPipelineState): UnitEnding | undefined {
|
|
1889
1893
|
const cap = s.input.grant?.costCapUsd;
|
|
1890
1894
|
if (s.input.recovery === undefined || cap === undefined) return undefined;
|
|
@@ -2037,6 +2041,21 @@ function settleCoding(
|
|
|
2037
2041
|
if (facts.description !== true) missingOutputs.push("missing updated pull request description");
|
|
2038
2042
|
const observedHead = fullHead(facts.headSha);
|
|
2039
2043
|
if (observedHead === undefined) missingOutputs.push("missing final observed commit");
|
|
2044
|
+
if (
|
|
2045
|
+
missingOutputs.length > 0 &&
|
|
2046
|
+
costEnding !== undefined &&
|
|
2047
|
+
observedHead !== undefined &&
|
|
2048
|
+
facts.pushed?.length === 1 &&
|
|
2049
|
+
facts.pushed[0]?.ref === next.input.unit.branch &&
|
|
2050
|
+
facts.pushed[0]?.sha === observedHead
|
|
2051
|
+
)
|
|
2052
|
+
return {
|
|
2053
|
+
state: {
|
|
2054
|
+
...next,
|
|
2055
|
+
phase: { at: "pr-check", round, runId, childHead: observedHead, findingsIncomplete: missingOutputs },
|
|
2056
|
+
},
|
|
2057
|
+
notes: [],
|
|
2058
|
+
};
|
|
2040
2059
|
if (missingOutputs.length > 0)
|
|
2041
2060
|
return end(
|
|
2042
2061
|
next,
|
|
@@ -2656,7 +2675,7 @@ function finishSupersededChild(
|
|
|
2656
2675
|
if (phase.round.kind === "findings") {
|
|
2657
2676
|
const settled = settleCoding(s, phase.round, phase.runId, facts);
|
|
2658
2677
|
const ready = settled.state.phase;
|
|
2659
|
-
if (ready.at === "pr-check" && ready.findingsReady === true)
|
|
2678
|
+
if (ready.at === "pr-check" && (ready.findingsReady === true || ready.findingsIncomplete !== undefined))
|
|
2660
2679
|
return settlePrCheck(settled.state, ready, supersession.pullRequest);
|
|
2661
2680
|
return settled;
|
|
2662
2681
|
}
|
|
@@ -2709,11 +2728,24 @@ export function reconcileTerminalPr(s: UnitPipelineState, pr: PrCheck): UnitPipe
|
|
|
2709
2728
|
/** The pull request heading the branch after round 0 or a findings step. */
|
|
2710
2729
|
function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-check" }>, pr: PrCheck): Transition {
|
|
2711
2730
|
const { round } = phase;
|
|
2731
|
+
const endIncompleteFindings = (verified: UnitPipelineState): Transition => {
|
|
2732
|
+
const cap = recoveryCostEnding(verified);
|
|
2733
|
+
return end(
|
|
2734
|
+
verified,
|
|
2735
|
+
{
|
|
2736
|
+
kind: "aborted",
|
|
2737
|
+
reason: `${cap?.kind === "aborted" ? `${cap.reason} ` : ""}The completed findings work still lacks required results (${phase.findingsIncomplete?.join("; ")}); no review started.`,
|
|
2738
|
+
round,
|
|
2739
|
+
reviewRounds: verified.reviewRounds,
|
|
2740
|
+
},
|
|
2741
|
+
[roundNote(round, "aborted")],
|
|
2742
|
+
);
|
|
2743
|
+
};
|
|
2712
2744
|
// A completed findings run is not ready because it says it pushed. It is
|
|
2713
2745
|
// ready only when this independent read sees the adopted pull request at the
|
|
2714
2746
|
// exact full observed source head (and, while open, its live branch). Every
|
|
2715
2747
|
// other remote fact is a resumable stop with no review or merged success.
|
|
2716
|
-
if (phase.findingsReady === true) {
|
|
2748
|
+
if (phase.findingsReady === true || phase.findingsIncomplete !== undefined) {
|
|
2717
2749
|
const observedHead = fullHead(phase.childHead)!;
|
|
2718
2750
|
const withPr = pr.state !== "none" ? { ...s, pr: { number: pr.prNumber, url: pr.url } } : s;
|
|
2719
2751
|
if (pr.state === "merged") {
|
|
@@ -2745,6 +2777,8 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
2745
2777
|
},
|
|
2746
2778
|
[roundNote(round, "aborted")],
|
|
2747
2779
|
);
|
|
2780
|
+
if (phase.findingsIncomplete !== undefined)
|
|
2781
|
+
return endIncompleteFindings({ ...withPr, lastReviewHead: remoteHead });
|
|
2748
2782
|
return foundMerged(withPr, pr, [roundNote(round, "completed")]);
|
|
2749
2783
|
}
|
|
2750
2784
|
if (pr.state !== "open" || pr.headBranchExists === false)
|
|
@@ -2788,6 +2822,10 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
2788
2822
|
},
|
|
2789
2823
|
[roundNote(round, "aborted")],
|
|
2790
2824
|
);
|
|
2825
|
+
const reconciled = { ...withPr, lastReviewHead: remoteHead };
|
|
2826
|
+
if (phase.findingsIncomplete !== undefined) return endIncompleteFindings(reconciled);
|
|
2827
|
+
const costEnding = recoveryCostEnding(reconciled);
|
|
2828
|
+
if (costEnding !== undefined) return end(reconciled, costEnding, [roundNote(round, "aborted")]);
|
|
2791
2829
|
return roundOnOpenPr(withPr, phase, pr);
|
|
2792
2830
|
}
|
|
2793
2831
|
// The merge landed during the round: the child found nothing left to ship
|
|
@@ -3845,9 +3883,16 @@ function renderUnitReportWithWake(
|
|
|
3845
3883
|
const rounds = `${e.reviewRounds} review round${e.reviewRounds === 1 ? "" : "s"}`;
|
|
3846
3884
|
const prUrl = s.pr?.url;
|
|
3847
3885
|
const prLine = prUrl ? ` PR: ${prUrl}` : "";
|
|
3886
|
+
const spentCapAction = (): string | undefined => {
|
|
3887
|
+
const cap = s.input.grant?.costCapUsd;
|
|
3888
|
+
if (cap === undefined || s.spendUsd === null || s.spendUsd < cap) return undefined;
|
|
3889
|
+
return `Next action: $${s.spendUsd.toFixed(2)} spent against the original $${cap.toFixed(2)} cost cap; this unit cannot start another child. Continue ${prUrl ? `the existing pull request (${prUrl})` : "the saved branch"} only under separate authorization, then request review of its exact head.`;
|
|
3890
|
+
};
|
|
3848
3891
|
const nextAction = (checkpoint?: { branch: string; sha: string }): string => {
|
|
3849
3892
|
if (durableWake)
|
|
3850
3893
|
return `The unit remains live; a reply in this thread is recorded as its continuation action${prUrl ? ` from the open pull request (${prUrl})` : " from the saved branch state"}.`;
|
|
3894
|
+
const cappedAction = spentCapAction();
|
|
3895
|
+
if (cappedAction !== undefined) return cappedAction;
|
|
3851
3896
|
const reconcile = checkpoint
|
|
3852
3897
|
? `verify \`${checkpoint.sha.slice(0, 7)}\` is on \`${checkpoint.branch}\`, then reconcile the remote branch and pull request if needed`
|
|
3853
3898
|
: prUrl
|
|
@@ -4129,7 +4174,10 @@ function renderUnitReportWithWake(
|
|
|
4129
4174
|
]);
|
|
4130
4175
|
case "no_verdict":
|
|
4131
4176
|
if (!shows(verbosity, "verbose"))
|
|
4132
|
-
return
|
|
4177
|
+
return join([
|
|
4178
|
+
`⚠️ No verdict from review round ${e.round.index} — aborted after ${rounds}.${prLine}`,
|
|
4179
|
+
spentCapAction(),
|
|
4180
|
+
]);
|
|
4133
4181
|
return join([
|
|
4134
4182
|
`⚠️ Review round ${e.round.index} ended without a submitted verdict (budget, refusal, or stop) — ship never converts that into a request for changes, so no findings step ran.`,
|
|
4135
4183
|
writeUpPointer(s, e.round.kind, s.reviewRunByRound[e.round.index]),
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Permissions for the trusted Git door's receive-pack operation. GitHub
|
|
2
|
+
* also requires Workflows when a pushed commit edits .github/workflows. */
|
|
3
|
+
export const DOOR_PUSH_PERMISSIONS = {
|
|
4
|
+
contents: "write",
|
|
5
|
+
workflows: "write",
|
|
6
|
+
metadata: "read",
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
/** The resident's root-owned token only clones/fetches its mirror and reads
|
|
10
|
+
* pull request state for worktree reclamation. */
|
|
11
|
+
export const RESIDENT_MIRROR_PERMISSIONS = {
|
|
12
|
+
contents: "read",
|
|
13
|
+
pull_requests: "read",
|
|
14
|
+
metadata: "read",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
type Permission = "read" | "write";
|
|
18
|
+
|
|
19
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
20
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Check GitHub's effective scope before a token enters either cache. GitHub
|
|
24
|
+
* may omit its implicit Metadata read grant from the response. Errors contain
|
|
25
|
+
* only a fixed reason; they never serialize a response or bearer. */
|
|
26
|
+
export function verifyGithubMintScope(
|
|
27
|
+
response: unknown,
|
|
28
|
+
repo: string,
|
|
29
|
+
requested: Readonly<Record<string, Permission>>,
|
|
30
|
+
): void {
|
|
31
|
+
if (!record(response) || !record(response.permissions)) throw new Error("GitHub token scope mismatch");
|
|
32
|
+
const actual = response.permissions;
|
|
33
|
+
if (
|
|
34
|
+
Object.entries(actual).some(([name, value]) => requested[name] !== value) ||
|
|
35
|
+
Object.keys(requested).some((name) => !(name in actual) && !(name === "metadata" && requested[name] === "read"))
|
|
36
|
+
) {
|
|
37
|
+
throw new Error("GitHub token scope mismatch");
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
response.repository_selection !== "selected" ||
|
|
41
|
+
!Array.isArray(response.repositories) ||
|
|
42
|
+
response.repositories.length !== 1 ||
|
|
43
|
+
!record(response.repositories[0]) ||
|
|
44
|
+
typeof response.repositories[0].full_name !== "string" ||
|
|
45
|
+
response.repositories[0].full_name.toLowerCase() !== repo.toLowerCase()
|
|
46
|
+
) {
|
|
47
|
+
throw new Error("GitHub token scope mismatch");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,45 +1,17 @@
|
|
|
1
|
-
/** The
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* executor before every `/exec` it sends
|
|
8
|
-
* (src/execution/sandboxCredentials.ts, issue 1915).
|
|
9
|
-
*
|
|
10
|
-
* Background: attach writes a 1-hour GitHub App installation token
|
|
11
|
-
* into `<worktree>/.git/github-credentials` and points git's `store` helper
|
|
12
|
-
* at it. Nothing used to refresh it during a run, so a push more than ~60
|
|
13
|
-
* minutes after attach got 401 — and git's `store` helper ERASES a rejected
|
|
14
|
-
* credential from its file, leaving the agent staring at an empty file. The
|
|
15
|
-
* Worker now asks this function before every writable `/exec` and re-mints
|
|
16
|
-
* (cached per slug) + rewrites the file when it says so.
|
|
17
|
-
*
|
|
18
|
-
* The refresh is driven by the TOKEN'S OWN EXPIRY, not the file's age.
|
|
19
|
-
* `mintRepoScopedToken` caches per repository slug, so a token minted for an
|
|
20
|
-
* earlier thread — with only a few minutes of life left — could be written
|
|
21
|
-
* for a brand-new attach and, keyed on file age alone, read "fresh" for ~45
|
|
22
|
-
* minutes while every writable exec in between ran on a dead token (the first
|
|
23
|
-
* git write 401s, `store` blanks the file, and only the NEXT exec refreshed).
|
|
24
|
-
* Now the binding records the token's `expiresAtMs`, and this predicate
|
|
25
|
-
* refreshes once the token is within `CREDENTIAL_EXPIRY_MARGIN_MS` of expiry
|
|
26
|
-
* — so a writable exec never starts on a token that cannot outlive it. */
|
|
1
|
+
/** The pure expiry decision retained for the optional legacy sandbox
|
|
2
|
+
* credential refresher. The production executor factory does not supply
|
|
3
|
+
* that credential source: model workspaces use a revocable Git-door run
|
|
4
|
+
* bearer, and the resident never refreshes a model-visible App token.
|
|
5
|
+
* The resident imports only the expiry margin for its root-owned mirror
|
|
6
|
+
* credential cache. */
|
|
27
7
|
|
|
28
8
|
import { BASH_TIMEOUT_MAX_MS } from "./bashTimeout.js";
|
|
29
9
|
|
|
30
|
-
/**
|
|
31
|
-
* this long after the file was written when the token's real expiry is
|
|
32
|
-
* unknown — comfortably before the 60-minute token expiry. New bindings carry
|
|
33
|
-
* the expiry and take the margin path below instead. */
|
|
10
|
+
/** Legacy refresher backstop when the stored token's expiry is unknown. */
|
|
34
11
|
export const CREDENTIAL_REFRESH_AFTER_MS = 45 * 60_000;
|
|
35
12
|
|
|
36
|
-
/**
|
|
37
|
-
*
|
|
38
|
-
* Sized like the sandbox's per-command credential margin: the longest
|
|
39
|
-
* single exec is `BASH_TIMEOUT_MAX_MS` (20 min), plus slack for the mint +
|
|
40
|
-
* file write and clock skew ⇒ 25 min. This is also `mintRepoScopedToken`'s
|
|
41
|
-
* cache serve threshold, so the cache and this predicate agree: a token the
|
|
42
|
-
* cache would still hand out is a token this predicate would not refresh. */
|
|
13
|
+
/** Legacy refresh and root-mirror cache margin: the longest exec is
|
|
14
|
+
* `BASH_TIMEOUT_MAX_MS` (20 min), plus five minutes for mint and clock skew. */
|
|
43
15
|
export const CREDENTIAL_EXPIRY_MARGIN_MS = BASH_TIMEOUT_MAX_MS + 5 * 60_000;
|
|
44
16
|
|
|
45
17
|
export type CredentialRefreshReason =
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Persist uncertainty before asking the platform to destroy a resident VM.
|
|
2
|
+
* A rejected or interrupted destroy leaves the fence in storage: callers may
|
|
3
|
+
* retry destruction, but may not provision or assign a pool user on the old
|
|
4
|
+
* disk. A failed clear is conservative too, even if the VM was destroyed. */
|
|
5
|
+
export async function destroyWithPersistentFence(
|
|
6
|
+
fence: { mark(): Promise<void>; clear(): Promise<void> },
|
|
7
|
+
destroy: () => Promise<void>,
|
|
8
|
+
): Promise<void> {
|
|
9
|
+
await fence.mark();
|
|
10
|
+
await destroy();
|
|
11
|
+
await fence.clear();
|
|
12
|
+
}
|