@coreplane/switchboard 1.241.0 → 1.242.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 +25 -12
- package/dist/assets/deploy/cloudflare-resident/worker.ts +197 -15
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +38 -5
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/project.json +2 -2
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +31 -14
- package/dist/assets/src/core/authz/actor.ts +12 -9
- package/dist/assets/src/core/authz/authorize.ts +17 -4
- package/dist/assets/src/core/authz/types.ts +12 -0
- package/dist/assets/src/core/coordinator/contract.ts +3 -1
- package/dist/assets/src/core/costs.ts +12 -4
- package/dist/assets/src/core/harness/scope.ts +21 -0
- package/dist/assets/src/core/runEvents.ts +20 -1
- package/dist/assets/src/core/runLedger/types.ts +2 -0
- package/dist/assets/src/core/runRecord.ts +4 -0
- package/dist/assets/src/core/ship/contract.ts +4 -2
- package/dist/assets/src/execution/residentAutoRebuild.ts +4 -2
- package/dist/assets/src/execution/residentHead.ts +23 -10
- package/dist/assets/src/execution/residentInfraStreak.ts +89 -0
- package/dist/assets/src/execution/residentRefresh.ts +15 -1
- package/dist/assets/src/execution/residentSteps.ts +1 -0
- package/dist/assets/src/execution/sandboxErrors.ts +6 -0
- package/dist/assets/src/execution/seedPlan.ts +17 -0
- package/dist/assets/web/dist/.vite/manifest.json +419 -425
- package/dist/assets/web/dist/assets/{AppShell-DEy5jRuS.js → AppShell-IDMGG6yi.js} +1 -1
- package/dist/assets/web/dist/assets/{CostsPage-BuHD-Bv7.js → CostsPage-CtmKhhOF.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-BGpPr5xr.js → DeliveryPage-n1tz5I_v.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-QtH1EwYF.js +2 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-DtmQM2gD.js → NotFoundPage-C97EhJcQ.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-CrEAWx5b.js → PendingTurnRow-BZA_vQt3.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-C-48fxat.js → ResidentDetailPage-DTMBgnIW.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-CBBIjPOa.js → ResidentsIndexPage-CaDvXhzJ.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-mgyLW0oV.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-DypJYMQa.js +6 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DYraPoWD.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-CtNGNnhN.js → RunsTabs-BUfdk0lH.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-Ci1Ygqnc.js → ScheduledPage-DXD2gLJk.js} +1 -1
- package/dist/assets/web/dist/assets/SettingsPage-BIGio8Y0.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-DPWE1JmO.js → StatusDot-ELoXHlFt.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DKhSRH9t.js → Tooltip-BoeFwYP2.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-jhCrWW3i.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-DgSK1qvr.js → angular-html-oBNfPJR0.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-D3gNdiSG.js → angular-ts-BvNwsyWA.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-CkHCYM8I.js → apl-CNUdRlYf.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DLm45axt.js → astro-Zb0NriSe.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-BKa-VE-c.js → blade-qPRVheqq.js} +1 -1
- package/dist/assets/web/dist/assets/{c-C8NCNWai.js → c-D8Awx4YO.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-DqDQ7IKH.js → chapel-Bt72Mhsx.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-j4vCD6AJ.js → cobol-BOBacexg.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-C6oDVH-0.js → coffee-E4u0liHW.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-Bd3A1Rtc.js → cpp-q2sLNlul.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CUOgOw0d.js → crystal-DyWqUnlb.js} +1 -1
- package/dist/assets/web/dist/assets/{css-RYljyv7G.js → css-CQY0hFsD.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BnJXKHCY.js → dist-BU5UivXC.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-BobbycC-.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BJhNdQDc.js → edge-C1MwhJkX.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-g1AWOAfT.js → elixir-Bb3YbHfn.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-CER5e4Dz.js → elm-DAN9IGQw.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-D5aOmMQf.js → erb-BEB8Xlsj.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-D6rfV8jp.js → git-rebase-tqpjRfxO.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-BvDH3-mC.js → glimmer-js-Ccbo65zR.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-DChtNs9V.js → glimmer-ts-B6WBVMpE.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-BHlsWrxf.js → glsl-tRec3Fcu.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-BsaBHb53.js → graphql-P8kbxT4F.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-0lbDosCd.js → hack-H9Zhkagy.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-BKtco2vr.js → haml-DtEnpn7Z.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-BE_cj04z.js → handlebars-DkgPfoAz.js} +1 -1
- package/dist/assets/web/dist/assets/{html-BNcr8EVd.js → html-D30RXpIs.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-BsJt2Kei.js → html-derivative-DwozLrEx.js} +1 -1
- package/dist/assets/web/dist/assets/{http-BR5P8ER_.js → http-DXuzBAPm.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-CtHalpWS.js → hurl-d1UUJIt_.js} +1 -1
- package/dist/assets/web/dist/assets/{java-BXVdhN11.js → java-DL0gWf34.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-CyoShgNQ.js → javascript-Cl7vavnS.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-BQYZzHex.js → jinja-CfQOWMX9.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DqrUa1Eq.js → jison-KdYirlqm.js} +1 -1
- package/dist/assets/web/dist/assets/{json-CQiTF9Jj.js → json-C9cDQ-Qj.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-Y16MkCYQ.js → jsx-CRx5NItd.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-zn16YONe.js → julia-CmsQsZQl.js} +1 -1
- package/dist/assets/web/dist/assets/{just-DcVeN_MN.js → just-CsM3Q8TE.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BNNoF-WP.js → latex-BXCh5YRX.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-DAi6qHzn.js → liquid-BF2vwK8p.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-BswS0axw.js → lua-DcMBATrl.js} +1 -1
- package/dist/assets/web/dist/assets/main-B2fX10aW.css +1 -0
- package/dist/assets/web/dist/assets/{main-B3is4sk6.js → main-D4EA1g6n.js} +2 -2
- package/dist/assets/web/dist/assets/{marko-C-4hkmQZ.js → marko-88MndKvG.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-0ZaHUkS7.js → mdc-DJ4kVAd8.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-DBDg5tOR.js → nginx-CP6mRgtV.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BVMz49Yw.js → nim-QQfj3fpF.js} +1 -1
- package/dist/assets/web/dist/assets/{org-1hO_K6ya.js → org-Bke3eBzc.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-CVRQLvVk.js → perl-Bkh0N7Kz.js} +1 -1
- package/dist/assets/web/dist/assets/{php-K9nCrB9n.js → php-Dnv1Piya.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-CTxOcmO3.js → pug-D6peFFI7.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-CmCkfG5q.js → qml-D8PEs-C-.js} +1 -1
- package/dist/assets/web/dist/assets/{r-pP47Xn1X.js → r-B1EL9b_j.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BpW6r3tC.js → razor-BsMTIh2b.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CINgdY4N.js → regexp-HhvC8spD.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-BZIUEq2Q.js → rst-D5paAxpg.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-nzAOxz6r.js → ruby-BrQwhLrl.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-QIS1bFth.js → sas-afot2B1o.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-BuhBBVNt.js → scss-Efm-mwuG.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-CQc1vXbk.js → shellscript-BK0Vv5fT.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CoubCAUv.js → shellsession-BGqlMC7N.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-CPRzlder.js → soy-M0b4UwGM.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-W9krb8-9.js → sql-sxe6IE9j.js} +1 -1
- package/dist/assets/web/dist/assets/sseReplay-g7ml86LM.js +9 -0
- package/dist/assets/web/dist/assets/{stata-CuISJEC0.js → stata-nPF_ddLP.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CSet7584.js → surrealql-D_GrC6u7.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-ChXTSYwK.js → svelte-DwL1AtNP.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-Bm55v62k.js → templ-7s7LTDkc.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-CICMX8Gj.js → tex-Bx-5fMxe.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-CA1UzWyB.js → ts-tags-DUMJnke_.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-Dy04HNbv.js → tsx-BN8biPDe.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-BBHsVnVD.js → twig-8nIu84TN.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-Dvc-wVBT.js → typescript-BooSPq_S.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-Dvlqx3_q.js → typst-CTBiBsem.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-zWi1MNU2.js → vue-C6Ft4Lea.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-D4YxT3An.js → vue-html-Ba36dD5D.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-0YE7uQlH.js → vue-vine-NkFexVo2.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-gm-iksZB.js → xml-C_THnHXZ.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-D_W5BcrY.js → xsl-D8G5xqjY.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-CHmZ21wZ.js → yaml-jAMIJzge.js} +1 -1
- package/dist/cli.js +1390 -484
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/HomePage-dKBvWh-E.js +0 -2
- package/dist/assets/web/dist/assets/RunFoldRow-B6vxjcBI.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-CQjd3Cz-.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-Cy9G6D0p.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-DxmJvAmT.js +0 -1
- package/dist/assets/web/dist/assets/SlackMark-VDNs7Vjh.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DEN5bSQf.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-DXG-3R7_.js +0 -1
- package/dist/assets/web/dist/assets/main-DP_zSemY.css +0 -1
- package/dist/assets/web/dist/assets/sseReplay-yji8a1wM.js +0 -9
|
@@ -223,6 +223,14 @@ export const FENCED_CONTENT_RULE =
|
|
|
223
223
|
const NOTEPAD = `YOUR NOTES AND YOUR REACH BACK. This thread's conversation outlives your context window and this run: every turn — yours, the person's, every tool call and its output, from this run and the runs before it in this thread — is kept in a log you can search with the \`recall\` tool (words → the matching turns with their numbers; a turn number → that turn whole). When something you need is no longer in front of you, recall it instead of redoing the work or guessing.
|
|
224
224
|
Keep notes with the \`notes\` tool: one short document, replaced whole each time, at most 8 KiB — decisions and their reasons, the names of things you found (files, tests, commits, the head your tests were green at), what is not yet proven. They are the one thing sure to survive a compaction and to reach the next run in this thread: they ride your system prompt at its start and come back to you right after a compaction. A person reads them too, on the run's page, so write them as a document and never as one paragraph: Markdown, a \`##\` heading per section — \`Done\`, \`In progress\`, \`Next\`, \`Facts\` (names, ids, heads, the reasons behind decisions), leaving out a section with nothing in it — one bullet per item, one line per bullet, no prose walls. Write them when you decide something worth keeping, not only at the end.`;
|
|
225
225
|
|
|
226
|
+
// Every coding prompt carries this verbatim (docs/reference/specs/agent-coding.md
|
|
227
|
+
// item 13): the order of checks and the push. Three plan children died at their
|
|
228
|
+
// budget in one evening with finished work unpushed because each ran the
|
|
229
|
+
// project's most expensive checks first; the rule is the runner's to hold, not
|
|
230
|
+
// a line every requester remembers to paste. Stack-agnostic on purpose — the
|
|
231
|
+
// classes are by duration, the project's own scripts and CI say which is which.
|
|
232
|
+
export const CHECKS_BY_COST = `CHECKS BY COST — push before the expensive ones. Every check you might run has a cost class: seconds (a formatter or a linter on the files you touched, one test file, a docs, link or spec check, the typecheck of one package) or minutes (the whole test suite, a build, a dependency install, an end-to-end or full verification). Know a command's class before you run it — from the project's own scripts and CI configuration, from how long it took last time, or by the class above when you have nothing better. Prove each change with the cheapest check that can prove it, matched to the change's scope: a documentation change gets the documentation checks, one module gets its own tests, a shared type gets the typecheck. As soon as the change exists and those checks pass, commit and push — the pushed branch is the deliverable, and an unpushed tree does not survive the run's end. Only then run the expensive checks, once, and fix forward with further commits and pushes. Never start an operation whose expected duration does not fit the time you have left minus what a commit, a push and the description need: push what there is and say plainly what is unverified instead. The description's validation names exactly what ran; what did not run is CI's to gate, and you say so.`;
|
|
233
|
+
|
|
226
234
|
const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
|
|
227
235
|
|
|
228
236
|
You work inside a dedicated workspace directory with bash, read_file, and write_file tools. ${SANDBOX_TOOLCHAIN}
|
|
@@ -237,10 +245,13 @@ Workflow for shipping a PR:
|
|
|
237
245
|
1. Clone the repo into the workspace if it's not already there (use gh or git; both are authenticated on this host). Orient with a few BATCHED commands (tree + the relevant files in one call), not file-by-file exploration.
|
|
238
246
|
2. Create a branch with a descriptive name.
|
|
239
247
|
3. Implement the change. Match the surrounding code's style and conventions.
|
|
240
|
-
4.
|
|
241
|
-
5. Commit with a clear message and push the branch.
|
|
242
|
-
6.
|
|
243
|
-
7.
|
|
248
|
+
4. Prove the change with the cheapest checks that can (CHECKS BY COST below): the linter and the tests nearest the files you touched, the documentation checks for a documentation change.
|
|
249
|
+
5. Commit with a clear message and push the branch — before any full suite, build or full verification.
|
|
250
|
+
6. Then, if the budget allows, run the project's expensive checks once and fix forward with further commits and pushes.
|
|
251
|
+
7. Call the submit_pr_description tool with the typed description object (content contract below) — every time, bringing forward the context you gained while implementing. Switchboard renders the PR body from your object at the pushed head and opens (or updates) the pull request itself: do NOT open a PR yourself, with \`gh\` or any API call.
|
|
252
|
+
8. Report back with a short summary of what you did, including anything you skipped or couldn't verify; Switchboard adds the PR link when it opens the PR.
|
|
253
|
+
|
|
254
|
+
${CHECKS_BY_COST}
|
|
244
255
|
|
|
245
256
|
${NEVER_MERGE}
|
|
246
257
|
|
|
@@ -279,11 +290,14 @@ Environment notes:
|
|
|
279
290
|
Workflow for shipping a change:
|
|
280
291
|
1. Create a branch with a descriptive name off the bound branch.
|
|
281
292
|
2. Implement the change. Match the surrounding code's style and conventions.
|
|
282
|
-
3.
|
|
283
|
-
4. Commit with a clear message and push the branch with \`git push -u origin <branch
|
|
284
|
-
5.
|
|
285
|
-
6. Call the
|
|
286
|
-
7.
|
|
293
|
+
3. Prove the change with the cheapest checks that can (CHECKS BY COST below): the linter and the tests nearest the files you touched, the documentation checks for a documentation change (dependencies are already present).
|
|
294
|
+
4. Commit with a clear message and push the branch with \`git push -u origin <branch>\` — before any full suite, build or full verification.
|
|
295
|
+
5. Then, if the budget allows, run the project's expensive checks once and fix forward with further commits and pushes.
|
|
296
|
+
6. Call the \`diff_digest\` tool to get a distilled summary of your change — per-file churn, totals, and risky-file flags. It is a distilled summary, not the raw diff: use it to shape the description you submit next — which files the Tour must walk, what belongs in risks.
|
|
297
|
+
7. Call the submit_pr_description tool with the typed description object (content contract below) — every time. Switchboard renders the PR body from your object at the pushed head and opens (or updates) the pull request itself: do NOT open a PR yourself, with any API call.
|
|
298
|
+
8. Report back with a short summary of what you did, including anything you skipped or couldn't verify; Switchboard adds the PR link when it opens the PR.
|
|
299
|
+
|
|
300
|
+
${CHECKS_BY_COST}
|
|
287
301
|
|
|
288
302
|
${NEVER_MERGE}
|
|
289
303
|
|
|
@@ -318,11 +332,14 @@ THE REPOSITORY IS ALREADY CLONED at \`/workspace/checkout\` — seeded from the
|
|
|
318
332
|
Workflow for shipping a change:
|
|
319
333
|
1. Create a branch with a descriptive name off the current branch.
|
|
320
334
|
2. Implement the change. Match the surrounding code's style and conventions.
|
|
321
|
-
3.
|
|
322
|
-
4. Commit with a clear message and push the branch with \`git push -u origin <branch
|
|
323
|
-
5.
|
|
324
|
-
6. Call the
|
|
325
|
-
7.
|
|
335
|
+
3. Prove the change with the cheapest checks that can (CHECKS BY COST below): the linter and the tests nearest the files you touched, the documentation checks for a documentation change (dependencies are already present).
|
|
336
|
+
4. Commit with a clear message and push the branch with \`git push -u origin <branch>\` — before any full suite, build or full verification.
|
|
337
|
+
5. Then, if the budget allows, run the project's expensive checks once and fix forward with further commits and pushes.
|
|
338
|
+
6. Call the \`diff_digest\` tool to get a distilled summary of your change — per-file churn, totals, and risky-file flags. It is a distilled summary, not the raw diff: use it to shape the description you submit next — which files the Tour must walk, what belongs in risks.
|
|
339
|
+
7. Call the submit_pr_description tool with the typed description object (content contract below) — every time. Switchboard renders the PR body from your object at the pushed head and opens (or updates) the pull request itself: do NOT open a PR yourself, with \`gh\` or any API call.
|
|
340
|
+
8. Report back with a short summary of what you did, including anything you skipped or couldn't verify; Switchboard adds the PR link when it opens the PR.
|
|
341
|
+
|
|
342
|
+
${CHECKS_BY_COST}
|
|
326
343
|
|
|
327
344
|
${NEVER_MERGE}
|
|
328
345
|
|
|
@@ -83,15 +83,18 @@ interface BoundFields {
|
|
|
83
83
|
userName?: string;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/** The actor
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
export function
|
|
94
|
-
|
|
86
|
+
/** The actor every gate in the dispatch path decides on: `resolveChatActor`
|
|
87
|
+
* over the message, with config's grants lookup. Every `canRunAgent` /
|
|
88
|
+
* `canUseRepo` / `canManageRepos` / `canEditChannelConfig` question asks
|
|
89
|
+
* about THIS actor, never `msg.userId`: a relayed message (`postedBy`,
|
|
90
|
+
* authorization.md item 14) decides on the app ∩ the person, a bound
|
|
91
|
+
* credential (`authenticatedAs`, item 15) on the credential alone — naming
|
|
92
|
+
* a person on a run never lends the run the person's grants. */
|
|
93
|
+
export function chatActorOf(
|
|
94
|
+
config: { grantsFor: GrantsLookup },
|
|
95
|
+
msg: { userId: string; channelId: string; threadKey: string; postedBy?: string } & BoundFields,
|
|
96
|
+
): Actor {
|
|
97
|
+
return resolveChatActor(msg, (id) => config.grantsFor(id));
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
const CHAT_SURFACES: Readonly<Record<string, ActorSurface>> = {
|
|
@@ -63,6 +63,14 @@ export function principalOf(actor: Actor): Actor {
|
|
|
63
63
|
return current;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** The channels the decision's person is in: the root principal's
|
|
67
|
+
* `memberOf`, as `selfIdsOf` reads its `self` — a fact from the channel
|
|
68
|
+
* directory, never a grant; absent → the empty set. */
|
|
69
|
+
export function memberChannelsOf(actor: Actor): ReadonlySet<string> {
|
|
70
|
+
return principalOf(actor).memberOf ?? EMPTY_SET;
|
|
71
|
+
}
|
|
72
|
+
const EMPTY_SET: ReadonlySet<string> = new Set();
|
|
73
|
+
|
|
66
74
|
/** The ids that mean "me" for a decision (record 0042): the root principal's
|
|
67
75
|
* `self` when it carries one — its own id and the person a dashboard session
|
|
68
76
|
* is linked to — else its id alone. Never read by a grant check. */
|
|
@@ -117,6 +125,7 @@ export function evaluateCondition(
|
|
|
117
125
|
grants: Grants,
|
|
118
126
|
selfIds: readonly string[],
|
|
119
127
|
attributes: ResourceAttributes,
|
|
128
|
+
memberOf: ReadonlySet<string> = EMPTY_SET,
|
|
120
129
|
): boolean {
|
|
121
130
|
switch (condition.kind) {
|
|
122
131
|
case "has-grant": {
|
|
@@ -124,11 +133,13 @@ export function evaluateCondition(
|
|
|
124
133
|
return grant !== undefined && hasAction(grants.actions, grant);
|
|
125
134
|
}
|
|
126
135
|
case "member-of":
|
|
127
|
-
// Granted the channel,
|
|
136
|
+
// Granted the channel, in the channel (the directory's fact on the
|
|
137
|
+
// actor), or the channel is public (a run's stamped
|
|
128
138
|
// visibility). `unknown` — no stamp, a directory failure — is never
|
|
129
139
|
// public: fail-closed.
|
|
130
140
|
return (
|
|
131
|
-
(attributes.channelId !== undefined &&
|
|
141
|
+
(attributes.channelId !== undefined &&
|
|
142
|
+
(holds(grants.channels, attributes.channelId) || memberOf.has(attributes.channelId))) ||
|
|
132
143
|
attributes.channelVisibility === "public"
|
|
133
144
|
);
|
|
134
145
|
case "is-self":
|
|
@@ -154,7 +165,8 @@ export function evaluateRule(rule: Rule, actor: Actor, resource: Resource): bool
|
|
|
154
165
|
if (rule.originVisibility && !rule.originVisibility.includes(attributes.visibility)) return false;
|
|
155
166
|
const grants = effectiveGrants(actor);
|
|
156
167
|
const selfIds = selfIdsOf(actor);
|
|
157
|
-
|
|
168
|
+
const memberOf = memberChannelsOf(actor);
|
|
169
|
+
return rule.when.every((condition) => evaluateCondition(condition, grants, selfIds, attributes, memberOf));
|
|
158
170
|
}
|
|
159
171
|
|
|
160
172
|
/** `authorize` over an explicit (validated) table. Tests use it to drive
|
|
@@ -173,9 +185,10 @@ export function authorizeWith(rules: readonly Rule[], actor: Actor, action: Acti
|
|
|
173
185
|
if (forOrigin.length === 0) return deny("origin-visibility");
|
|
174
186
|
const grants = effectiveGrants(actor);
|
|
175
187
|
const selfIds = selfIdsOf(actor);
|
|
188
|
+
const memberOf = memberChannelsOf(actor);
|
|
176
189
|
let reason: DenyReason | undefined;
|
|
177
190
|
for (const rule of forOrigin) {
|
|
178
|
-
const failed = rule.when.find((condition) => !evaluateCondition(condition, grants, selfIds, attributes));
|
|
191
|
+
const failed = rule.when.find((condition) => !evaluateCondition(condition, grants, selfIds, attributes, memberOf));
|
|
179
192
|
if (!failed) return ALLOW;
|
|
180
193
|
reason ??= FAILURE_REASON[failed.kind];
|
|
181
194
|
}
|
|
@@ -57,6 +57,15 @@ export interface Actor {
|
|
|
57
57
|
readonly self?: readonly string[];
|
|
58
58
|
/** The linked person, for display and the audit line (`asUser`); absent when unlinked. */
|
|
59
59
|
readonly asUser?: { readonly id: string; readonly name?: string };
|
|
60
|
+
/**
|
|
61
|
+
* The channels the platform says the actor's person is in: the
|
|
62
|
+
* channel directory's `channelsOf`, resolved once with the actor — a fact
|
|
63
|
+
* about the person, never a grant from config. `member-of` reads it beside
|
|
64
|
+
* `grants.channels`, so a private channel's runs and config open to the
|
|
65
|
+
* people in it. Absent (no directory, a lookup failure, an unlinked session)
|
|
66
|
+
* → nothing: fail-closed, exactly today's behaviour.
|
|
67
|
+
*/
|
|
68
|
+
readonly memberOf?: ReadonlySet<string>;
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
/** `<group>:<read|write|exec>` plus the non-command actions. A plain
|
|
@@ -184,4 +193,7 @@ export type Predicate =
|
|
|
184
193
|
export interface ChannelDirectory {
|
|
185
194
|
info(channelId: string): Promise<{ visibility: ChannelVisibility }>;
|
|
186
195
|
isMember(actorId: string, channelId: string): Promise<boolean | "unknown">;
|
|
196
|
+
/** Every channel the actor is in, platform-namespaced — what the resolver puts on
|
|
197
|
+
* `Actor.memberOf`; `unknown` when the adapter cannot say (fail-closed). */
|
|
198
|
+
channelsOf(actorId: string): Promise<ReadonlySet<string> | "unknown">;
|
|
187
199
|
}
|
|
@@ -150,6 +150,8 @@ export interface CoordinatorInstance {
|
|
|
150
150
|
/** The bound credential behind the person (authorization.md item 15), when
|
|
151
151
|
* there was one: every child is authorized under ITS grants, as the request was. */
|
|
152
152
|
authenticatedAs?: string;
|
|
153
|
+
/** The app that relayed the request for the person (authorization.md item 14), when one did: every child is authorized under app ∩ person, as the request was. */
|
|
154
|
+
postedBy?: string;
|
|
153
155
|
channelId: string;
|
|
154
156
|
channelName?: string;
|
|
155
157
|
/** The requesting thread: where the card lives and where a generated plan's
|
|
@@ -260,7 +262,7 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
|
|
|
260
262
|
if (r.kind !== "ship") return false;
|
|
261
263
|
if (!isText(r.userId) || !isText(r.channelId) || !isText(r.threadKey)) return false;
|
|
262
264
|
if (!isOptionalText(r.userName) || !isOptionalText(r.channelName) || !isOptionalText(r.sourceUrl)) return false;
|
|
263
|
-
if (!isOptionalText(r.authenticatedAs)) return false;
|
|
265
|
+
if (!isOptionalText(r.authenticatedAs) || !isOptionalText(r.postedBy)) return false;
|
|
264
266
|
if (typeof r.repo !== "string" || !REPO_SLUG.test(r.repo)) return false;
|
|
265
267
|
if (!isText(r.branch) || !isOptionalText(r.base)) return false;
|
|
266
268
|
if (!isFinite(r.createdAt)) return false;
|
|
@@ -72,6 +72,9 @@ export interface CostsConfig {
|
|
|
72
72
|
snapshot: {
|
|
73
73
|
/** Hours between two reads of the billing sources. */
|
|
74
74
|
everyHours: number;
|
|
75
|
+
/** The platform-namespaced channel (`slack:C…`) told when takes keep failing and when they
|
|
76
|
+
* land again (`ALERT_AFTER_FAILURES` in a row); absent → the status alone says so. */
|
|
77
|
+
alertChannel?: string;
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -82,13 +85,15 @@ const DEFAULT_ANTHROPIC_ADMIN_ENV = "ANTHROPIC_ADMIN_KEY";
|
|
|
82
85
|
* default — both sources bucket by UTC day, and the page is read about as often. */
|
|
83
86
|
export const COSTS_SNAPSHOT_EVERY_HOURS = Object.freeze({ default: 24, min: 1, max: 168 });
|
|
84
87
|
|
|
85
|
-
/** `costs.snapshot`: absent → the default interval; a value outside the
|
|
88
|
+
/** `costs.snapshot`: absent → the default interval and no alert channel; a value outside the
|
|
89
|
+
* bounds or not a whole number throws by name; `alertChannel`, when given, is a platform-namespaced id. */
|
|
86
90
|
function snapshotConfig(raw: unknown): CostsConfig["snapshot"] {
|
|
87
91
|
if (raw === undefined) return { everyHours: COSTS_SNAPSHOT_EVERY_HOURS.default };
|
|
88
92
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
89
93
|
throw new Error("costs.snapshot must be a mapping");
|
|
90
|
-
const
|
|
91
|
-
|
|
94
|
+
const r = raw as Record<string, unknown>;
|
|
95
|
+
// `undefined` alone defaults: a bare `everyHours:` key (null) is a malformed value, refused below by name.
|
|
96
|
+
const every = r.everyHours === undefined ? COSTS_SNAPSHOT_EVERY_HOURS.default : r.everyHours;
|
|
92
97
|
if (
|
|
93
98
|
typeof every !== "number" ||
|
|
94
99
|
!Number.isInteger(every) ||
|
|
@@ -98,7 +103,10 @@ function snapshotConfig(raw: unknown): CostsConfig["snapshot"] {
|
|
|
98
103
|
throw new Error(
|
|
99
104
|
`costs.snapshot.everyHours must be a whole number of hours between ${COSTS_SNAPSHOT_EVERY_HOURS.min} and ${COSTS_SNAPSHOT_EVERY_HOURS.max}`,
|
|
100
105
|
);
|
|
101
|
-
return { everyHours: every };
|
|
106
|
+
if (r.alertChannel === undefined) return { everyHours: every };
|
|
107
|
+
if (typeof r.alertChannel !== "string" || !/^[a-z]+:.+$/.test(r.alertChannel))
|
|
108
|
+
throw new Error("costs.snapshot.alertChannel must be a platform-namespaced channel id (`slack:C…`)");
|
|
109
|
+
return { everyHours: every, alertChannel: r.alertChannel };
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
function labelMap(raw: unknown, what: string): Record<string, string> {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Where a preset's harness word is set (docs/reference/specs/harness.md item
|
|
2
|
+
// 8), in a module with no imports of its own: the run record (`run_meta.harnessScope`)
|
|
3
|
+
// and the timeline fold read it, and both are compiled into the Workers' and
|
|
4
|
+
// the dashboard's programs, where the roster's neighbours — the harness
|
|
5
|
+
// objects, their Node-only process code — must not follow. The roster
|
|
6
|
+
// re-exports these names, so every reader inside the bot can still take them
|
|
7
|
+
// from there.
|
|
8
|
+
|
|
9
|
+
/** The scopes a word is set at, most specific first — the order the resolution
|
|
10
|
+
* walks the layers: the requester's own scope, the channel's, then the
|
|
11
|
+
* deployment's top-level `harness` block, which is the defaults layer under
|
|
12
|
+
* its one spelling. What `run_meta.harnessScope` and the config block name, so
|
|
13
|
+
* a reader of a run on OpenCode never guesses whose word put it there. */
|
|
14
|
+
export const HARNESS_SCOPES = ["user", "channel", "defaults"] as const;
|
|
15
|
+
export type HarnessScope = (typeof HARNESS_SCOPES)[number];
|
|
16
|
+
|
|
17
|
+
/** Whether a value is one of the scopes a word is set at: the timeline's test
|
|
18
|
+
* for the field a record carries. */
|
|
19
|
+
export function isHarnessScope(value: unknown): value is HarnessScope {
|
|
20
|
+
return typeof value === "string" && (HARNESS_SCOPES as readonly string[]).includes(value);
|
|
21
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// the node-free contract the memory Worker and web app compile with their own
|
|
3
3
|
// tsconfigs — importing prDescription.ts would drag zod into those graphs.
|
|
4
4
|
import type { PrDescription, RenderedTourStep } from "./prDescriptionTypes.js";
|
|
5
|
+
import type { HarnessScope } from "./harness/scope.js";
|
|
5
6
|
|
|
6
7
|
/** The `pr_description` review artifact minus the event envelope
|
|
7
8
|
* (docs/reference/specs/reading-diff.md item 7). */
|
|
@@ -97,6 +98,11 @@ export type RunNoteKind =
|
|
|
97
98
|
/** The PR head moved while a review ran and the same run is re-reviewing at
|
|
98
99
|
* the new head (agent-review.md item 12). Published by the dispatcher. */
|
|
99
100
|
| "head_moved"
|
|
101
|
+
/** The run loop threw and the run finishes `failed`: the summary is the
|
|
102
|
+
* error's message, redacted and capped, so the run page says why a failed
|
|
103
|
+
* run failed even when the reply is never delivered (run-history.md).
|
|
104
|
+
* Published by the run loop's catch, before the finish. */
|
|
105
|
+
| "run_failed"
|
|
100
106
|
/** An MCP server configured for this agent did not answer discovery
|
|
101
107
|
* (docs/reference/specs/mcp-tools.md item 8); the run proceeds without its tools. One
|
|
102
108
|
* note per server, published by the dispatcher before the first turn. */
|
|
@@ -187,6 +193,11 @@ export type RunNoteKind =
|
|
|
187
193
|
* item 7): the summary names the tool and the rule; the model read the same
|
|
188
194
|
* reason as the tool's result. Published by the bot's authorize route. */
|
|
189
195
|
| "tool_refused"
|
|
196
|
+
/** A ship coding child's budget ended with work still in the tree: the run
|
|
197
|
+
* loop committed and pushed it to the unit's branch (or says plainly that
|
|
198
|
+
* there was nothing to push), so a re-issue starts from the partial work
|
|
199
|
+
* (docs/reference/specs/agent-ship.md item 8). */
|
|
200
|
+
| "budget_salvage"
|
|
190
201
|
/** The native loop's stuck-loop guard: the same tool call failed identically
|
|
191
202
|
* six times in a row and the run was forced into its write-up. Written by
|
|
192
203
|
* no loop since that loop's deletion; a record from before it may carry it. */
|
|
@@ -205,6 +216,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
205
216
|
"stopped",
|
|
206
217
|
"spans_dropped",
|
|
207
218
|
"head_moved",
|
|
219
|
+
"run_failed",
|
|
208
220
|
"mcp_unavailable",
|
|
209
221
|
"follow_up",
|
|
210
222
|
"resumed",
|
|
@@ -220,6 +232,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
220
232
|
"harness_error",
|
|
221
233
|
"policy_refusal",
|
|
222
234
|
"tool_refused",
|
|
235
|
+
"budget_salvage",
|
|
223
236
|
"stuck_loop",
|
|
224
237
|
] as const satisfies readonly RunNoteKind[];
|
|
225
238
|
type _EveryKindListed = [RunNoteKind] extends [(typeof RUN_NOTE_KINDS)[number]] ? true : never;
|
|
@@ -519,9 +532,15 @@ export type RunEvent =
|
|
|
519
532
|
/** The request's trace id (docs/reference/specs/tracing.md), once the root exists. */
|
|
520
533
|
traceId?: string;
|
|
521
534
|
/** The harness the run is driven by (`Harness.name`; docs/reference/specs/harness.md
|
|
522
|
-
*
|
|
535
|
+
* items 8 and 10): the word the scopes resolved for the preset, `pi`
|
|
536
|
+
* when none named it. Absent on a command run, which starts no process,
|
|
523
537
|
* and on a record written before the seam existed. Additive. */
|
|
524
538
|
harness?: string;
|
|
539
|
+
/** Whose word put the run on that harness (item 10): the requester's own
|
|
540
|
+
* scope, the channel's, or the deployment's top-level block. Absent when
|
|
541
|
+
* no scope named the preset — the roster's default — and on records
|
|
542
|
+
* written before the word was a scope setting. */
|
|
543
|
+
harnessScope?: HarnessScope;
|
|
525
544
|
effort?: string;
|
|
526
545
|
repo?: string;
|
|
527
546
|
ref?: string;
|
|
@@ -56,6 +56,8 @@ export interface LiveRunMeta {
|
|
|
56
56
|
/** The bound credential behind the person (authorization.md item 15): a
|
|
57
57
|
* resume or restart dispatches under ITS grants again, never the person's. */
|
|
58
58
|
authenticatedAs?: string;
|
|
59
|
+
/** The app that relayed the request for the person (authorization.md item 14): a resume or restart keeps app ∩ person at the gates. */
|
|
60
|
+
postedBy?: string;
|
|
59
61
|
effort?: string;
|
|
60
62
|
ref?: string;
|
|
61
63
|
headSha?: string;
|
|
@@ -109,6 +109,9 @@ export interface RunRecord {
|
|
|
109
109
|
receivedAt?: number;
|
|
110
110
|
sealedAt?: number;
|
|
111
111
|
replyOk?: boolean;
|
|
112
|
+
/** Why a `replyOk: false` reply was not delivered (e.g. "no channel to deliver
|
|
113
|
+
* to", run-history.md item 38). Absent when the reply was delivered or none was made. */
|
|
114
|
+
replyNote?: string;
|
|
112
115
|
stepCount?: number;
|
|
113
116
|
schema?: number;
|
|
114
117
|
status: RunStatus;
|
|
@@ -783,6 +786,7 @@ export function isRunRecord(v: unknown): v is RunRecord {
|
|
|
783
786
|
if (r[key] !== undefined && !isFiniteNumber(r[key])) return false;
|
|
784
787
|
}
|
|
785
788
|
if (r.replyOk !== undefined && typeof r.replyOk !== "boolean") return false;
|
|
789
|
+
if (r.replyNote !== undefined && typeof r.replyNote !== "string") return false;
|
|
786
790
|
for (const key of ["stepCount", "schema"] as const) {
|
|
787
791
|
if (r[key] !== undefined && (!isFiniteNumber(r[key]) || !Number.isInteger(r[key]) || (r[key] as number) < 0))
|
|
788
792
|
return false;
|
|
@@ -428,8 +428,10 @@ function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
|
|
|
428
428
|
return (
|
|
429
429
|
`Rebase ${branch} onto ${onto} before any other work — the parent unit has merged and the base has moved; ` +
|
|
430
430
|
`the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop. ` +
|
|
431
|
-
`
|
|
432
|
-
`
|
|
431
|
+
`Push the branch as soon as the change exists and its cheapest proving checks pass — before the project's ` +
|
|
432
|
+
`full verification, which runs after that push with any fix as a further commit; an unpushed tree does not ` +
|
|
433
|
+
`survive the run's end. Right before each push, fetch ${onto} again and rebase once more if it moved while ` +
|
|
434
|
+
`you worked, so the pull request is not born conflicting.`
|
|
433
435
|
);
|
|
434
436
|
}
|
|
435
437
|
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* cycle, and these say the snapshot (or the container that would restore it)
|
|
10
10
|
* cannot be used — never a provision failure, which would loop against the
|
|
11
11
|
* same broken build. `runtime-unreachable` is item 64's last rung: a recreated
|
|
12
|
-
* container that did not answer either
|
|
13
|
-
|
|
12
|
+
* container that did not answer either; `infra-streak` is item 67's: a
|
|
13
|
+
* recreated container whose cycles kept failing in the resident's own steps. */
|
|
14
|
+
export const REHYDRATION_FAILURE_RE =
|
|
15
|
+
/^(r2-restore-failed|snapshot-stamp-mismatch|no-snapshot|runtime-unreachable|infra-streak)/;
|
|
14
16
|
|
|
15
17
|
/** How many auto-rebuilds one resident gets inside one window. A rebuild is a
|
|
16
18
|
* clone, an install and a build (minutes to half an hour) that holds the cap
|
|
@@ -67,10 +67,9 @@ export type FetchReason = "missing-ref" | "stale-tip" | "returnable-ref";
|
|
|
67
67
|
* lost and a ref a person named that the thread never pushed has no way
|
|
68
68
|
* back, so neither pays this fetch: the refresh cycle is their freshness.
|
|
69
69
|
* - otherwise null.
|
|
70
|
-
* A fetch that STILL leaves the tip elsewhere (a push racing this attach,
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* review of it may do. */
|
|
70
|
+
* A fetch that STILL leaves the tip elsewhere (a push racing this attach, a
|
|
71
|
+
* force-push, or a fetch that failed) is `attachTarget`'s concern: the attach
|
|
72
|
+
* is refused `stale-tip` rather than run at a commit nobody asked for. */
|
|
74
73
|
export function mirrorFetchReason(input: {
|
|
75
74
|
refExists: boolean;
|
|
76
75
|
mirrorSha?: string;
|
|
@@ -85,8 +84,16 @@ export function mirrorFetchReason(input: {
|
|
|
85
84
|
|
|
86
85
|
/** What the attach checks out, once the mirror is as fresh as it will get. */
|
|
87
86
|
export type AttachTarget =
|
|
88
|
-
/** The bound ref is in the mirror
|
|
87
|
+
/** The bound ref is in the mirror, and its tip is the commit the caller
|
|
88
|
+
* named (or the caller named none): clone its tip, as always. */
|
|
89
89
|
| { kind: "ref" }
|
|
90
|
+
/** The bound ref is in the mirror but its tip is NOT the commit the caller
|
|
91
|
+
* named, even after the fetch — the mirror is behind it (the fetch failed
|
|
92
|
+
* or was skipped) or ahead of it (a push raced the attach). Refused: a run
|
|
93
|
+
* executes at the sha it asked for, or not on this resident; the caller
|
|
94
|
+
* falls back cold at the requested commit. `tip` is null when the tip
|
|
95
|
+
* could not be read — never assumed fresh. */
|
|
96
|
+
| { kind: "stale-tip"; tip: string | null; want: string }
|
|
90
97
|
/** The ref is gone but the commit the caller expects is in the mirror — a
|
|
91
98
|
* merged PR's branch was deleted while `refs/pull/N/head` (a `--mirror`
|
|
92
99
|
* clone carries every ref) still holds its head: check that commit out,
|
|
@@ -96,16 +103,22 @@ export type AttachTarget =
|
|
|
96
103
|
/** Neither: the attach is refused as `unknown-ref`. */
|
|
97
104
|
| { kind: "unknown-ref" };
|
|
98
105
|
|
|
99
|
-
/** The attach target after the fetch (item 51).
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
106
|
+
/** The attach target after the fetch (item 51). A ref that exists is cloned
|
|
107
|
+
* at its tip when the caller named no commit or the tip is that commit;
|
|
108
|
+
* a tip that is any other commit is `stale-tip`, refused. A detached tree is
|
|
109
|
+
* only for a ref that is gone; a caller that named no commit, or whose commit
|
|
110
|
+
* the mirror does not hold either, gets the refusal it always got. */
|
|
103
111
|
export function attachTarget(input: {
|
|
104
112
|
refExists: boolean;
|
|
105
113
|
wantSha: string | null;
|
|
106
114
|
commitInMirror: boolean;
|
|
115
|
+
/** The ref's tip in the mirror after the fetch; null or absent when it could not be read. */
|
|
116
|
+
tipSha?: string | null;
|
|
107
117
|
}): AttachTarget {
|
|
108
|
-
if (input.refExists)
|
|
118
|
+
if (input.refExists) {
|
|
119
|
+
if (input.wantSha === null || input.tipSha === input.wantSha) return { kind: "ref" };
|
|
120
|
+
return { kind: "stale-tip", tip: input.tipSha ?? null, want: input.wantSha };
|
|
121
|
+
}
|
|
109
122
|
if (input.wantSha !== null && input.commitInMirror) return { kind: "sha", sha: input.wantSha };
|
|
110
123
|
return { kind: "unknown-ref" };
|
|
111
124
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// A refresh cycle that keeps failing in the resident's OWN steps heals itself
|
|
2
|
+
// (docs/reference/specs/resident-repos.md item 67). Pure: the Worker's
|
|
3
|
+
// `refreshFailed` bumps the row and acts on the rung; the cycle's gate asks
|
|
4
|
+
// `parksOnRepeat` before it counts a degraded reason toward the park streak.
|
|
5
|
+
//
|
|
6
|
+
// The split this module draws: a step that runs the repository's own command
|
|
7
|
+
// (the onboard-time command table — install, build, test) failing is evidence
|
|
8
|
+
// about the repository, and the right response is to park and wait for the
|
|
9
|
+
// head to be fixed, which is what the park streak does. Every other step is
|
|
10
|
+
// the resident's machinery — git against the mirror, the probes, the markers,
|
|
11
|
+
// the restores — and a failure there that repeats is the container or the
|
|
12
|
+
// disk gone wrong, which a rebuild does fix. Item 64 already climbs this
|
|
13
|
+
// ladder for one signature (the control port that never answers); this is
|
|
14
|
+
// the same ladder for every other resident-step failure.
|
|
15
|
+
|
|
16
|
+
import { STALE_SWEEP_SUFFIX } from "./residentSteps.js";
|
|
17
|
+
|
|
18
|
+
/** The steps that run the repository's own commands. */
|
|
19
|
+
export const REPO_COMMAND_STEPS: ReadonlySet<string> = new Set(["deps-install", "build", "test"]);
|
|
20
|
+
|
|
21
|
+
/** Whether a failed step ran the repository's command (evidence about the
|
|
22
|
+
* repo) rather than the resident's own machinery. The stale-process sweep
|
|
23
|
+
* that precedes a repo step is ours. */
|
|
24
|
+
export function isRepoCommandStep(step: string): boolean {
|
|
25
|
+
if (step.endsWith(STALE_SWEEP_SUFFIX)) return false;
|
|
26
|
+
return REPO_COMMAND_STEPS.has(step);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The step named by a `<step>-failed:` reason (`classifyRefreshFailure`'s
|
|
30
|
+
* plain-failure shape), or null for every other reason. */
|
|
31
|
+
export function stepOfFailedReason(reason: string): string | null {
|
|
32
|
+
const m = /^([a-z][a-z0-9-]*)-failed:/.exec(reason);
|
|
33
|
+
return m ? m[1] : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Whether a repeated degraded reason should PARK the resident (the 6-hour
|
|
37
|
+
* cadence, `DEGRADED_PARK_AFTER_CYCLES`): only evidence about the repository
|
|
38
|
+
* or about GitHub — a repo-command step's failure, or a reason that names no
|
|
39
|
+
* step at all (`github-unreachable`). A resident-step failure never parks:
|
|
40
|
+
* the ladder below is its escalation, and parking would only slow it. */
|
|
41
|
+
export function parksOnRepeat(reason: string): boolean {
|
|
42
|
+
const step = stepOfFailedReason(reason);
|
|
43
|
+
return step === null || isRepoCommandStep(step);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The ladder's rungs: consecutive cycles failed in resident steps. */
|
|
47
|
+
export const INFRA_STREAK_RECREATE_AT = 3;
|
|
48
|
+
export const INFRA_STREAK_DOWN_AT = 5;
|
|
49
|
+
|
|
50
|
+
export type InfraStreakRung = "count" | "recreate" | "down";
|
|
51
|
+
|
|
52
|
+
/** Count 1–2: record and let the next cycle try. Count 3: destroy the
|
|
53
|
+
* container, snapshots kept — the next cycle restores from the snapshot onto
|
|
54
|
+
* a fresh disk (item 64's rung 3). Count 4: the recreated container's one
|
|
55
|
+
* cycle of its own. Count ≥ 5: a fresh container failed the same way; `down`
|
|
56
|
+
* with a rehydration-flavored reason, so item 36's transition rebuild is the
|
|
57
|
+
* exit. */
|
|
58
|
+
export function infraStreakRung(count: number): InfraStreakRung {
|
|
59
|
+
if (!Number.isFinite(count) || count < 1) {
|
|
60
|
+
throw new RangeError(`infra-streak rung needs a count of at least 1, got ${count}`);
|
|
61
|
+
}
|
|
62
|
+
if (count >= INFRA_STREAK_DOWN_AT) return "down";
|
|
63
|
+
if (count === INFRA_STREAK_RECREATE_AT) return "recreate";
|
|
64
|
+
return "count";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The persisted row (`resident:infraStreak`): the last failing step, the
|
|
68
|
+
* consecutive count and the span. */
|
|
69
|
+
export interface InfraStreakRow {
|
|
70
|
+
step: string;
|
|
71
|
+
count: number;
|
|
72
|
+
firstAt: string;
|
|
73
|
+
lastAt: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const INFRA_STREAK_ACTION: Readonly<Record<Exclude<InfraStreakRung, "count">, string>> = {
|
|
77
|
+
recreate: "the container was destroyed, snapshots kept; the next cycle restores from the snapshot",
|
|
78
|
+
down: "a recreated container failed the same way; rebuilding",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** The reason a rung records: `infra-streak:` so the gate, the serviceability
|
|
82
|
+
* check and item 36's eligibility all read it as the resident's, never the repo's. */
|
|
83
|
+
export function infraStreakReason(row: InfraStreakRow, rung: Exclude<InfraStreakRung, "count">): string {
|
|
84
|
+
return `infra-streak: ${row.count} consecutive cycles failed in the resident's own steps (last: ${row.step}, since ${row.firstAt}) — ${INFRA_STREAK_ACTION[rung]}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isInfraStreakReason(reason: string): boolean {
|
|
88
|
+
return /^infra-streak: /.test(reason);
|
|
89
|
+
}
|
|
@@ -132,6 +132,10 @@ export interface RefreshFailure {
|
|
|
132
132
|
* with the attempt count (the ladder below decides what the resident does
|
|
133
133
|
* about it); real failures keep `<step>-failed:`. */
|
|
134
134
|
reason: string;
|
|
135
|
+
/** The step that failed, as the caller named it (`refresh` for a failure
|
|
136
|
+
* between steps): item 67 reads whether it ran the repository's own command
|
|
137
|
+
* or the resident's machinery. */
|
|
138
|
+
step: string;
|
|
135
139
|
interrupted: boolean;
|
|
136
140
|
diskFull: boolean;
|
|
137
141
|
runtimeUnreachable: boolean;
|
|
@@ -234,6 +238,7 @@ export function classifyRefreshFailure(input: {
|
|
|
234
238
|
const { step, message } = input;
|
|
235
239
|
if (input.runtimeUnreachable) {
|
|
236
240
|
return {
|
|
241
|
+
step,
|
|
237
242
|
interrupted: false,
|
|
238
243
|
diskFull: false,
|
|
239
244
|
runtimeUnreachable: true,
|
|
@@ -242,6 +247,7 @@ export function classifyRefreshFailure(input: {
|
|
|
242
247
|
}
|
|
243
248
|
if (isDiskFullMessage(message)) {
|
|
244
249
|
return {
|
|
250
|
+
step,
|
|
245
251
|
interrupted: false,
|
|
246
252
|
diskFull: true,
|
|
247
253
|
runtimeUnreachable: false,
|
|
@@ -251,6 +257,7 @@ export function classifyRefreshFailure(input: {
|
|
|
251
257
|
const timedOut = /\(timed out\)/.test(message);
|
|
252
258
|
if (!timedOut && (INTERRUPTION_SIGNATURE.test(message) || RUNTIME_REPLACEMENT_WORDING.test(message))) {
|
|
253
259
|
return {
|
|
260
|
+
step,
|
|
254
261
|
interrupted: true,
|
|
255
262
|
diskFull: false,
|
|
256
263
|
runtimeUnreachable: false,
|
|
@@ -259,13 +266,20 @@ export function classifyRefreshFailure(input: {
|
|
|
259
266
|
}
|
|
260
267
|
if (input.freeKiB !== undefined && input.freeKiB !== null && input.freeKiB < DISK_FULL_FREE_KIB) {
|
|
261
268
|
return {
|
|
269
|
+
step,
|
|
262
270
|
interrupted: false,
|
|
263
271
|
diskFull: true,
|
|
264
272
|
runtimeUnreachable: false,
|
|
265
273
|
reason: diskFullReason({ step, message, freeKiB: input.freeKiB }),
|
|
266
274
|
};
|
|
267
275
|
}
|
|
268
|
-
return {
|
|
276
|
+
return {
|
|
277
|
+
step,
|
|
278
|
+
interrupted: false,
|
|
279
|
+
diskFull: false,
|
|
280
|
+
runtimeUnreachable: false,
|
|
281
|
+
reason: `${step}-failed: ${message}`,
|
|
282
|
+
};
|
|
269
283
|
}
|
|
270
284
|
|
|
271
285
|
// -- the runtime that never answers --------------------------------------------
|
|
@@ -49,6 +49,7 @@ export const RESIDENT_STEP_LABELS = {
|
|
|
49
49
|
"worktree-clean": "cleaning the worktree",
|
|
50
50
|
"clean-workspace": "cleaning the workspace",
|
|
51
51
|
"clean-before-restore": "cleaning before the restore",
|
|
52
|
+
"break-mirror": "removing the mirror's objects (fault injection)",
|
|
52
53
|
"unmount-restores": "unmounting earlier restores",
|
|
53
54
|
"mirror-restore-extract": "extracting the mirror snapshot",
|
|
54
55
|
"checkout-restore-extract": "extracting the checkout snapshot",
|