@coreplane/switchboard 1.244.0 → 1.245.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/assets/config/config.example.yaml +6 -3
  2. package/dist/assets/deploy/cloudflare/worker.ts +2 -0
  3. package/dist/assets/deploy/cloudflare-resident/threadErr.ts +337 -0
  4. package/dist/assets/deploy/cloudflare-resident/tsconfig.json +1 -1
  5. package/dist/assets/deploy/cloudflare-resident/worker.ts +31 -262
  6. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +5 -10
  7. package/dist/assets/deploy/secrets.manifest.json +12 -0
  8. package/dist/assets/package-lock.json +3 -3
  9. package/dist/assets/package.json +1 -1
  10. package/dist/assets/source.json +3 -3
  11. package/dist/assets/src/agents/registry.ts +4 -4
  12. package/dist/assets/src/config/profile.ts +17 -1
  13. package/dist/assets/src/core/authz/types.ts +8 -0
  14. package/dist/assets/src/core/budgets.ts +25 -0
  15. package/dist/assets/src/core/reviewVerdict.ts +101 -24
  16. package/dist/assets/src/core/runEvents.ts +26 -1
  17. package/dist/assets/src/core/runRecord.ts +34 -0
  18. package/dist/assets/src/core/ship/coordinator.ts +25 -21
  19. package/dist/assets/src/execution/residentRefresh.ts +25 -6
  20. package/dist/assets/web/dist/.vite/manifest.json +481 -462
  21. package/dist/assets/web/dist/assets/AppShell-qqs_p96-.js +1 -0
  22. package/dist/assets/web/dist/assets/CostsPage-Dd9aXxxI.js +2 -0
  23. package/dist/assets/web/dist/assets/DeliveryPage-D90FTjYq.js +1 -0
  24. package/dist/assets/web/dist/assets/HomePage-VLTFopnl.js +2 -0
  25. package/dist/assets/web/dist/assets/InputMenu-lQP3ia9D.js +1 -0
  26. package/dist/assets/web/dist/assets/NotFoundPage-8ViskpN3.js +1 -0
  27. package/dist/assets/web/dist/assets/PendingTurnRow-D2gHef5v.js +1 -0
  28. package/dist/assets/web/dist/assets/ResidentDetailPage-nWLN8BOd.js +1 -0
  29. package/dist/assets/web/dist/assets/ResidentsIndexPage-D3Q48CJW.js +1 -0
  30. package/dist/assets/web/dist/assets/{RunFoldRow-BPMQxTcl.js → RunFoldRow-CHy_IfZG.js} +1 -1
  31. package/dist/assets/web/dist/assets/RunRoutePage-C6kX5om4.js +9 -0
  32. package/dist/assets/web/dist/assets/RunsIndexPage-DjxGj1Q5.js +1 -0
  33. package/dist/assets/web/dist/assets/RunsTabs-WyeG5tKb.js +1 -0
  34. package/dist/assets/web/dist/assets/ScheduledPage-B8D5AU4d.js +1 -0
  35. package/dist/assets/web/dist/assets/SettingSelect-UTHsqUUS.js +1 -0
  36. package/dist/assets/web/dist/assets/SettingsPage-BoW7Zjzt.js +1 -0
  37. package/dist/assets/web/dist/assets/StatusDot-BGHureCJ.js +1 -0
  38. package/dist/assets/web/dist/assets/Tooltip-CiP8qPd7.js +1 -0
  39. package/dist/assets/web/dist/assets/UnitRoutePage-2zEeRBP1.js +1 -0
  40. package/dist/assets/web/dist/assets/{angular-html-Dail07rr.js → angular-html-qXhb9ti8.js} +1 -1
  41. package/dist/assets/web/dist/assets/{angular-ts-BReExio8.js → angular-ts-DzvdyoUZ.js} +1 -1
  42. package/dist/assets/web/dist/assets/{apl-DhNO6Yyx.js → apl-Ck7Ru2Sm.js} +1 -1
  43. package/dist/assets/web/dist/assets/{astro-BYotTY4C.js → astro-CwehvReb.js} +1 -1
  44. package/dist/assets/web/dist/assets/{blade-mUxTtD1X.js → blade-WSyVbSFK.js} +1 -1
  45. package/dist/assets/web/dist/assets/{c-DhuiwcSQ.js → c-DMHI1wDz.js} +1 -1
  46. package/dist/assets/web/dist/assets/{chapel-GD2vFtRi.js → chapel-BF8AYK8_.js} +1 -1
  47. package/dist/assets/web/dist/assets/{cobol-BhFDwkes.js → cobol-UAfImuWu.js} +1 -1
  48. package/dist/assets/web/dist/assets/{coffee-DWUgbE0r.js → coffee-D54hi96d.js} +1 -1
  49. package/dist/assets/web/dist/assets/{cpp-CO9XFjGW.js → cpp-BRWfLgBc.js} +1 -1
  50. package/dist/assets/web/dist/assets/{crystal-B9pVurpn.js → crystal-CSqC5deT.js} +1 -1
  51. package/dist/assets/web/dist/assets/{css-CFVVV6lI.js → css-D4ZuRFGk.js} +1 -1
  52. package/dist/assets/web/dist/assets/{dist-BtPs2AkQ.js → dist-73ZMn_Mg.js} +2 -2
  53. package/dist/assets/web/dist/assets/{durationTone-BobbycC-.js → durationTone-D51enaGR.js} +1 -1
  54. package/dist/assets/web/dist/assets/{edge-BDhIDQIb.js → edge-DDPtNbZc.js} +1 -1
  55. package/dist/assets/web/dist/assets/{elixir-DG3J8WoD.js → elixir-BTQkQfsJ.js} +1 -1
  56. package/dist/assets/web/dist/assets/{elm-FiOdiaOE.js → elm-B_PkLzGZ.js} +1 -1
  57. package/dist/assets/web/dist/assets/{erb-vxbtW503.js → erb-DfjIMVM_.js} +1 -1
  58. package/dist/assets/web/dist/assets/{eventSource-tRprhqm7.js → eventSource-By0B_w4S.js} +1 -1
  59. package/dist/assets/web/dist/assets/{git-rebase-DINz8M3H.js → git-rebase-DP8wmn7K.js} +1 -1
  60. package/dist/assets/web/dist/assets/{glimmer-js-yann6r9i.js → glimmer-js-G12VK2gK.js} +1 -1
  61. package/dist/assets/web/dist/assets/{glimmer-ts-CXTbrZn_.js → glimmer-ts-D3XiSI8E.js} +1 -1
  62. package/dist/assets/web/dist/assets/{glsl-DNYC-hq0.js → glsl-DOKAhMj-.js} +1 -1
  63. package/dist/assets/web/dist/assets/{graphql-CwzhHzi9.js → graphql-CJB_fmON.js} +1 -1
  64. package/dist/assets/web/dist/assets/{hack-BBXF1cwR.js → hack-BHqtRWap.js} +1 -1
  65. package/dist/assets/web/dist/assets/{haml-oEqBcsTy.js → haml-DwotyarS.js} +1 -1
  66. package/dist/assets/web/dist/assets/{handlebars-YE5thkT1.js → handlebars-B6fDaA3U.js} +1 -1
  67. package/dist/assets/web/dist/assets/{html-derivative-DCT1GYtQ.js → html-derivative-C5bpQmmI.js} +1 -1
  68. package/dist/assets/web/dist/assets/{html-Dp4jRQ1j.js → html-s52taj36.js} +1 -1
  69. package/dist/assets/web/dist/assets/{http-BPcGbmzJ.js → http-99zC935r.js} +1 -1
  70. package/dist/assets/web/dist/assets/{hurl-CQnNHwyd.js → hurl-Bgk-4-0x.js} +1 -1
  71. package/dist/assets/web/dist/assets/{java-Dm8Ovj6W.js → java-CuV_c5I0.js} +1 -1
  72. package/dist/assets/web/dist/assets/{javascript-Btm9oJK9.js → javascript-CZlOvSMQ.js} +1 -1
  73. package/dist/assets/web/dist/assets/{jinja-VZYjWF_U.js → jinja-BuXeZ4Tu.js} +1 -1
  74. package/dist/assets/web/dist/assets/{jison-Dcmjxa9x.js → jison-DIuP0hkG.js} +1 -1
  75. package/dist/assets/web/dist/assets/{json-DoWiOGZ8.js → json-CZ74vfoD.js} +1 -1
  76. package/dist/assets/web/dist/assets/{jsx-Dvmo55ma.js → jsx-BNjRs8Vj.js} +1 -1
  77. package/dist/assets/web/dist/assets/{julia-d8AtfV26.js → julia-OCE-fFOg.js} +1 -1
  78. package/dist/assets/web/dist/assets/{just-Cc5EStA-.js → just-CtvD87WF.js} +1 -1
  79. package/dist/assets/web/dist/assets/{latex-B5fKKIH7.js → latex-C3kf86OR.js} +1 -1
  80. package/dist/assets/web/dist/assets/{liquid-DUYI9JND.js → liquid-B1WI--Kp.js} +1 -1
  81. package/dist/assets/web/dist/assets/{lua-BhMwKWpw.js → lua-DtoT0gKg.js} +1 -1
  82. package/dist/assets/web/dist/assets/main-ClluP-Qf.js +28 -0
  83. package/dist/assets/web/dist/assets/main-CpkwzHX6.css +1 -0
  84. package/dist/assets/web/dist/assets/{marko-Cnf2cPAi.js → marko-BE2MC6Yt.js} +1 -1
  85. package/dist/assets/web/dist/assets/{mdc-FDDXoir9.js → mdc-BwO4weXJ.js} +1 -1
  86. package/dist/assets/web/dist/assets/{nginx-q-8U_1jR.js → nginx-BTuOgGVC.js} +1 -1
  87. package/dist/assets/web/dist/assets/{nim-DIA0TUWs.js → nim-CiIUM-oV.js} +1 -1
  88. package/dist/assets/web/dist/assets/{org-CR88hy7S.js → org-L78DEWB-.js} +1 -1
  89. package/dist/assets/web/dist/assets/{perl-Bkd8N7ja.js → perl-KqHOm4Ft.js} +1 -1
  90. package/dist/assets/web/dist/assets/{php-BOs84mvy.js → php-C41XbEkn.js} +1 -1
  91. package/dist/assets/web/dist/assets/{pug-BbpZBb3w.js → pug-B12-4i-k.js} +1 -1
  92. package/dist/assets/web/dist/assets/{qml-D2jQpjFE.js → qml--fw2Up5c.js} +1 -1
  93. package/dist/assets/web/dist/assets/{r-DKzcAQ22.js → r-Db1JBXrC.js} +1 -1
  94. package/dist/assets/web/dist/assets/{razor-SOQMWcC9.js → razor-BYpVBgbu.js} +1 -1
  95. package/dist/assets/web/dist/assets/{regexp-B6KK4W8d.js → regexp-BMqGLSPc.js} +1 -1
  96. package/dist/assets/web/dist/assets/{rst-kPSg-NPf.js → rst-B63n7-Sa.js} +1 -1
  97. package/dist/assets/web/dist/assets/{ruby-cKS-JYLH.js → ruby-D3VI8UYv.js} +1 -1
  98. package/dist/assets/web/dist/assets/{sas-DNxzE5gs.js → sas-DxRf8ZAR.js} +1 -1
  99. package/dist/assets/web/dist/assets/{scss-DGcHQ5OO.js → scss-DRRA6d6A.js} +1 -1
  100. package/dist/assets/web/dist/assets/seed-DFfT5S_N.js +6 -0
  101. package/dist/assets/web/dist/assets/{settingsApi-priOPiwu.js → settingsApi-Dm6eaQcu.js} +1 -1
  102. package/dist/assets/web/dist/assets/{shellscript-CRZeZBa5.js → shellscript-CoOBe3_y.js} +1 -1
  103. package/dist/assets/web/dist/assets/{shellsession-DSNEL0HX.js → shellsession-DOV1HYxf.js} +1 -1
  104. package/dist/assets/web/dist/assets/{soy-zisNbkDO.js → soy-Acpmpp1W.js} +1 -1
  105. package/dist/assets/web/dist/assets/{sql-MG06KMMb.js → sql-DeT9N-fA.js} +1 -1
  106. package/dist/assets/web/dist/assets/{sseReplay-DaRtc7nI.js → sseReplay-DPXVxhL5.js} +3 -3
  107. package/dist/assets/web/dist/assets/{stata-CB_wKwv9.js → stata-CbxXAMO4.js} +1 -1
  108. package/dist/assets/web/dist/assets/{surrealql-DYdSt7re.js → surrealql-BdKbi4hc.js} +1 -1
  109. package/dist/assets/web/dist/assets/{svelte-DHO-F7c7.js → svelte-tAcjDM7R.js} +1 -1
  110. package/dist/assets/web/dist/assets/{templ-CUL5L4Lr.js → templ-CObINNJj.js} +1 -1
  111. package/dist/assets/web/dist/assets/{tex-_JNEKjVW.js → tex-BYwpZT1J.js} +1 -1
  112. package/dist/assets/web/dist/assets/{ts-tags--9EpEV_8.js → ts-tags-BciREL5R.js} +1 -1
  113. package/dist/assets/web/dist/assets/{tsx-BcNq2x2u.js → tsx-CRiv3Pr3.js} +1 -1
  114. package/dist/assets/web/dist/assets/{twig-EsY5Dk_D.js → twig-DuZZ1Ej0.js} +1 -1
  115. package/dist/assets/web/dist/assets/{typescript-CyA_HiUH.js → typescript-jkHz_5HZ.js} +1 -1
  116. package/dist/assets/web/dist/assets/{typst-SzaHlDt4.js → typst-DOac4Gal.js} +1 -1
  117. package/dist/assets/web/dist/assets/{vue-D3cn5acV.js → vue-CgOX5wXA.js} +1 -1
  118. package/dist/assets/web/dist/assets/{vue-html-6MxhUxOa.js → vue-html-Duu99VNw.js} +1 -1
  119. package/dist/assets/web/dist/assets/{vue-vine-qJnqhgo9.js → vue-vine-Bmj_wvRJ.js} +1 -1
  120. package/dist/assets/web/dist/assets/wallClock-DD_fPuSY.js +1 -0
  121. package/dist/assets/web/dist/assets/{xml-DyRpC7xv.js → xml-BtUIVeWH.js} +1 -1
  122. package/dist/assets/web/dist/assets/{xsl-BXMRPAsg.js → xsl-wX71n1E5.js} +1 -1
  123. package/dist/assets/web/dist/assets/{yaml-BNmPCtAN.js → yaml-CdLdXvmz.js} +1 -1
  124. package/dist/cli.js +1474 -684
  125. package/package.json +1 -1
  126. package/dist/assets/web/dist/assets/AppShell-Ck4hSZDi.js +0 -1
  127. package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +0 -2
  128. package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +0 -1
  129. package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +0 -2
  130. package/dist/assets/web/dist/assets/NotFoundPage-CiGrVOyv.js +0 -1
  131. package/dist/assets/web/dist/assets/PendingTurnRow-BHCSpsPf.js +0 -1
  132. package/dist/assets/web/dist/assets/ResidentDetailPage-C5USV_8P.js +0 -1
  133. package/dist/assets/web/dist/assets/ResidentsIndexPage-CSKisWnX.js +0 -1
  134. package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +0 -9
  135. package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +0 -1
  136. package/dist/assets/web/dist/assets/RunsTabs-BWRHzhtT.js +0 -1
  137. package/dist/assets/web/dist/assets/ScheduledPage-DBaChcJZ.js +0 -1
  138. package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +0 -1
  139. package/dist/assets/web/dist/assets/StatusDot-_Q_bmReh.js +0 -1
  140. package/dist/assets/web/dist/assets/Tooltip-Cafsgd4m.js +0 -1
  141. package/dist/assets/web/dist/assets/UnitRoutePage-CdoBZwtf.js +0 -1
  142. package/dist/assets/web/dist/assets/main-C4366PZF.css +0 -1
  143. package/dist/assets/web/dist/assets/main-DlTNAC6c.js +0 -28
  144. package/dist/assets/web/dist/assets/seed-DeKSHrt2.js +0 -6
  145. package/dist/assets/web/dist/assets/wallClock-61NpVkY6.js +0 -1
package/dist/cli.js CHANGED
@@ -1501,6 +1501,9 @@ function postStepLease(preset, remainingMs) {
1501
1501
  if (remainingMs === void 0) return allowance;
1502
1502
  return Math.min(allowance, Math.max(1, remainingMs / MINUTE_MS));
1503
1503
  }
1504
+ function leaseMinimum(preset) {
1505
+ return ALLOWANCES.writeUp + postStepMinutes(preset) + 1;
1506
+ }
1504
1507
  function turnLeaseMs(askMinutes, remainingMs) {
1505
1508
  return Math.min(askMinutes * MINUTE_MS, Math.max(MINUTE_MS, remainingMs));
1506
1509
  }
@@ -1547,13 +1550,14 @@ function fit(pipeline) {
1547
1550
  const need = ALLOWANCES.provision + ASKS.coding + reserveMinutes({ kind: "coding", index: 0 }, pipeline);
1548
1551
  return { ok: pipeline.maxMinutes >= need, need, have: pipeline.maxMinutes };
1549
1552
  }
1550
- var MINUTE_MS, DAY_MS, CONFIRMATION_TTL_MS, LOOP_PRESETS, ASKS, PRESET_FLOORS, FLOORS, SHIP_WAIT, ALLOWANCES, POST_STEP_MINUTES, WRAP_UP_WARNING, BASH_COMMAND, RUNAWAY_TURNS_PER_MINUTE, DEFAULT_GRANT, GRANT_RENEWALS_MAX;
1553
+ var MINUTE_MS, DAY_MS, CONFIRMATION_TTL_MS, FIRST_ATTACH_WAIT_MS, LOOP_PRESETS, ASKS, PRESET_FLOORS, FLOORS, SHIP_WAIT, ALLOWANCES, POST_STEP_MINUTES, WRAP_UP_WARNING, BASH_COMMAND, RUNAWAY_TURNS_PER_MINUTE, DEFAULT_GRANT, GRANT_RENEWALS_MAX;
1551
1554
  var init_budgets = __esm({
1552
1555
  "../../src/core/budgets.ts"() {
1553
1556
  "use strict";
1554
1557
  MINUTE_MS = 6e4;
1555
1558
  DAY_MS = 24 * 60 * MINUTE_MS;
1556
1559
  CONFIRMATION_TTL_MS = 10 * MINUTE_MS;
1560
+ FIRST_ATTACH_WAIT_MS = MINUTE_MS;
1557
1561
  LOOP_PRESETS = ["general", "coding", "review", "research", "explore", "conductor"];
1558
1562
  ASKS = {
1559
1563
  general: 5,
@@ -2017,7 +2021,7 @@ var init_registry = __esm({
2017
2021
  IDENTITIES = ["none", "read", "write"];
2018
2022
  PR_DESCRIPTION_TEMPLATE = `PR description \u2014 submit it with the submit_pr_description tool for EVERY PR (this is the default, not something to wait to be asked for). Switchboard renders the GitHub body from the object you submit, so never author PR-body markdown yourself. Before submitting, judge your title with the ${PR_TITLE_GUARD} gate \u2014 \`npm run check:pr-title -- "<title>"\` \u2014 and submit only a title it accepts; the same gate refuses the PR in CI. The body is a fixed-size MAP for the reader with everything for agents collapsed under it; every field is capped in visible characters (a link's URL is not counted) and the tool refuses an object over a cap naming the field and the count \u2014 cut and resubmit. Prose is unwrapped \u2014 no hard line breaks inside a paragraph. Always hyperlink the triggering issue/request. Never fabricate validation \u2014 state exactly what you ran and the real result. BEFORE authoring the pointers, load the \`pr-description\` skill with use_skill \u2014 it defines how to choose at most seven pointers, the mechanical anchor rules and what goes below the fold; follow it for every PR.
2019
2023
  EVERY PR includes one that already exists when you push \u2014 opened by a person, by dependabot, or by an earlier run. After EVERY push to such a PR: read its current title and body (\`github_issue_get\` with the PR number works for pull requests; \`gh pr view\` where gh exists), judge them against the change as it now stands at the pushed head, and submit the object that describes the PR as it is NOW \u2014 carry forward what the existing body says that is still true (a dependency bump's release notes belong in why), add what you changed, and anchor the pointers at the new head. Switchboard replaces the PR's title and body with your rendering. A description that describes an earlier state of its branch is a bug; "it is someone else's PR" is never a reason to leave it.
2020
- - **title**: the PR title \u2014 one line naming the change, specific enough to pick out of a PR list.
2024
+ - **title** (\u226472 characters in all): the changelog line \u2014 \`type(scope): what a reader can now do or expect\`, one change, one clause, present tense. The type and the scope spend the same budget as the description, so the description is short; the body carries the rest. The tool refuses a longer title naming the count, and so does the gate in CI.
2021
2025
  - **TL;DR** (\`tldr\`, rendered first, \u2264300): two sentences for a reader with zero context \u2014 what this PR does and why it matters.
2022
2026
  - **Why** (\`why\`, \u2264400): the problem and the motivation, with the triggering issue/request, the record and the stack position hyperlinked. Why, never what: the diff shows what.
2023
2027
  - **Where to look** (\`pointers\`, 1 to 7): the files a reviewer would open first, in reading order, each { label \u226460, text \u2264160, optional risk \u2264100, anchor } with the anchor a { path, from, to } line range at your pushed head, rendered as a link (never embedded code). One pointer per idea, never per file; when the change has more ideas than seven, keep the seven whose mistake would cost most.
@@ -2157,7 +2161,7 @@ ${statusCardRule()}
2157
2161
  Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
2158
2162
  ${FENCED_CONTENT_RULE}
2159
2163
  Your final message is posted to Slack \u2014 keep it readable, lead with the outcome.`;
2160
- REVIEW_VERDICT_INSTRUCTION = `VERDICT: before your final message, call the submit_verdict tool exactly once with \`approve\` (no blocking issues \u2014 nits alone are not blocking) or \`request_changes\`, a one-line summary, \`head\` = the output of \`git rev-parse HEAD\` in the checkout you reviewed, and \`findings\` \u2014 every issue you report as a structured entry with a stable id you assign in order (F1, F2, \u2026), a severity of exactly blocking|major|minor|nit, the file (plus line when it points at one), and a one-line title. The findings array is the index of your review: the full explanation of each finding stays in your prose, keyed by the same ids. Switchboard writes the verdict as the first line of the GitHub comment itself and lists the findings under it; a review with no submitted verdict is posted as not approving, so never skip it. An \`approve\` carrying a blocking finding is downgraded to \`request_changes\` \u2014 approve only when nothing blocking remains. Do not write "LGTM" in your own text \u2014 the verdict line carries it.`;
2164
+ REVIEW_VERDICT_INSTRUCTION = `VERDICT: before your final message, call the submit_verdict tool exactly once with \`approve\` (no finding at or above the severity to address remains \u2014 the level in force for this run, \`minor\` by default: a major or a minor finding means \`request_changes\`; nits alone never block) or \`request_changes\`, a one-line summary, \`head\` = the output of \`git rev-parse HEAD\` in the checkout you reviewed, and \`findings\` \u2014 every issue you report as a structured entry with a stable id you assign in order (F1, F2, \u2026), a severity of exactly blocking|major|minor|nit, the file (plus line when it points at one), and a one-line title. The findings array is the index of your review: the full explanation of each finding stays in your prose, keyed by the same ids. Switchboard writes the verdict as the first line of the GitHub comment itself and lists the findings under it; a review with no submitted verdict is posted as not approving, so never skip it. An \`approve\` carrying a finding at or above the severity to address is downgraded to \`request_changes\` and the tool's ack says so \u2014 approve only when every finding sits below the level. Do not write "LGTM" in your own text \u2014 the verdict line carries it.`;
2161
2165
  REVIEW_SPEC_CHECK = `3. SPEC CONTRADICTION CHECK, when the repository has \`docs/reference/specs/\`: list the specs the change touches \u2014 \`npm run --silent specs:coverage -- --changed origin/<base>...HEAD\` (\`<base>\` is the PR's base branch) when the repository's package.json has that script, otherwise match the changed paths against each spec's \`- **Code**:\` / \`- **Tests**:\` header lines (a header path covers itself and everything beneath it). If the command fails for any reason \u2014 dependencies not installed, tsx missing, a cold checkout \u2014 fall back to matching the header lines by hand; never install dependencies or build to make it run. Then read ONLY those specs, never the whole specs tree \u2014 fold the reads into your gather batch where you can. For each touched spec, judge whether the diff contradicts a numbered behavior statement or a validation criterion: code that now does what the spec says it does not, a criterion whose named test the diff removed or retitled, a behavior the diff deleted that the spec still promises. A contradiction is a finding of severity \`minor\` or higher titled \`Spec contradiction \u2014 <spec file> item <n>: <what the code now does vs what the spec says>\`; a spec updated in the same diff to match the code is not a finding. A repository with no \`docs/reference/specs/\` has nothing to check \u2014 skip this step silently.
2162
2166
  3a. TEST GUARD, in the same repositories: run \`npm run --silent specs:coverage -- --changed origin/<base>...HEAD --test-guard\` (fold it into the same batch). It compares every test file the diff touches at the base and at the head and prints one line per thing lost, in two classes. A \`test-guard: <file> \u2014 removed: \u2026\` line is deterministic \u2014 the test file deleted, an it/test/describe title gone with no new title to pair with, a skip/only/todo marker (\`.skip(\`, \`.only(\`, \`xit(\`, \`xdescribe(\`, \`it.todo(\`, \`test.todo(\`) on a test the base ran \u2014 and each one is a finding of severity \`minor\` or higher titled \`Test removed \u2014 <file>: <what>\` whose explanation quotes the guard's line exactly as printed. A \`test-guard: <file> \u2014 check: \u2026\` line is a heuristic \u2014 fewer \`expect(\` calls in the file, a title gone while another arrived (a rename or a split) \u2014 and you dispose of every one of them explicitly in your review, never silently: either "weakened", which makes it a finding at \`minor\`, or "refactor, verification intact" with one clause saying why. A line ending \`\u2014 allowed by <spec>\` is licensed by a spec change in the same diff and is neither; \`test-guard ok\` is nothing to report. If the command fails for any reason, judge the same facts from the diff by hand \u2014 a deleted test file, a removed title, a new skip marker, fewer assertions \u2014 and file each one the diff does not license the same way; never install or build to make it run.`;
2163
2167
  REVIEW_UNIT_CONTRACT = ` 3b. UNIT CONTRACT, when this prompt carries a \`${CONTRACT_HEADING}\` block after the REVIEW TARGET block (its sub-headings, in order: ${CONTRACT_HEADINGS_LIST}): it is what the coding child was handed for this plan unit, rendered by Switchboard from the plan, and the diff is judged against it. Read the unit's Test scenarios and find each one in the diff: a test scenario the unit listed and the diff did not add is a finding of severity \`minor\` titled \`Contract \u2014 test scenario missing: <the scenario>\` \u2014 the same severity as a spec contradiction. For each spec row the block names, check its proof binding resolves in the diff; a named row the diff leaves untouched is disposed of out loud, as in 3a: one clause on why it needed no change, or a finding at \`minor\` titled \`Contract \u2014 spec row not updated: <spec> item <n>\`. A guard the block names that the diff weakens is the guard's own finding (3a). No \`${CONTRACT_HEADING}\` block in this prompt \u2192 nothing to check; skip this step silently.`;
@@ -2598,33 +2602,45 @@ var init_redact = __esm({
2598
2602
  });
2599
2603
 
2600
2604
  // ../../src/core/reviewVerdict.ts
2601
- function parseVerdictInput(input) {
2605
+ function severityAtOrAbove(severity, level) {
2606
+ return isAddressSeverity(severity) && severityRank(severity) <= severityRank(level);
2607
+ }
2608
+ function resolveAddressSeverity(layers) {
2609
+ if (layers.run !== void 0) return { level: layers.run, source: "run" };
2610
+ if (layers.user !== void 0) return { level: layers.user, source: "user" };
2611
+ if (layers.channel !== void 0) return { level: layers.channel, source: "channel" };
2612
+ return { level: layers.org ?? DEFAULT_ADDRESS_SEVERITY, source: "org" };
2613
+ }
2614
+ function downgradeNote(d) {
2615
+ return `finding${d.findings.length === 1 ? "" : "s"} ${d.findings.join(", ")} at or above ${d.level}, the severity to address`;
2616
+ }
2617
+ function parseVerdictInput(input, opts = {}) {
2602
2618
  const verdict = input.verdict;
2603
2619
  if (verdict !== "approve" && verdict !== "request_changes") return null;
2620
+ const level = opts.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY;
2604
2621
  const summary = typeof input.summary === "string" ? oneLine2(input.summary) : "";
2605
2622
  const head = normalizeHead(input.head);
2606
2623
  const out = { verdict, summary };
2607
2624
  if (head) out.head = head;
2608
2625
  if (input.findings !== void 0) {
2609
- const parsed = parseFindings(input.findings);
2626
+ const parsed = parseFindings(input.findings, level);
2610
2627
  if (parsed.findings) out.findings = parsed.findings;
2611
2628
  if (parsed.dropped.length) out.droppedFindings = parsed.dropped;
2612
- if (out.verdict === "approve" && parsed.blocking.length > 0) {
2629
+ if (out.verdict === "approve" && parsed.gated.length > 0) {
2613
2630
  out.verdict = "request_changes";
2614
- out.summary = oneLine2(
2615
- `${out.summary} [downgraded from approve: blocking finding${parsed.blocking.length === 1 ? "" : "s"} ${parsed.blocking.join(", ")}]`
2616
- );
2631
+ out.downgraded = { findings: parsed.gated, level };
2632
+ out.summary = oneLine2(`${out.summary} [downgraded from approve: ${downgradeNote(out.downgraded)}]`);
2617
2633
  }
2618
2634
  }
2619
2635
  return out;
2620
2636
  }
2621
- function parseFindings(value) {
2637
+ function parseFindings(value, level) {
2622
2638
  if (!Array.isArray(value)) {
2623
- return { dropped: ["findings: dropped \u2014 not an array"], blocking: [] };
2639
+ return { dropped: ["findings: dropped \u2014 not an array"], gated: [] };
2624
2640
  }
2625
2641
  const findings = [];
2626
2642
  const dropped = [];
2627
- const blocking = [];
2643
+ const gated = [];
2628
2644
  value.forEach((raw, i) => {
2629
2645
  const parsed = parseFinding(raw);
2630
2646
  if (parsed.finding) {
@@ -2634,9 +2650,9 @@ function parseFindings(value) {
2634
2650
  }
2635
2651
  const label = parsed.finding?.id ?? parsed.id ?? `findings[${i}]`;
2636
2652
  const r = typeof raw === "object" && raw !== null ? raw : void 0;
2637
- if (r?.severity === "blocking") blocking.push(label);
2653
+ if (severityAtOrAbove(r?.severity, level)) gated.push(`${label} (${r?.severity})`);
2638
2654
  });
2639
- return { findings, dropped, blocking };
2655
+ return { findings, dropped, gated };
2640
2656
  }
2641
2657
  function parseFinding(raw) {
2642
2658
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { reason: "not an object" };
@@ -2745,13 +2761,17 @@ function redactVerdict(v, redact = redactSecrets) {
2745
2761
  function redactDispositions(dispositions, redact = redactSecrets) {
2746
2762
  return dispositions.map((d) => ({ findingId: d.findingId, disposition: d.disposition, note: redact(d.note) }));
2747
2763
  }
2748
- var FINDING_SEVERITIES, LGTM_TOKEN, CHANGES_TOKEN, NO_VERDICT_LINE, VERDICT_KINDS, DISPOSITION_KINDS, isRecordLike, REVIEW_POST_HEAD;
2764
+ var FINDING_SEVERITIES, ADDRESS_SEVERITIES, DEFAULT_ADDRESS_SEVERITY, isAddressSeverity, severityRank, LGTM_TOKEN, CHANGES_TOKEN, NO_VERDICT_LINE, VERDICT_KINDS, DISPOSITION_KINDS, isRecordLike, REVIEW_POST_HEAD;
2749
2765
  var init_reviewVerdict = __esm({
2750
2766
  "../../src/core/reviewVerdict.ts"() {
2751
2767
  "use strict";
2752
2768
  init_reviewedHead();
2753
2769
  init_redact();
2754
2770
  FINDING_SEVERITIES = ["blocking", "major", "minor", "nit"];
2771
+ ADDRESS_SEVERITIES = FINDING_SEVERITIES;
2772
+ DEFAULT_ADDRESS_SEVERITY = "minor";
2773
+ isAddressSeverity = (v) => ADDRESS_SEVERITIES.includes(v);
2774
+ severityRank = (s) => ADDRESS_SEVERITIES.indexOf(s);
2755
2775
  LGTM_TOKEN = "LGTM:";
2756
2776
  CHANGES_TOKEN = "Changes requested:";
2757
2777
  NO_VERDICT_LINE = "No verdict submitted \u2014 not approving.";
@@ -2993,7 +3013,7 @@ function matchDispositions(findings, dispositions) {
2993
3013
  const dropped = [...new Set(dispositions.filter((d) => !issued.has(d.findingId)).map((d) => d.findingId))];
2994
3014
  return { matched, dropped };
2995
3015
  }
2996
- var MIN, PLAN_ID_PATTERN, PLAN_BRANCH, UNIT_REF, SLUG_MAX, INSTANCE_ID_MAX, ADDRESS_SEVERITIES, DEFAULT_ADDRESS_SEVERITY, isAddressSeverity, WAIT_MARGIN_MS, WAIT_CHUNK_MS, MERGE_WAIT_CHUNK_MS, BUSY_RETRY_MS;
3016
+ var MIN, PLAN_ID_PATTERN, PLAN_BRANCH, UNIT_REF, SLUG_MAX, INSTANCE_ID_MAX, WAIT_MARGIN_MS, WAIT_CHUNK_MS, MERGE_WAIT_CHUNK_MS, BUSY_RETRY_MS;
2997
3017
  var init_coordinator = __esm({
2998
3018
  "../../src/core/ship/coordinator.ts"() {
2999
3019
  "use strict";
@@ -3008,9 +3028,6 @@ var init_coordinator = __esm({
3008
3028
  UNIT_REF = /\bU(\d+)\s+to\s+U(\d+)\b|\bU\d+\b/g;
3009
3029
  SLUG_MAX = 24;
3010
3030
  INSTANCE_ID_MAX = 100;
3011
- ADDRESS_SEVERITIES = ["blocking", "major", "minor", "nit"];
3012
- DEFAULT_ADDRESS_SEVERITY = "minor";
3013
- isAddressSeverity = (v) => ADDRESS_SEVERITIES.includes(v);
3014
3031
  WAIT_MARGIN_MS = SHIP_WAIT.marginMinutes * MIN;
3015
3032
  WAIT_CHUNK_MS = SHIP_WAIT.chunkMinutes * MIN;
3016
3033
  MERGE_WAIT_CHUNK_MS = SHIP_WAIT.mergeChunkMinutes * MIN;
@@ -3019,12 +3036,6 @@ var init_coordinator = __esm({
3019
3036
  });
3020
3037
 
3021
3038
  // ../../src/core/shipPipeline.ts
3022
- function resolveAddressSeverity(layers) {
3023
- if (layers.run !== void 0) return { level: layers.run, source: "run" };
3024
- if (layers.user !== void 0) return { level: layers.user, source: "user" };
3025
- if (layers.channel !== void 0) return { level: layers.channel, source: "channel" };
3026
- return { level: layers.org ?? DEFAULT_ADDRESS_SEVERITY, source: "org" };
3027
- }
3028
3039
  function resolveGrant2(layers) {
3029
3040
  const scoped = layers.user !== void 0 ? { grant: layers.user, source: "user" } : layers.channel !== void 0 ? { grant: layers.channel, source: "channel" } : { grant: layers.org ?? DEFAULT_GRANT, source: "org" };
3030
3041
  if (layers.run === void 0) return scoped;
@@ -3052,7 +3063,7 @@ var init_shipPipeline = __esm({
3052
3063
  init_registry();
3053
3064
  init_budgets();
3054
3065
  init_coordinator();
3055
- init_coordinator();
3066
+ init_reviewVerdict();
3056
3067
  SHIP_DEFAULT_MAX_ROUNDS = 3;
3057
3068
  SHIP_DEFAULT_MAX_MINUTES = AGENTS.ship.maxMinutes;
3058
3069
  }
@@ -3189,7 +3200,7 @@ function boundedByParent(boundary, parentRemainingMs) {
3189
3200
  if (boundary?.maxMinutes && boundary.maxMinutes.value <= minutes) return boundary;
3190
3201
  return { ...boundary, maxMinutes: { value: minutes, scope: "parent" } };
3191
3202
  }
3192
- function effectiveProfile(preset, directives, boundary) {
3203
+ function effectiveProfile(preset, directives, boundary, minimum) {
3193
3204
  if (boundary?.maxIdentity && !identityWithin(preset.identity, boundary.maxIdentity.value)) {
3194
3205
  return {
3195
3206
  kind: "refused",
@@ -3222,6 +3233,12 @@ function effectiveProfile(preset, directives, boundary) {
3222
3233
  minutes = boundary.maxMinutes.value;
3223
3234
  boundedBy = boundary.maxMinutes.scope;
3224
3235
  }
3236
+ if (minimum !== void 0 && minutes < minimum) {
3237
+ return {
3238
+ kind: "refused",
3239
+ refusal: { axis: "minutes", needs: minimum, have: minutes, scope: boundedBy ?? "defaults" }
3240
+ };
3241
+ }
3225
3242
  return {
3226
3243
  kind: "profile",
3227
3244
  profile: {
@@ -3838,7 +3855,7 @@ function validateConfig(cfg) {
3838
3855
  for (const key of unknownKeys2(cfg, CONFIG_KEYS)) throw new Error(`config.yaml: unknown key \`${key}\``);
3839
3856
  validateScopeEfforts(cfg, "config.yaml");
3840
3857
  validateBoundaries(cfg, "config.yaml");
3841
- validateShipScopes(cfg, "config.yaml");
3858
+ validateScopeBlocks(cfg, "config.yaml");
3842
3859
  validateHarnessWords(cfg, "config.yaml");
3843
3860
  validateMcpServers(cfg, "config.yaml");
3844
3861
  if (typeof cfg.organization !== "string" || cfg.organization.trim() === "") {
@@ -3862,6 +3879,7 @@ function validateConfig(cfg) {
3862
3879
  if (cfg.runHistory !== void 0) validateRunHistory(cfg.runHistory);
3863
3880
  if (cfg.tracing !== void 0) validateTracing(cfg.tracing);
3864
3881
  validateRuntimeOverrides(cfg.runtimeOverrides);
3882
+ if (cfg.review !== void 0) validateReview(cfg.review);
3865
3883
  if (cfg.ship !== void 0) validateShip(cfg.ship);
3866
3884
  if (cfg.spawn !== void 0) validateSpawn(cfg.spawn);
3867
3885
  if (cfg.routing !== void 0) validateRouting(cfg.routing, cfg.providers);
@@ -3978,12 +3996,23 @@ function grantProblem(path, raw) {
3978
3996
  return `${path}.costCapUsd must be a positive number of dollars`;
3979
3997
  return void 0;
3980
3998
  }
3981
- function validateShipScopes(layer, source2) {
3999
+ function addressSeverityProblem(path, value) {
4000
+ return value !== void 0 && !isAddressSeverity(value) ? `${path} must be one of ${ADDRESS_SEVERITIES.join(", ")} (docs/reference/specs/agent-review.md item 5a)` : void 0;
4001
+ }
4002
+ function validateReview(review) {
4003
+ const problem = addressSeverityProblem("review.addressSeverity", review.addressSeverity);
4004
+ if (problem) throw new Error(`config.yaml: ${problem}`);
4005
+ }
4006
+ function validateScopeBlocks(layer, source2) {
3982
4007
  for (const [kind, scopes] of [
3983
4008
  ["channels", layer.channels],
3984
4009
  ["users", layer.users]
3985
4010
  ]) {
3986
4011
  for (const [id, scope] of Object.entries(scopes ?? {})) {
4012
+ const level = addressSeverityProblem(`${kind}.${id}.review.addressSeverity`, scope.review?.addressSeverity);
4013
+ if (level) throw new Error(`${source2}: ${level}`);
4014
+ if (scope.ship !== void 0 && "addressSeverity" in scope.ship)
4015
+ throw new Error(`${source2}: ${ADDRESS_SEVERITY_MOVED(`${kind}.${id}.ship.addressSeverity`)}`);
3987
4016
  const grant2 = scope.ship?.grant;
3988
4017
  if (grant2 === void 0) continue;
3989
4018
  const problem = grantProblem(`${kind}.${id}.ship.grant`, grant2);
@@ -3998,6 +4027,7 @@ function validateShip(ship) {
3998
4027
  throw new Error(
3999
4028
  "config.yaml: ship.coordinator is no longer a key \u2014 every agent:ship request runs on the plan runner; remove it (docs/reference/migrations.md)"
4000
4029
  );
4030
+ if (key === "addressSeverity") throw new Error(`config.yaml: ${ADDRESS_SEVERITY_MOVED("ship.addressSeverity")}`);
4001
4031
  throw new Error(`config.yaml: ship.${key} is not a known key`);
4002
4032
  }
4003
4033
  const rounds = ship.maxRounds;
@@ -4015,10 +4045,6 @@ function validateShip(ship) {
4015
4045
  throw new Error(
4016
4046
  `config.yaml: ship.maxMinutes ${pipeline.maxMinutes} cannot hold the loop ship.maxRounds ${pipeline.maxRounds} allows \u2014 ${held2.need} minutes are needed (${ALLOWANCES.provision} to provision, the coding child's ${ASKS.coding}, and the reserve for ${pipeline.maxRounds} review rounds at their floors); raise ship.maxMinutes or lower ship.maxRounds (docs/reference/specs/agent-ship.md item 8)`
4017
4047
  );
4018
- if (ship.addressSeverity !== void 0 && !isAddressSeverity(ship.addressSeverity))
4019
- throw new Error(
4020
- `config.yaml: ship.addressSeverity must be one of ${ADDRESS_SEVERITIES.join(", ")} (docs/reference/specs/agent-ship.md item 9)`
4021
- );
4022
4048
  if (ship.grant !== void 0) {
4023
4049
  const problem = grantProblem("ship.grant", ship.grant);
4024
4050
  if (problem) throw new Error(`config.yaml: ${problem}`);
@@ -4089,7 +4115,7 @@ function validateRuntimeOverrides(ro) {
4089
4115
  }
4090
4116
  }
4091
4117
  }
4092
- var MAX_INSTRUCTIONS_LENGTH, CONFIG_KEYS, SELF_IMPROVEMENT_KEYS, BOUNDARY_KEYS, MIN_BOUNDARY_MINUTES, PI_KEYS, PI_COMPACTION_KEYS, harnessWords, OPENCODE_KEYS, OPENCODE_COMPACTION_KEYS, ROUTE_ANSWER_MODES, REFERENCES_KEYS, ROUTING_KEYS, SHIP_KEYS, GRANT_KEYS, SPAWN_KEYS;
4118
+ var MAX_INSTRUCTIONS_LENGTH, CONFIG_KEYS, SELF_IMPROVEMENT_KEYS, BOUNDARY_KEYS, MIN_BOUNDARY_MINUTES, PI_KEYS, PI_COMPACTION_KEYS, harnessWords, OPENCODE_KEYS, OPENCODE_COMPACTION_KEYS, ROUTE_ANSWER_MODES, REFERENCES_KEYS, ROUTING_KEYS, SHIP_KEYS, GRANT_KEYS, ADDRESS_SEVERITY_MOVED, SPAWN_KEYS;
4093
4119
  var init_validate = __esm({
4094
4120
  "../../src/config/validate.ts"() {
4095
4121
  "use strict";
@@ -4161,10 +4187,10 @@ var init_validate = __esm({
4161
4187
  SHIP_KEYS = {
4162
4188
  maxRounds: true,
4163
4189
  maxMinutes: true,
4164
- addressSeverity: true,
4165
4190
  grant: true
4166
4191
  };
4167
4192
  GRANT_KEYS = { renewals: true, costCapUsd: true };
4193
+ ADDRESS_SEVERITY_MOVED = (path) => `${path} moved to ${path.replace(/ship\.addressSeverity$/, "review.addressSeverity")} \u2014 the severity to address now gates every review's verdict, not ship alone (docs/reference/migrations.md)`;
4168
4194
  SPAWN_KEYS = { maxChildren: true };
4169
4195
  }
4170
4196
  });
@@ -4443,7 +4469,7 @@ var init_config2 = __esm({
4443
4469
  validateInstructions(doc, `overrides (${this.backing.describe()})`);
4444
4470
  validateScopeEfforts(doc, `overrides (${this.backing.describe()})`);
4445
4471
  validateBoundaries(doc, `overrides (${this.backing.describe()})`);
4446
- validateShipScopes(doc, `overrides (${this.backing.describe()})`);
4472
+ validateScopeBlocks(doc, `overrides (${this.backing.describe()})`);
4447
4473
  validateHarnessWords(doc, `overrides (${this.backing.describe()})`);
4448
4474
  validateMcpServers(
4449
4475
  { channels: doc.channels, users: doc.users, defaults: doc.org },
@@ -9159,6 +9185,7 @@ var init_runEvents = __esm({
9159
9185
  RUN_NOTE_KINDS = [
9160
9186
  "wrap_up",
9161
9187
  "time_budget_exhausted",
9188
+ "tool_cut",
9162
9189
  "turn_budget_exhausted",
9163
9190
  "sandbox_dead",
9164
9191
  "fleet_busy",
@@ -9177,6 +9204,7 @@ var init_runEvents = __esm({
9177
9204
  "cold_sandbox",
9178
9205
  "rebind_refused",
9179
9206
  "work_left_behind",
9207
+ "workspace_torn_down",
9180
9208
  "pr_not_opened",
9181
9209
  "review_not_posted",
9182
9210
  "compacted",
@@ -9185,6 +9213,7 @@ var init_runEvents = __esm({
9185
9213
  "tool_refused",
9186
9214
  "settle_set_aside",
9187
9215
  "tool_unnamed",
9216
+ "directory_reached",
9188
9217
  "budget_salvage",
9189
9218
  "stuck_loop"
9190
9219
  ];
@@ -9595,6 +9624,9 @@ function execDeadline(timeoutMs, signal2) {
9595
9624
  signal2.addEventListener("abort", onStop, { once: true });
9596
9625
  return AbortSignal.any([deadline.signal, signal2]);
9597
9626
  }
9627
+ function isRunStopError(err2) {
9628
+ return err2 instanceof ExecInfraError && err2.reason === "aborted";
9629
+ }
9598
9630
  function infraMayClear(err2) {
9599
9631
  return EXEC_INFRA_WAITABLE.has(err2.reason);
9600
9632
  }
@@ -10433,7 +10465,7 @@ var init_residentRefresh = __esm({
10433
10465
  "use strict";
10434
10466
  init_residentDisk();
10435
10467
  RUNTIME_MOVED_WORDING = /previous runtime incarnation|interrupted because the runtime changed|runtime identity is no longer active|sandbox lifetime is no longer current|platform was updating the sandbox runtime|no longer identifies pid/i;
10436
- STOPPED_CONTAINER_WORDING = /process supervisor is closed|container is not running, consider calling start/i;
10468
+ STOPPED_CONTAINER_WORDING = /process supervisor is closed|container is not running, consider calling start|the container just exited/i;
10437
10469
  RUNTIME_REPLACEMENT_WORDING = new RegExp(
10438
10470
  `${RUNTIME_MOVED_WORDING.source}|${STOPPED_CONTAINER_WORDING.source}`,
10439
10471
  "i"
@@ -10655,34 +10687,45 @@ function residentAnswerReason(status3, data) {
10655
10687
  function answeredStatus(httpStatus, data) {
10656
10688
  return httpStatus === 200 && typeof data.error === "string" && typeof data.status === "number" ? data.status : httpStatus;
10657
10689
  }
10690
+ function isTransientRefusal(answer) {
10691
+ return answer.status >= 500 && answer.status <= 599 && answer.data.transient === true;
10692
+ }
10658
10693
  function lifecycleReasonOf(data) {
10659
10694
  if (typeof data.stateReason === "string") return data.stateReason;
10660
10695
  if (typeof data.reason === "string" && !ANSWER_OWN_WORDS.has(data.reason)) return data.reason;
10661
10696
  return "";
10662
10697
  }
10663
- function wakeStrikeReason(last) {
10698
+ function wakeStrikeReason(last, transient = false) {
10699
+ if (transient && isUnansweredProbe(last)) return "worker-unavailable";
10664
10700
  return last.kind === "status" && isWakeable(last.state, last.reason) ? "worker-unavailable" : "refused";
10665
10701
  }
10666
- function residentWakeBudgetStrike(route3, refusal, spentMs, last) {
10702
+ function isUnansweredProbe(view) {
10703
+ return view.kind === "unreachable" && (view.transport || view.status !== void 0 && view.status >= 500);
10704
+ }
10705
+ function residentWakeBudgetStrike(route3, refusal, spentMs, last, wait = {}) {
10706
+ const origin = wait.origin ?? "container-exited";
10707
+ const transient = wait.transient ?? origin === "transient-refusal";
10708
+ const waitedFor = origin === "container-exited" ? "to wake" : "to come back";
10667
10709
  return residentWakeStrike(
10668
10710
  route3,
10669
10711
  refusal,
10670
- `waited ${Math.round(spentMs / 1e3)}s for the resident to wake (last seen ${describeProbe(last)}) and gave up`,
10671
- wakeStrikeReason(last)
10712
+ `waited ${Math.round(spentMs / 1e3)}s for the resident ${waitedFor} (last seen ${describeProbe(last)}) and gave up`,
10713
+ wakeStrikeReason(last, transient),
10714
+ origin
10672
10715
  );
10673
10716
  }
10674
- function residentWakeStrike(route3, refusal, why, reason2) {
10717
+ function residentWakeStrike(route3, refusal, why, reason2, origin = "container-exited") {
10675
10718
  return classifyError(new ExecInfraError(`resident ${route3}: ${refusal}; ${why}`, reason2), {
10676
10719
  kind: "infra",
10677
- code: "container-exited"
10720
+ code: origin
10678
10721
  });
10679
10722
  }
10680
- function wakePause(ms, signal2) {
10723
+ function wakePause(ms, signal2, route3) {
10681
10724
  return new Promise((resolve13, reject) => {
10682
- if (signal2?.aborted) return reject(wakeStopped());
10725
+ if (signal2?.aborted) return reject(wakeStopped(route3));
10683
10726
  const onAbort = () => {
10684
10727
  clearTimeout(timer);
10685
- reject(wakeStopped());
10728
+ reject(wakeStopped(route3));
10686
10729
  };
10687
10730
  const timer = setTimeout(() => {
10688
10731
  signal2?.removeEventListener("abort", onAbort);
@@ -10756,7 +10799,10 @@ var init_resident = __esm({
10756
10799
  init_executor();
10757
10800
  DETACH_TIMEOUT_MS = 1e4;
10758
10801
  ANSWER_OWN_WORDS = /* @__PURE__ */ new Set(["mirror-busy", DISK_PRESSURE_REASON, "image-stale"]);
10759
- wakeStopped = () => new Error("stopped waiting for the resident to wake: the run was stopped");
10802
+ wakeStopped = (route3) => classifyError(
10803
+ new ExecInfraError(`resident ${route3}: stopped waiting for the resident to wake: the run was stopped`, "aborted"),
10804
+ { kind: "transport" }
10805
+ );
10760
10806
  ResidentNeedsRefError = class extends Error {
10761
10807
  /** The resident's default branch when the Worker names one in the 409 body:
10762
10808
  * the factory binds the thread to it (loudly) instead of asking. Undefined
@@ -10772,6 +10818,11 @@ var init_resident = __esm({
10772
10818
  resource;
10773
10819
  defaultRef;
10774
10820
  needs = "ref";
10821
+ /** How long the wake wait ran before this refusal, when the attach met one
10822
+ * (a transient refusal cleared, then the resident asked for a ref): the
10823
+ * factory's retry by default draws on one budget across both attaches and
10824
+ * the card names the total. */
10825
+ wokeAfterMs;
10775
10826
  };
10776
10827
  ResidentReuseRefusedError = class extends Error {
10777
10828
  constructor(resource, why) {
@@ -10817,7 +10868,13 @@ var init_resident = __esm({
10817
10868
  const data = await parseResidentBody(res);
10818
10869
  const err2 = typeof data.error === "string" ? data.error : "";
10819
10870
  if (err2.startsWith("op-refused")) return { kind: "refused", reason: err2 };
10820
- if (err2) return { kind: "error", message: `resident /op: ${err2}` };
10871
+ if (err2) {
10872
+ return {
10873
+ kind: "error",
10874
+ message: `resident /op: ${err2}`,
10875
+ ...data.transient === true ? { transient: true } : {}
10876
+ };
10877
+ }
10821
10878
  if (!res.ok) return { kind: "error", message: `resident /op HTTP ${res.status}` };
10822
10879
  const ok2 = data.ok === true;
10823
10880
  const summary = typeof data.summary === "string" && data.summary ? data.summary : `${op} ${ok2 ? "succeeded" : "failed"}`;
@@ -10888,7 +10945,13 @@ var init_resident = __esm({
10888
10945
  }
10889
10946
  const data = sanitizeResidentBody(await res.json().catch(() => ({})));
10890
10947
  if (!res.ok) {
10891
- return { kind: "unreachable", error: `probe HTTP ${res.status}: ${String(data.error ?? "")}`, transport: false };
10948
+ return {
10949
+ kind: "unreachable",
10950
+ error: `probe HTTP ${res.status}: ${String(data.error ?? "")}`,
10951
+ transport: false,
10952
+ status: res.status,
10953
+ ...res.status >= 500 && data.transient === true ? { transient: true } : {}
10954
+ };
10892
10955
  }
10893
10956
  const seed = seedHandleOf(data.snapshot);
10894
10957
  return {
@@ -10898,6 +10961,31 @@ var init_resident = __esm({
10898
10961
  ...seed ? { seed } : {}
10899
10962
  };
10900
10963
  }
10964
+ /** The selection probe, waited through a blip the Worker typed transient
10965
+ * (execution.md item 9): a 5xx carrying `transient: true` — the Durable
10966
+ * Object reset or lost under the probe, which the attach's own wait would
10967
+ * have waited through a moment later — is re-probed every `WAKE_POLL_MS`
10968
+ * under `budgetMs`, the run's stop ending the wait at once with its typed
10969
+ * error. Starts from the view the caller already has (`first`), re-probes
10970
+ * at once — a blip the Durable Object has already recovered from costs no
10971
+ * pause, as the wake wait's first view costs none — then every poll; answers
10972
+ * the last view and how long was waited. A transport failure or any other
10973
+ * answer is returned at once — untyped, it is the caller's outage breaker's
10974
+ * case, never a wait. */
10975
+ static async probeStatusThroughBlip(baseUrl, token2, resource, timeoutMs, first, wait) {
10976
+ const t0 = systemClock();
10977
+ let probe = first;
10978
+ let reprobed = false;
10979
+ for (; ; ) {
10980
+ if (probe.kind !== "unreachable" || !probe.transient) return { probe, waitedMs: systemClock() - t0 };
10981
+ const spent = systemClock() - t0;
10982
+ if (spent >= wait.budgetMs) return { probe, waitedMs: spent };
10983
+ if (reprobed) await wakePause(Math.min(WAKE_POLL_MS, wait.budgetMs - spent), wait.signal, "/status");
10984
+ reprobed = true;
10985
+ probe = await _ResidentExecutor.probeStatus(baseUrl, token2, resource, timeoutMs, wait.span, wait.signal);
10986
+ if (wait.signal?.aborted) throw wakeStopped("/status");
10987
+ }
10988
+ }
10901
10989
  /** The one held request while the resident restores (item 27): POST
10902
10990
  * /await-restore, which the resident's Durable Object holds until its state
10903
10991
  * leaves `restoring` and the transition publishes — event-driven, no
@@ -10972,10 +11060,32 @@ var init_resident = __esm({
10972
11060
  * request and the resident moves a default-bound thread onto it (item 16;
10973
11061
  * `rebound` / `rebindRefused` say which) — and the sha the worktree is at. A
10974
11062
  * 200 without both fields is a malformed resident (the attach contract
10975
- * always carries them) and is an error, never a half-bound executor. */
10976
- async attach(span) {
10977
- const answer = await this.attachOnce(span);
11063
+ * always carries them) and is an error, never a half-bound executor. A
11064
+ * refusal the Worker typed as the platform's transient (`isTransientRefusal`:
11065
+ * the Durable Object reset or lost under the attach) is waited through
11066
+ * here, for the run's first attach and every re-attach alike: the wake wait
11067
+ * (item 65) probes `/status`, re-attaches as soon as the engine says it
11068
+ * serves, and is bounded by `opts.budgetMs` under the wake ceiling; past the
11069
+ * budget it throws the wake's strike (`worker-unavailable`), so a caller
11070
+ * with a longer clock may still wait and the factory falls back cold naming
11071
+ * the wait. A deterministic refusal is judged at once, with no probe.
11072
+ * `attachTimeoutMs` is how long one attach request may take — the exec
11073
+ * default when absent (a first attach or a head move may clone and install
11074
+ * deps); an operation passes its own call bound, so its recovery never
11075
+ * waits past the operation's wall clock. */
11076
+ async attach(span, opts = {}) {
11077
+ const answer = await this.attachOnce(span, opts.attachTimeoutMs, opts.signal);
10978
11078
  if (answer.ok) return answer.binding;
11079
+ if (isTransientRefusal(answer)) {
11080
+ const woke = await this.awaitWake("/attach", String(answer.data.error), {
11081
+ origin: "transient-refusal",
11082
+ signal: opts.signal,
11083
+ budgetMs: opts.budgetMs,
11084
+ attachTimeoutMs: opts.attachTimeoutMs,
11085
+ span
11086
+ });
11087
+ return { ...woke.binding, wokeAfterMs: woke.waitedMs };
11088
+ }
10979
11089
  throw this.attachRefusal(answer);
10980
11090
  }
10981
11091
  /** One `/attach`, answered rather than thrown: the binding on a 200, else
@@ -11036,7 +11146,7 @@ var init_resident = __esm({
11036
11146
  if (status3 === 409 && data.needs === "recreate")
11037
11147
  return traced(new ResidentReuseRefusedError(this.opts.resource, err2));
11038
11148
  if (status3 === 404) return traced(new Error(`resident attach: ${this.opts.resource} is not onboarded (${err2})`));
11039
- if (status3 >= 500 && status3 <= 599 && data.transient === true) {
11149
+ if (isTransientRefusal(answer)) {
11040
11150
  return traced(
11041
11151
  classifyError(
11042
11152
  new ExecInfraError(`resident attach failed for ${this.opts.resource}: ${err2}`, "worker-unavailable"),
@@ -11052,11 +11162,13 @@ var init_resident = __esm({
11052
11162
  * mechanism a re-review after a push uses, applied mid-run. The move's own
11053
11163
  * attach carries the new sha; a later recovery re-attach names none (item
11054
11164
  * 51: the run's own pushes may move the tip). Answers the sha the worktree
11055
- * is at; throws like attach() on a refusal. */
11165
+ * is at; throws like attach() on a refusal. The run's stop (`opts.signal`)
11166
+ * rides into the attach and the wake wait a transient refusal begins, so a
11167
+ * stopped round never sits out the wake ceiling. */
11056
11168
  async moveTo(sha, opts) {
11057
11169
  this.opts = { ...this.opts, sha };
11058
11170
  this.shaPending = true;
11059
- const binding = await this.attach(opts?.span);
11171
+ const binding = await this.attach(opts?.span, { signal: opts?.signal });
11060
11172
  return { sha: binding.sha };
11061
11173
  }
11062
11174
  /** POST /detach: return this thread's pool user (and remove its worktree)
@@ -11125,10 +11237,27 @@ var init_resident = __esm({
11125
11237
  if (!CONTROL_RESET_REISSUE_ROUTES.has(route3) && saysControlReset(data))
11126
11238
  throw new ExecControlResetError(String(data.error).trim());
11127
11239
  };
11240
+ let waitStarted;
11241
+ const waitBudget = wakeWaitBudget(opts.waitBudgetMs);
11242
+ const waitLeft = () => {
11243
+ waitStarted ??= systemClock();
11244
+ return Math.max(0, waitBudget - (systemClock() - waitStarted));
11245
+ };
11246
+ const recoveryAttachTimeoutMs = Math.max(callTimeoutMs, BASH_TIMEOUT_MS);
11128
11247
  let r = await this.call(route3, body, callTimeoutMs, signal2, span);
11129
11248
  if (isContainerRolling(r.data.error)) {
11130
- const woke = await this.awaitWake(route3, String(r.data.error), { signal: signal2, budgetMs: opts.waitBudgetMs, span });
11131
- if (route3 === "/exec") throw new ExecSandboxRestartedError(sandboxRestartedMessage(woke), woke.waitedMs);
11249
+ const woke = await this.awaitWake(route3, String(r.data.error), {
11250
+ origin: "container-exited",
11251
+ signal: signal2,
11252
+ budgetMs: waitLeft(),
11253
+ attachTimeoutMs: callTimeoutMs,
11254
+ span
11255
+ });
11256
+ if (route3 === "/exec")
11257
+ throw new ExecSandboxRestartedError(
11258
+ sandboxRestartedMessage({ waitedMs: woke.waitedMs, ref: woke.binding.ref, sha: woke.binding.sha }),
11259
+ woke.waitedMs
11260
+ );
11132
11261
  r = await this.call(route3, body, callTimeoutMs, signal2, span);
11133
11262
  if (isContainerRolling(r.data.error)) {
11134
11263
  throw classifyError(
@@ -11143,21 +11272,21 @@ var init_resident = __esm({
11143
11272
  goneUnderThread(r.data);
11144
11273
  controlResetUnderThread(r.data);
11145
11274
  if (r.data.needs === "attach") {
11146
- await this.attach(span);
11275
+ await this.attach(span, { signal: signal2, budgetMs: waitLeft(), attachTimeoutMs: recoveryAttachTimeoutMs });
11147
11276
  r = await this.call(route3, body, callTimeoutMs, signal2, span);
11148
11277
  goneUnderThread(r.data);
11149
11278
  controlResetUnderThread(r.data);
11150
11279
  if (r.data.needs === "attach") throw stillGone(r.data);
11151
11280
  }
11152
11281
  if (saysControlReset(r.data)) {
11153
- await this.attach(span);
11282
+ await this.attach(span, { signal: signal2, budgetMs: waitLeft(), attachTimeoutMs: recoveryAttachTimeoutMs });
11154
11283
  r = await this.call(route3, body, callTimeoutMs, signal2, span);
11155
11284
  if (saysControlReset(r.data)) throw new ExecControlResetError(String(r.data.error).trim());
11156
11285
  if (r.data.needs === "attach") throw stillGone(r.data);
11157
11286
  }
11158
11287
  if (r.data.reason === "runtime-replaced") {
11159
11288
  this.noteRuntimeReplaced(route3, r.data);
11160
- await this.attach(span);
11289
+ await this.attach(span, { signal: signal2, budgetMs: waitLeft(), attachTimeoutMs: recoveryAttachTimeoutMs });
11161
11290
  r = await this.call(route3, body, callTimeoutMs, signal2, span);
11162
11291
  if (r.data.reason === "runtime-replaced") this.noteRuntimeReplaced(route3, r.data);
11163
11292
  if (r.data.needs === "attach") throw stillGone(r.data);
@@ -11185,13 +11314,28 @@ var init_resident = __esm({
11185
11314
  * the wait going). Answers the wait and the fresh binding. Throws the
11186
11315
  * strike (an `ExecInfraError` the tracker counts) when nothing is
11187
11316
  * recovering, when the resident goes down mid-wait, when a re-attach does
11188
- * not answer, or when the budget is spent; a hard stop rejects at once
11189
- * with a plain error the runner unwinds; any other attach refusal is the
11190
- * attach's own legible error. */
11317
+ * not answer, or when the budget is spent; a hard stop rejects at once with
11318
+ * the stop's one typed shape (`wakeStopped`: `aborted`, the transport) from
11319
+ * every stop point — the pause, the probe, the re-attach's own call; any
11320
+ * other attach refusal is the attach's own legible error.
11321
+ *
11322
+ * A wait a TRANSIENT refusal began (`opts.transient`, or a re-attach's
11323
+ * transient 500 met inside it) reads an unreachable Worker differently: the
11324
+ * Durable Object that reset or lost its connection under the attach loses
11325
+ * `/status` too, so an unanswered probe is the same blip, waited through
11326
+ * under the budget — never the definite "nothing says the container is
11327
+ * coming back" a container's exit is judged by — and a wait spent with the
11328
+ * Worker unreachable throughout ends in `worker-unavailable`, never
11329
+ * `refused`. */
11191
11330
  async awaitWake(route3, refusal, opts) {
11192
11331
  const t0 = systemClock();
11193
11332
  const waited = () => systemClock() - t0;
11194
- const definite = (why) => residentWakeStrike(route3, refusal, why, "refused");
11333
+ let transient = opts.origin === "transient-refusal";
11334
+ const definite = (why) => residentWakeStrike(route3, refusal, why, "refused", opts.origin);
11335
+ const decide = (view) => transient && isUnansweredProbe(view) ? {
11336
+ wait: true,
11337
+ why: `the resident Worker did not answer /status (${view.error}) after a transient refusal: the same blip, waited through`
11338
+ } : wakeDecision(view);
11195
11339
  const probe = async () => {
11196
11340
  const seen2 = await _ResidentExecutor.probeStatus(
11197
11341
  this.opts.baseUrl,
@@ -11201,37 +11345,47 @@ var init_resident = __esm({
11201
11345
  opts.span,
11202
11346
  opts.signal
11203
11347
  );
11204
- if (opts.signal?.aborted) throw wakeStopped();
11348
+ if (opts.signal?.aborted) throw wakeStopped(route3);
11205
11349
  return seen2;
11206
11350
  };
11207
11351
  let seen = await probe();
11208
- const decision = wakeDecision(seen);
11352
+ const decision = decide(seen);
11209
11353
  if (!decision.wait) throw definite(decision.why);
11210
11354
  const budget = wakeWaitBudget(opts.budgetMs);
11355
+ let firstView = true;
11211
11356
  for (; ; ) {
11357
+ if (seen.kind === "status" && isServiceable(seen.state, seen.reason) && (transient || !firstView)) {
11358
+ let answer;
11359
+ try {
11360
+ const bound = opts.attachTimeoutMs === void 0 ? Math.max(budget - waited(), BASH_TIMEOUT_MS) : Math.min(WAKE_WAIT_MAX_MS, Math.max(budget - waited(), opts.attachTimeoutMs));
11361
+ answer = await this.attachOnce(opts.span, bound, opts.signal);
11362
+ } catch (err2) {
11363
+ if (!(err2 instanceof ExecInfraError)) throw err2;
11364
+ if (err2.reason === "aborted") throw err2;
11365
+ throw residentWakeStrike(
11366
+ route3,
11367
+ refusal,
11368
+ `the re-attach after ${Math.round(waited() / 1e3)}s did not answer (${err2.message})`,
11369
+ wakeStrikeReason(seen, transient),
11370
+ opts.origin
11371
+ );
11372
+ }
11373
+ if (answer.ok) return { waitedMs: waited(), binding: answer.binding };
11374
+ if (isTransientRefusal(answer)) transient = true;
11375
+ else if (!isContainerRolling(answer.data.error)) {
11376
+ const refused7 = this.attachRefusal(answer);
11377
+ if (refused7 instanceof ResidentNeedsRefError) refused7.wokeAfterMs = waited();
11378
+ throw refused7;
11379
+ }
11380
+ }
11381
+ firstView = false;
11212
11382
  const spent = waited();
11213
- if (spent >= budget) throw residentWakeBudgetStrike(route3, refusal, spent, seen);
11214
- await wakePause(Math.min(WAKE_POLL_MS, budget - spent), opts.signal);
11383
+ if (spent >= budget)
11384
+ throw residentWakeBudgetStrike(route3, refusal, spent, seen, { origin: opts.origin, transient });
11385
+ await wakePause(Math.min(WAKE_POLL_MS, budget - spent), opts.signal, route3);
11215
11386
  seen = await probe();
11216
- const again = wakeDecision(seen);
11387
+ const again = decide(seen);
11217
11388
  if (!again.wait) throw definite(again.why);
11218
- if (seen.kind !== "status" || !isServiceable(seen.state, seen.reason)) continue;
11219
- let answer;
11220
- try {
11221
- answer = await this.attachOnce(opts.span, Math.max(budget - waited(), 1e3), opts.signal);
11222
- } catch (err2) {
11223
- if (!(err2 instanceof ExecInfraError)) throw err2;
11224
- if (err2.reason === "aborted") throw err2;
11225
- throw residentWakeStrike(
11226
- route3,
11227
- refusal,
11228
- `the re-attach after ${Math.round(waited() / 1e3)}s did not answer (${err2.message})`,
11229
- wakeStrikeReason(seen)
11230
- );
11231
- }
11232
- if (answer.ok) return { waitedMs: waited(), ref: answer.binding.ref, sha: answer.binding.sha };
11233
- if (answer.data.transient === true) continue;
11234
- if (!isContainerRolling(answer.data.error)) throw this.attachRefusal(answer);
11235
11389
  }
11236
11390
  }
11237
11391
  async exec(command2, opts) {
@@ -11489,6 +11643,19 @@ async function makeExecutor(opts, ctx, span) {
11489
11643
  if (!token2) throw new Error(`execution.resident is configured but ${tokenEnv} is not set`);
11490
11644
  const resource = repoResourceId(ctx.repo);
11491
11645
  let probe = await probeResident(resident, token2, resource, span);
11646
+ let probeWaitMs = 0;
11647
+ if (probe.kind === "unreachable" && probe.transient) {
11648
+ const through = await ResidentExecutor.probeStatusThroughBlip(
11649
+ resident.baseUrl,
11650
+ token2.reveal(),
11651
+ resource,
11652
+ resident.probeTimeoutMs ?? 2e3,
11653
+ probe,
11654
+ { budgetMs: FIRST_ATTACH_WAIT_MS, signal: ctx.stopSignal, span }
11655
+ );
11656
+ probe = through.probe;
11657
+ probeWaitMs = through.waitedMs;
11658
+ }
11492
11659
  let waitedForRestore = false;
11493
11660
  if (probe.kind === "status" && probe.state === "restoring") {
11494
11661
  const wait = await ResidentExecutor.awaitRestore(
@@ -11526,7 +11693,15 @@ async function makeExecutor(opts, ctx, span) {
11526
11693
  ...ctx.ownPr !== void 0 ? { ownPr: ctx.ownPr } : {}
11527
11694
  },
11528
11695
  nonWarm,
11529
- span
11696
+ span,
11697
+ // A refusal the Worker typed transient is waited through here, under
11698
+ // what the probe's wait left of the factory's own budget and the
11699
+ // run's stop (item 9); the card names both waits' total.
11700
+ {
11701
+ signal: ctx.stopSignal,
11702
+ budgetMs: Math.max(0, FIRST_ATTACH_WAIT_MS - probeWaitMs),
11703
+ waitedMs: probeWaitMs
11704
+ }
11530
11705
  );
11531
11706
  return waitedForRestore ? {
11532
11707
  ...selection,
@@ -11534,12 +11709,14 @@ async function makeExecutor(opts, ctx, span) {
11534
11709
  } : selection;
11535
11710
  } catch (err2) {
11536
11711
  if (err2 instanceof ResidentNeedsRefError) throw err2;
11712
+ if (isRunStopError(err2)) throw err2;
11537
11713
  failedAttach = residentTraceOf(err2);
11538
11714
  const wait = waitedForRestore ? " after waiting for the resident's restore" : "";
11539
11715
  reason2 = oneLine(`resident attach failed (${err2 instanceof Error ? err2.message : String(err2)})${wait}`);
11540
11716
  }
11541
11717
  } else if (probe.kind === "unreachable") {
11542
- reason2 = oneLine(`resident unreachable (${probe.error})`);
11718
+ const waited = probeWaitMs > 0 ? ` after waiting ${Math.round(probeWaitMs / 1e3)}s` : "";
11719
+ reason2 = oneLine(`resident unreachable (${probe.error})${waited}`);
11543
11720
  } else if (probe.state !== "not-onboarded") {
11544
11721
  const wait = waitedForRestore ? " after waiting for the resident's restore" : "";
11545
11722
  reason2 = oneLine(`resident ${probe.state}${probe.reason ? ` (${probe.reason})` : ""}${wait}`);
@@ -11623,10 +11800,24 @@ async function reattachWorkspace(opts, ctx, recorded, span) {
11623
11800
  const token2 = processSecrets.named(tokenEnv);
11624
11801
  if (!token2) throw new Error(`execution.resident is configured but ${tokenEnv} is not set`);
11625
11802
  const resource = repoResourceId(ctx.repo);
11626
- const probe = await probeResident(resident, token2, resource, span);
11627
- if (probe.kind === "unreachable") throw refuse3(`resident unreachable (${probe.error})`);
11803
+ let probe = await probeResident(resident, token2, resource, span);
11804
+ let probeWaitMs = 0;
11805
+ if (probe.kind === "unreachable" && probe.transient) {
11806
+ const through = await ResidentExecutor.probeStatusThroughBlip(
11807
+ resident.baseUrl,
11808
+ token2.reveal(),
11809
+ resource,
11810
+ resident.probeTimeoutMs ?? 2e3,
11811
+ probe,
11812
+ { budgetMs: FIRST_ATTACH_WAIT_MS, signal: ctx.stopSignal, span }
11813
+ );
11814
+ probe = through.probe;
11815
+ probeWaitMs = through.waitedMs;
11816
+ }
11817
+ const waitedNote = probeWaitMs > 0 ? ` after waiting ${Math.round(probeWaitMs / 1e3)}s` : "";
11818
+ if (probe.kind === "unreachable") throw refuse3(`resident unreachable (${probe.error})${waitedNote}`);
11628
11819
  if (!isServiceable(probe.state, probe.reason))
11629
- throw refuse3(`resident ${probe.state}${probe.reason ? ` (${probe.reason})` : ""}`);
11820
+ throw refuse3(`resident ${probe.state}${probe.reason ? ` (${probe.reason})` : ""}${waitedNote}`);
11630
11821
  const nonWarm = probe.state === "warm" ? void 0 : oneLine(`${probe.state}${probe.reason ? ` (${probe.reason})` : ""}`);
11631
11822
  const executor = new ResidentExecutor({
11632
11823
  baseUrl: resident.baseUrl,
@@ -11640,8 +11831,12 @@ async function reattachWorkspace(opts, ctx, recorded, span) {
11640
11831
  });
11641
11832
  let binding;
11642
11833
  try {
11643
- binding = await executor.attach(span);
11834
+ binding = await executor.attach(span, {
11835
+ signal: ctx.stopSignal,
11836
+ budgetMs: Math.max(0, FIRST_ATTACH_WAIT_MS - probeWaitMs)
11837
+ });
11644
11838
  } catch (err2) {
11839
+ if (isRunStopError(err2)) throw err2;
11645
11840
  throw refuse3(err2 instanceof Error ? err2.message : String(err2));
11646
11841
  }
11647
11842
  const same = (recordedValue, answered) => recordedValue === void 0 || answered === void 0 || recordedValue === answered;
@@ -11652,6 +11847,8 @@ async function reattachWorkspace(opts, ctx, recorded, span) {
11652
11847
  );
11653
11848
  }
11654
11849
  const where = `${resource.replace(/^repo:/, "")} \xB7 ${binding.ref}@${binding.sha.slice(0, 7)}`;
11850
+ const wokeSeconds = Math.round((probeWaitMs + (binding.wokeAfterMs ?? 0)) / 1e3);
11851
+ const woke = wokeSeconds > 0 ? ` \xB7 after waiting ${wokeSeconds}s for the resident` : "";
11655
11852
  return {
11656
11853
  executor,
11657
11854
  resident: true,
@@ -11659,21 +11856,25 @@ async function reattachWorkspace(opts, ctx, recorded, span) {
11659
11856
  ...binding.trace ? { trace: binding.trace } : {},
11660
11857
  ...binding.attachMs !== void 0 ? { attachMs: binding.attachMs } : {},
11661
11858
  binding,
11662
- note: nonWarm ? `resident ${nonWarm} \xB7 ${where} \xB7 re-attached to the run's worktree` : `resident \xB7 ${where} \xB7 re-attached to the run's worktree`
11859
+ note: nonWarm ? `resident ${nonWarm} \xB7 ${where} \xB7 re-attached to the run's worktree${woke}` : `resident \xB7 ${where} \xB7 re-attached to the run's worktree${woke}`
11663
11860
  };
11664
11861
  }
11665
- async function openResident(opts, nonWarm, span) {
11862
+ async function openResident(opts, nonWarm, span, wait = {}) {
11666
11863
  let executor = new ResidentExecutor(opts);
11667
11864
  let binding;
11668
11865
  let byDefault = false;
11669
11866
  try {
11670
- binding = await executor.attach(span);
11867
+ binding = await executor.attach(span, wait);
11671
11868
  } catch (err2) {
11672
11869
  if (!(err2 instanceof ResidentNeedsRefError) || !err2.defaultRef) throw err2;
11673
11870
  byDefault = true;
11871
+ const firstWaitMs = err2.wokeAfterMs ?? 0;
11872
+ const budgetLeft = wait.budgetMs === void 0 ? void 0 : Math.max(0, wait.budgetMs - firstWaitMs);
11674
11873
  executor = new ResidentExecutor({ ...opts, refHint: err2.defaultRef, refByDefault: true });
11675
11874
  try {
11676
- binding = await executor.attach(span);
11875
+ binding = await executor.attach(span, { ...wait, budgetMs: budgetLeft });
11876
+ const totalWaitMs = firstWaitMs + (binding.wokeAfterMs ?? 0);
11877
+ if (totalWaitMs > 0) binding = { ...binding, wokeAfterMs: totalWaitMs };
11677
11878
  } catch (again) {
11678
11879
  if (again instanceof ResidentNeedsRefError) {
11679
11880
  throw new Error(
@@ -11687,6 +11888,8 @@ async function openResident(opts, nonWarm, span) {
11687
11888
  const where = `${opts.resource.replace(/^repo:/, "")} \xB7 ${binding.ref}@${binding.sha.slice(0, 7)}`;
11688
11889
  const why = byDefault ? " (repo default \u2014 no branch named)" : "";
11689
11890
  const moved = rebindLabel(binding);
11891
+ const wokeSeconds = Math.round(((wait.waitedMs ?? 0) + (binding.wokeAfterMs ?? 0)) / 1e3);
11892
+ const woke = wokeSeconds > 0 ? ` \xB7 after waiting ${wokeSeconds}s for the resident` : "";
11690
11893
  return {
11691
11894
  executor,
11692
11895
  resident: true,
@@ -11694,7 +11897,7 @@ async function openResident(opts, nonWarm, span) {
11694
11897
  ...binding.trace ? { trace: binding.trace } : {},
11695
11898
  ...binding.attachMs !== void 0 ? { attachMs: binding.attachMs } : {},
11696
11899
  binding,
11697
- note: nonWarm ? `resident ${nonWarm} \xB7 ${where}${why}${moved} \u2014 attached to the last snapshot` : `resident \xB7 ${where}${why}${moved}`
11900
+ note: nonWarm ? `resident ${nonWarm} \xB7 ${where}${why}${moved} \u2014 attached to the last snapshot${woke}` : `resident \xB7 ${where}${why}${moved}${woke}`
11698
11901
  };
11699
11902
  }
11700
11903
  function rebindLabel(binding) {
@@ -11829,6 +12032,7 @@ var BACKENDS, WorkspaceReattachRefusedError, PROBE_OUTAGE_WINDOW_MS, AWAIT_RESTO
11829
12032
  var init_factory = __esm({
11830
12033
  "../../src/execution/factory.ts"() {
11831
12034
  "use strict";
12035
+ init_budgets();
11832
12036
  init_redact();
11833
12037
  init_residentTrace();
11834
12038
  init_executor();
@@ -14707,6 +14911,19 @@ function pushedBranchesOf(events) {
14707
14911
  }
14708
14912
  return [...byRef].map(([ref, pr]) => ({ ref, pr }));
14709
14913
  }
14914
+ function callsInFlight(events, status3) {
14915
+ const calls = /* @__PURE__ */ new Map();
14916
+ for (const e of events) {
14917
+ if (e.type === "tool_call" && e.callId !== void 0) {
14918
+ if (calls.get(e.callId)?.state !== "cut") calls.set(e.callId, { call: e, state: "open" });
14919
+ } else if (e.type === "tool_result" && e.callId !== void 0) {
14920
+ const seen = calls.get(e.callId);
14921
+ if (seen !== void 0 && seen.state !== "cut") seen.state = e.cut === true ? "cut" : "settled";
14922
+ }
14923
+ }
14924
+ const orderly = status3 === "completed" || status3 === "stopped_soft";
14925
+ return [...calls.values()].filter((c) => c.state === "cut" || c.state === "open" && !orderly).map((c) => c.call);
14926
+ }
14710
14927
  function routeOfEvents(events) {
14711
14928
  const meta = events.find((e) => e.type === "run_meta");
14712
14929
  if (meta?.type !== "run_meta" || meta.agentSource !== "route") return void 0;
@@ -16354,6 +16571,22 @@ var init_runDuration = __esm({
16354
16571
  }
16355
16572
  });
16356
16573
 
16574
+ // ../../src/core/authz/viewAs.ts
16575
+ function holdsAll(actor) {
16576
+ return actor.grants.actions === "all" && actor.grants.channels === "all" && actor.grants.repos === "all";
16577
+ }
16578
+ function isViewablePerson(id) {
16579
+ return /^slack:U[A-Z0-9]+$/.test(id);
16580
+ }
16581
+ function viewingRefusal(person) {
16582
+ return `You are viewing as ${person.name ?? person.id}; writes are your own to make \u2014 exit view-as to write.`;
16583
+ }
16584
+ var init_viewAs = __esm({
16585
+ "../../src/core/authz/viewAs.ts"() {
16586
+ "use strict";
16587
+ }
16588
+ });
16589
+
16357
16590
  // ../../src/mcp/config.ts
16358
16591
  function parseMcpSettings(raw) {
16359
16592
  if (raw === void 0 || raw === null) return void 0;
@@ -16661,6 +16894,7 @@ var init_commandRegistry = __esm({
16661
16894
  init_clock();
16662
16895
  init_formatDuration();
16663
16896
  init_authorize();
16897
+ init_viewAs();
16664
16898
  init_capabilities();
16665
16899
  init_untrusted();
16666
16900
  COMMAND_ID = /^[a-z][a-z0-9]*\.[a-z][a-z0-9]*$/;
@@ -16753,6 +16987,7 @@ var init_commandRegistry = __esm({
16753
16987
  callerKind: caller.kind,
16754
16988
  callerId: caller.id,
16755
16989
  ...caller.actor.asUser ? { asUser: caller.actor.asUser.id } : {},
16990
+ ...caller.actor.viewingAs ? { viewingAs: caller.actor.viewingAs.id } : {},
16756
16991
  effect: cmd.effect,
16757
16992
  outcome: res.ok ? "ok" : res.error,
16758
16993
  ...reason2 === void 0 ? {} : { reason: reason2 },
@@ -16760,6 +16995,8 @@ var init_commandRegistry = __esm({
16760
16995
  });
16761
16996
  return res;
16762
16997
  };
16998
+ if (caller.actor.viewingAs && cmd.effect !== "read")
16999
+ return done(fail("unauthorized", viewingRefusal(caller.actor.viewingAs)), "viewing");
16763
17000
  const decision = authorize(caller.actor, cmd.action, resourceOf(cmd, input, caller));
16764
17001
  if (!decision.allow)
16765
17002
  return done(fail("unauthorized", `${caller.id} is not allowed to run ${cmd.id}`), decision.reason);
@@ -17119,8 +17356,10 @@ var init_config4 = __esm({
17119
17356
  `per-agent harness: --harness.<agent> ${harnessWords2} \u2014 which process drives that agent's runs (under me, your own runs only)`
17120
17357
  ),
17121
17358
  boundary: boundaryOption,
17122
- ship: z6.object({
17123
- addressSeverity: z6.enum(ADDRESS_SEVERITIES).optional().describe("the severity agent:ship addresses before an approve stands (--ship.addressSeverity <level>)")
17359
+ review: z6.object({
17360
+ addressSeverity: z6.enum(ADDRESS_SEVERITIES).optional().describe(
17361
+ "the severity to address: a review's approve carrying a finding at or above it is a request_changes, and ship's rounds are held to it (--review.addressSeverity <level>)"
17362
+ )
17124
17363
  }).optional(),
17125
17364
  channel: channelOption
17126
17365
  }),
@@ -17159,7 +17398,8 @@ var init_config4 = __esm({
17159
17398
  if (options.effort !== void 0) patch.effort = options.effort;
17160
17399
  if (options.efforts) patch.efforts = options.efforts;
17161
17400
  if (options.harness) patch.harness = options.harness;
17162
- if (options.ship?.addressSeverity !== void 0) patch.ship = { addressSeverity: options.ship.addressSeverity };
17401
+ if (options.review?.addressSeverity !== void 0)
17402
+ patch.review = { addressSeverity: options.review.addressSeverity };
17163
17403
  if (options.boundary) {
17164
17404
  const { machines, ...fields } = options.boundary;
17165
17405
  const raw = { ...fields };
@@ -19520,7 +19760,7 @@ var init_help = __esm({
19520
19760
  init_docsLink();
19521
19761
  defineCommand10 = commandDefiner();
19522
19762
  COMMANDS_REFERENCE_URL = `${PROJECT_DOCS_URL}/reference/slack-commands`;
19523
- DIRECTIVES_HELP = "`agent:review model:anthropic/claude-opus-5 effort:low budget:20 look at PR 42` (effort: low | medium | high \u2014 lower = faster turns; budget: whole minutes, at least 2 \u2014 narrows this run's wall clock, never widens it)";
19763
+ DIRECTIVES_HELP = "`agent:review model:anthropic/claude-opus-5 effort:low budget:20 look at PR 42` (effort: low | medium | high \u2014 lower = faster turns; budget: whole minutes \u2014 narrows this run's wall clock, never widens it; a budget too short for a turn plus the write-up is refused by name)";
19524
19764
  helpShow = defineCommand10({
19525
19765
  id: "help.show",
19526
19766
  action: "help:read",
@@ -23029,7 +23269,10 @@ function defineOp(op) {
23029
23269
  `\`${args.slug}\` is not onboarded as a resident, so \`repo ${op}\` has nothing to run against \u2014 \`repo onboard ${args.slug}\` first, or ask the coding agent directly.`
23030
23270
  );
23031
23271
  case "error":
23032
- throw new CommandError("unavailable", result.message);
23272
+ throw new CommandError(
23273
+ "unavailable",
23274
+ result.transient === true ? `${result.message} \u2014 the resident was unavailable for a moment; re-run the command` : result.message
23275
+ );
23033
23276
  }
23034
23277
  }
23035
23278
  });
@@ -27585,10 +27828,10 @@ async function closeRestartRow(adopted, restart, why) {
27585
27828
  );
27586
27829
  }
27587
27830
  }
27588
- async function closeResumedRow(adopted, resume, why) {
27831
+ async function closeResumedRow(adopted, resume, why, status3 = "interrupted") {
27589
27832
  try {
27590
27833
  await adopted.sink.put(
27591
- reclaimedRunRecord({ row: resume.row, events: resume.events, status: "interrupted", finishedAt: systemClock() })
27834
+ reclaimedRunRecord({ row: resume.row, events: resume.events, status: status3, finishedAt: systemClock() })
27592
27835
  );
27593
27836
  } catch (err2) {
27594
27837
  console.warn(
@@ -31527,6 +31770,12 @@ function capped2(base, max) {
31527
31770
  if (n2 > max) ctx.addIssue({ code: z21.ZodIssueCode.custom, message: `at most ${max} visible characters (got ${n2})` });
31528
31771
  });
31529
31772
  }
31773
+ function cappedRaw(base, max) {
31774
+ return base.superRefine((s, ctx) => {
31775
+ const n2 = s.length;
31776
+ if (n2 > max) ctx.addIssue({ code: z21.ZodIssueCode.custom, message: `at most ${max} characters (got ${n2})` });
31777
+ });
31778
+ }
31530
31779
  function boundedArray(item, min, max, what) {
31531
31780
  return z21.array(item).superRefine((arr, ctx) => {
31532
31781
  if (arr.length < min)
@@ -31933,6 +32182,10 @@ var init_prDescription = __esm({
31933
32182
  "use strict";
31934
32183
  init_redact();
31935
32184
  PR_DESCRIPTION_CAPS = {
32185
+ /** The squash subject and the changelog line, the whole line counted: the
32186
+ * number the title gate (`scripts/check-pr-title.mjs`, `TITLE_MAX_LENGTH`)
32187
+ * holds it to; a test keeps the two equal. */
32188
+ title: 72,
31936
32189
  tldr: 300,
31937
32190
  why: 400,
31938
32191
  pointers: 7,
@@ -31971,8 +32224,9 @@ var init_prDescription = __esm({
31971
32224
  });
31972
32225
  PrDescriptionSchema = z21.object({
31973
32226
  /** The PR title's single source. Metadata for the PR's own title field —
31974
- * never rendered into the body (GitHub shows the title itself). */
31975
- title: line2,
32227
+ * never rendered into the body (GitHub shows the title itself) — and the
32228
+ * squash subject, so it is capped like every other field. */
32229
+ title: cappedRaw(line2, PR_DESCRIPTION_CAPS.title),
31976
32230
  tldr: capped2(prose, PR_DESCRIPTION_CAPS.tldr),
31977
32231
  why: capped2(prose, PR_DESCRIPTION_CAPS.why),
31978
32232
  pointers: boundedArray(PointerSchema, 1, PR_DESCRIPTION_CAPS.pointers, "pointers"),
@@ -32054,7 +32308,7 @@ var init_submit = __esm({
32054
32308
  submitVerdictTool = {
32055
32309
  name: "submit_verdict",
32056
32310
  failsInText: true,
32057
- description: "Record your review verdict. REQUIRED before your final message when reviewing a PR: `approve` when there are no blocking issues (nits alone are not blocking), `request_changes` otherwise. Switchboard writes the verdict as the first line of the GitHub comment itself (`LGTM:` only for approve); a review with no submitted verdict is posted as NOT approving. Call it once, after your analysis; a later call replaces the earlier one. `head` is the commit you reviewed \u2014 run `git rev-parse HEAD` in the checkout you read and tested and pass its output; Switchboard posts to the PR only if that commit IS the PR's head, so a review of the wrong branch can never land on a PR. Enumerate EVERY issue you report in `findings` with STABLE ids assigned in order (F1, F2, \u2026) \u2014 a fix round references findings by these ids, so never renumber them. Severity is exactly one of blocking|major|minor|nit; the entry carries the file (plus line when it points at one) and a one-line title, while the full explanation stays in your review text keyed by the same ids. An `approve` carrying a `blocking` finding is downgraded to `request_changes` \u2014 approve only when nothing blocking remains.",
32311
+ description: "Record your review verdict. REQUIRED before your final message when reviewing a PR: `approve` when no finding at or above the severity to address remains (the level in force for this run, `minor` by default: a major or minor finding means `request_changes`; nits alone never block), `request_changes` otherwise. Switchboard writes the verdict as the first line of the GitHub comment itself (`LGTM:` only for approve); a review with no submitted verdict is posted as NOT approving. Call it once, after your analysis; a later call replaces the earlier one. `head` is the commit you reviewed \u2014 run `git rev-parse HEAD` in the checkout you read and tested and pass its output; Switchboard posts to the PR only if that commit IS the PR's head, so a review of the wrong branch can never land on a PR. Enumerate EVERY issue you report in `findings` with STABLE ids assigned in order (F1, F2, \u2026) \u2014 a fix round references findings by these ids, so never renumber them. Severity is exactly one of blocking|major|minor|nit; the entry carries the file (plus line when it points at one) and a one-line title, while the full explanation stays in your review text keyed by the same ids. An `approve` carrying a finding at or above the severity to address is downgraded to `request_changes` and the ack names the finding and the level \u2014 approve only when every finding sits below it.",
32058
32312
  inputSchema: {
32059
32313
  type: "object",
32060
32314
  properties: {
@@ -32093,10 +32347,13 @@ var init_submit = __esm({
32093
32347
  required: ["verdict", "summary", "head"]
32094
32348
  },
32095
32349
  async run(input, ctx) {
32096
- const verdict = parseVerdictInput(input);
32350
+ const verdict = parseVerdictInput(input, {
32351
+ ...ctx.addressSeverity !== void 0 ? { addressSeverity: ctx.addressSeverity } : {}
32352
+ });
32097
32353
  if (!verdict) return "error: verdict must be exactly `approve` or `request_changes`";
32098
32354
  ctx.onVerdict?.(verdict);
32099
32355
  const notes = [];
32356
+ if (verdict.downgraded) notes.push(`downgraded from approve: ${downgradeNote(verdict.downgraded)}`);
32100
32357
  if (verdict.findings) notes.push(`${verdict.findings.length} finding${verdict.findings.length === 1 ? "" : "s"}`);
32101
32358
  if (verdict.droppedFindings?.length) notes.push(`dropped: ${verdict.droppedFindings.join("; ")}`);
32102
32359
  return notes.length ? `verdict recorded: ${verdict.verdict} (${notes.join("; ")})` : `verdict recorded: ${verdict.verdict}`;
@@ -32141,7 +32398,10 @@ var init_submit = __esm({
32141
32398
  inputSchema: {
32142
32399
  type: "object",
32143
32400
  properties: {
32144
- title: { type: "string", description: "The PR title \u2014 one line naming the change" },
32401
+ title: {
32402
+ type: "string",
32403
+ description: "The PR title \u2014 one line naming the change, `type(scope): what a reader can now do or expect`, at most 72 characters in all"
32404
+ },
32145
32405
  tldr: {
32146
32406
  type: "string",
32147
32407
  description: "Two sentences for a reader with zero context: what this PR does and why it matters (\u2264300 visible chars)"
@@ -33597,7 +33857,7 @@ function parseDirectives(input) {
33597
33857
  } else if (f.key === "severity") {
33598
33858
  if (!isAddressSeverity(f.value)) {
33599
33859
  throw new Error(
33600
- `Unknown severity "${f.value}". severity:<level> takes one of ${ADDRESS_SEVERITIES.join(", ")} \u2014 the level agent:ship addresses before an approve stands.`
33860
+ `Unknown severity "${f.value}". severity:<level> takes one of ${ADDRESS_SEVERITIES.join(", ")} \u2014 the severity to address: a review's approve carrying a finding at or above it is a request_changes.`
33601
33861
  );
33602
33862
  }
33603
33863
  out.severity = f.value;
@@ -33693,7 +33953,12 @@ function resolveProfile(ctx) {
33693
33953
  const carried = ctx.resume?.row.meta.profile;
33694
33954
  const declared = carried ? { machine: carried.machine, identity: carried.identity, maxMinutes: carried.minutes } : ctx.agent;
33695
33955
  const boundary = ctx.parentRemainingMs !== void 0 ? boundedByParent(ctx.resolved.boundary, ctx.parentRemainingMs) : ctx.resolved.boundary;
33696
- const resolution = effectiveProfile(declared, ctx.budget !== void 0 ? { budget: ctx.budget } : {}, boundary);
33956
+ const resolution = effectiveProfile(
33957
+ declared,
33958
+ ctx.budget !== void 0 ? { budget: ctx.budget } : {},
33959
+ boundary,
33960
+ leaseMinimum(ctx.agent.name)
33961
+ );
33697
33962
  if (resolution.kind === "profile" && carried?.boundedBy && !resolution.profile.boundedBy) {
33698
33963
  return { kind: "profile", profile: { ...resolution.profile, boundedBy: carried.boundedBy } };
33699
33964
  }
@@ -33730,6 +33995,7 @@ function repoVetFor(profile, resident) {
33730
33995
  var init_resolve = __esm({
33731
33996
  "../../src/core/dispatch/resolve.ts"() {
33732
33997
  "use strict";
33998
+ init_budgets();
33733
33999
  init_registry();
33734
34000
  init_profile();
33735
34001
  init_directives();
@@ -33953,7 +34219,7 @@ var init_digestCoverage = __esm({
33953
34219
 
33954
34220
  // ../../src/core/reviewRound.ts
33955
34221
  function releaseModeFor(identity2, opts) {
33956
- return identity2 === "read" || opts.hardStopped ? "always" : "if-idle";
34222
+ return identity2 === "read" || opts.hardStopped || opts.commandInFlight === true || opts.gateBypassed === true ? "always" : "if-idle";
33957
34223
  }
33958
34224
  async function attachRoundWorkspace(input) {
33959
34225
  const { agent, profile } = input.round;
@@ -33967,7 +34233,8 @@ async function attachRoundWorkspace(input) {
33967
34233
  ref: input.round.ref,
33968
34234
  headSha: input.round.headSha,
33969
34235
  ...input.round.ownPr !== void 0 ? { ownPr: input.round.ownPr } : {},
33970
- ...input.round.reattach !== void 0 ? { reattach: input.round.reattach } : {}
34236
+ ...input.round.reattach !== void 0 ? { reattach: input.round.reattach } : {},
34237
+ ...input.round.stopSignal !== void 0 ? { stopSignal: input.round.stopSignal } : {}
33971
34238
  },
33972
34239
  input.span
33973
34240
  );
@@ -34109,7 +34376,8 @@ async function settle(input, span) {
34109
34376
  let worktreeMoved = false;
34110
34377
  if (executor.moveTo) {
34111
34378
  try {
34112
- const at = normalizeHead((await executor.moveTo(current, trace)).sha);
34379
+ const moved = await executor.moveTo(current, { ...trace, signal: turn.control.hardSignal });
34380
+ const at = normalizeHead(moved.sha);
34113
34381
  worktreeMoved = at !== void 0 && sameCommit(at, current);
34114
34382
  console.log(
34115
34383
  `[review] ${logKey} worktree moved to ${at?.slice(0, 7) ?? "?"}${worktreeMoved ? "" : " (not the expected head)"}`
@@ -34313,7 +34581,14 @@ async function authorizeProfile(deps, ctx) {
34313
34581
  return { kind: "refused", reason: "profile_bounded" };
34314
34582
  }
34315
34583
  function refusalSummary(refusal) {
34316
- return refusal.axis === "identity" ? `identity ${refusal.needs} > ${refusal.cap} (${refusal.scope})` : `machine ${refusal.needs} not in ${refusal.allowed.join(",")} (${refusal.scopes.join(",")})`;
34584
+ switch (refusal.axis) {
34585
+ case "identity":
34586
+ return `identity ${refusal.needs} > ${refusal.cap} (${refusal.scope})`;
34587
+ case "machine":
34588
+ return `machine ${refusal.needs} not in ${refusal.allowed.join(",")} (${refusal.scopes.join(",")})`;
34589
+ case "minutes":
34590
+ return `minutes ${refusal.have} < ${refusal.needs} (${refusal.scope})`;
34591
+ }
34317
34592
  }
34318
34593
  function boundaryOf(scope) {
34319
34594
  switch (scope) {
@@ -34340,7 +34615,21 @@ function wayForward(scope, axis, needs, adminsHint) {
34340
34615
  case "directive":
34341
34616
  return "send the message again without the budget directive";
34342
34617
  case "parent":
34343
- return "spawn it from a run with more time left";
34618
+ return `spawn it from a run with at least ${needs} left`;
34619
+ }
34620
+ }
34621
+ function minutesWayForward(scope, needs, adminsHint) {
34622
+ switch (scope) {
34623
+ case "directive":
34624
+ return `send the message again with \`budget:${needs}\` or more`;
34625
+ case "user":
34626
+ return `raise your own boundary with \`config set me --boundary.maxMinutes ${needs}\`, or drop your overrides with \`config clear me\``;
34627
+ case "channel":
34628
+ return `run it in a channel whose boundary allows ${needs} minutes, or ask ${adminsHint} to raise this channel's boundary`;
34629
+ case "defaults":
34630
+ return `ask ${adminsHint} to raise \`defaults.boundary\` in the configuration`;
34631
+ case "parent":
34632
+ return `spawn it from a run with at least ${needs} minutes left`;
34344
34633
  }
34345
34634
  }
34346
34635
  function profileRefusalReply(agentName, refusal, adminsHint) {
@@ -34349,6 +34638,10 @@ function profileRefusalReply(agentName, refusal, adminsHint) {
34349
34638
  const how2 = wayForward(refusal.scope, "maxIdentity", refusal.needs, adminsHint);
34350
34639
  return `\u{1F6AB} \`${agentName}\` needs a \`${refusal.needs}\` credential; ${boundaryOf(refusal.scope)} caps runs at \`${refusal.cap}\`. ${capitalize(how2)}.`;
34351
34640
  }
34641
+ if (refusal.axis === "minutes") {
34642
+ const how2 = minutesWayForward(refusal.scope, refusal.needs, adminsHint);
34643
+ return `\u{1F6AB} \`${agentName}\` needs at least ${refusal.needs} minutes \u2014 a turn, then its write-up and post-step \u2014 and ${boundaryOf(refusal.scope)} gives it ${refusal.have}. ${capitalize(how2)}.`;
34644
+ }
34352
34645
  const scopes = refusal.scopes.map(boundaryOf);
34353
34646
  const who2 = scopes.length > 1 ? `${scopes.join(" and ")} allow` : `${scopes[0] ?? "the boundary"} allows`;
34354
34647
  const allowed2 = refusal.allowed.map((m) => `\`${m}\``).join(", ");
@@ -35225,7 +35518,7 @@ async function reserveRun(deps, ctx) {
35225
35518
  return void 0;
35226
35519
  }
35227
35520
  async function attachRound(deps, ctx) {
35228
- const { threadKey, agent, profile, repoCtx, root, clock: clock4, reattach } = ctx;
35521
+ const { threadKey, agent, profile, repoCtx, root, clock: clock4, reattach, stopSignal } = ctx;
35229
35522
  const ownPr = ownPrOf(repoCtx);
35230
35523
  return root.span("dispatch.workspace.attach", async (span) => {
35231
35524
  const graft = (steps, residentTotalMs) => graftResidentSteps(steps, {
@@ -35253,7 +35546,8 @@ async function attachRound(deps, ctx) {
35253
35546
  // The thread's own pull request, when the ref is its head (resident-
35254
35547
  // repos item 16): the one reason the resident may move a binding.
35255
35548
  ...ownPr !== void 0 ? { ownPr } : {},
35256
- ...reattach !== void 0 ? { reattach } : {}
35549
+ ...reattach !== void 0 ? { reattach } : {},
35550
+ ...stopSignal !== void 0 ? { stopSignal } : {}
35257
35551
  },
35258
35552
  logKey: threadKey,
35259
35553
  span
@@ -35273,11 +35567,12 @@ async function reattachWorkspace2(deps, ctx) {
35273
35567
  return { kind: "attached", round: await attachRound(deps, ctx) };
35274
35568
  } catch (err2) {
35275
35569
  if (err2 instanceof WorkspaceReattachRefusedError) return { kind: "reattach_refused", why: err2.why };
35570
+ if (isRunStopError(err2)) return { kind: "stopped" };
35276
35571
  throw err2;
35277
35572
  }
35278
35573
  }
35279
35574
  async function attachWorkspace(deps, ctx) {
35280
- const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, profile, repoCtx, root, reattach } = ctx;
35575
+ const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, profile, repoCtx, root, reattach, stopSignal } = ctx;
35281
35576
  let round2;
35282
35577
  try {
35283
35578
  round2 = await attachRound(deps, {
@@ -35287,7 +35582,8 @@ async function attachWorkspace(deps, ctx) {
35287
35582
  repoCtx,
35288
35583
  root,
35289
35584
  clock: clock4,
35290
- ...reattach !== void 0 ? { reattach } : {}
35585
+ ...reattach !== void 0 ? { reattach } : {},
35586
+ ...stopSignal !== void 0 ? { stopSignal } : {}
35291
35587
  });
35292
35588
  } catch (err2) {
35293
35589
  if (err2 instanceof ResidentNeedsRefError) {
@@ -35303,6 +35599,7 @@ async function attachWorkspace(deps, ctx) {
35303
35599
  return { kind: "refused", reason: "which_branch" };
35304
35600
  }
35305
35601
  if (err2 instanceof WorkspaceReattachRefusedError) return { kind: "reattach_refused", why: err2.why };
35602
+ if (isRunStopError(err2)) return { kind: "stopped" };
35306
35603
  throw err2;
35307
35604
  }
35308
35605
  return { kind: "attached", round: round2 };
@@ -35449,6 +35746,7 @@ var init_provision = __esm({
35449
35746
  init_actor();
35450
35747
  init_profile();
35451
35748
  init_factory();
35749
+ init_executor();
35452
35750
  init_residentTrace();
35453
35751
  init_resident();
35454
35752
  init_memory();
@@ -35679,7 +35977,7 @@ function openThroughSeam(harness, deps, run2) {
35679
35977
  }
35680
35978
  return harness.open(deps, run2);
35681
35979
  }
35682
- var isRecord4, relaunchesOf, isPiFacts, HarnessInterruptedError, HarnessMismatchError, RELAUNCH_CEILING, HarnessContainerReplacedError;
35980
+ var isRecord4, relaunchesOf, isPiFacts, HarnessInterruptedError, HarnessMismatchError, HarnessGateBypassedError, RELAUNCH_CEILING, HarnessContainerReplacedError;
35683
35981
  var init_contract4 = __esm({
35684
35982
  "../../src/core/harness/contract.ts"() {
35685
35983
  "use strict";
@@ -35710,6 +36008,12 @@ var init_contract4 = __esm({
35710
36008
  expected;
35711
36009
  found;
35712
36010
  };
36011
+ HarnessGateBypassedError = class extends Error {
36012
+ constructor(message4) {
36013
+ super(message4);
36014
+ this.name = "HarnessGateBypassedError";
36015
+ }
36016
+ };
35713
36017
  RELAUNCH_CEILING = 2;
35714
36018
  HarnessContainerReplacedError = class extends HarnessInterruptedError {
35715
36019
  constructor(message4, said2, was, now, record2, condition = "word") {
@@ -35766,8 +36070,10 @@ async function prepareRelaunch(deps, ctx) {
35766
36070
  repoCtx: ctx.repoCtx,
35767
36071
  root: ctx.root,
35768
36072
  clock: ctx.clock,
35769
- reattach: ctx.binding
36073
+ reattach: ctx.binding,
36074
+ ...ctx.stopSignal !== void 0 ? { stopSignal: ctx.stopSignal } : {}
35770
36075
  });
36076
+ if (reattached.kind === "stopped") return { kind: "stopped" };
35771
36077
  if (reattached.kind === "reattach_refused")
35772
36078
  return refuse3(
35773
36079
  `the run's workspace could not be re-attached in the replacement container (${reattached.why}); the run restarts from its request as a new run in this thread`,
@@ -35976,9 +36282,25 @@ async function identityOrNothing(container) {
35976
36282
  function saysContainerDown(err2) {
35977
36283
  return err2 instanceof Error && !isContainerGone(err2) && !RUNTIME_WORD.test(err2.message) && CONTAINER_DOWN_WORDING.test(err2.message);
35978
36284
  }
36285
+ function* selfAndCauses(err2) {
36286
+ const seen = /* @__PURE__ */ new Set();
36287
+ for (let e = err2; e !== void 0 && e !== null && !seen.has(e); e = e.cause) {
36288
+ seen.add(e);
36289
+ yield e;
36290
+ }
36291
+ }
36292
+ function infraErrorOf(err2) {
36293
+ for (const e of selfAndCauses(err2)) if (e instanceof ExecInfraError) return e;
36294
+ return void 0;
36295
+ }
35979
36296
  function saysTransportLost(err2) {
35980
36297
  if (!(err2 instanceof Error) || isContainerGone(err2) || RUNTIME_WORD.test(err2.message)) return false;
35981
36298
  if (err2 instanceof HarnessControlFileLostError) return false;
36299
+ const typed = infraErrorOf(err2);
36300
+ if (typed !== void 0) {
36301
+ if (infraMayClear(typed)) return true;
36302
+ if (typed.reason !== "answered") return false;
36303
+ }
35982
36304
  return err2 instanceof HarnessContainerDownError || CONTAINER_DOWN_WORDING.test(err2.message);
35983
36305
  }
35984
36306
  function isControlReset(err2) {
@@ -36141,7 +36463,7 @@ var init_container = __esm({
36141
36463
  }
36142
36464
  };
36143
36465
  CONTAINER_DOWN_WORDING = new RegExp(
36144
- `${STOPPED_CONTAINER_WORDING.source}|${CONTAINER_GONE_WORDING.source}|peer closed websocket|without sending close frame|\\bECONNRESET\\b|connection reset|socket hang up`,
36466
+ `${STOPPED_CONTAINER_WORDING.source}|${CONTAINER_GONE_WORDING.source}|peer closed websocket|without sending close frame|\\bECONNRESET\\b|connection reset|socket hang up|network connection lost`,
36145
36467
  "i"
36146
36468
  );
36147
36469
  HarnessContainerDownError = class extends HarnessContainerError {
@@ -36683,7 +37005,7 @@ function elapsedMinutes(ms) {
36683
37005
  if (minutes < 1) return "under a minute";
36684
37006
  return `${minutes} minute${minutes === 1 ? "" : "s"}`;
36685
37007
  }
36686
- var HARD_STOP_MESSAGE, CONTINUE_PROMPT, wrapUpNote, wrapUpInstruction, WRITE_UP_REQUEST, timeBudgetInstruction, turnGuardInstruction, SOFT_STOP_INSTRUCTION, turnGuardPace, MODEL_CALL_IN_FLIGHT, timeBudgetNote, turnGuardNote, softStopNote, hardStopNote, windDownFailureNote, finaleTimedOutNote, wrapUpUndeliveredNote, wrapUpWriteFailedNote, wrapUpName, finaleAbortReason, noWriteUp, timeBudgetAnswer, turnGuardAnswer, softStopAnswer;
37008
+ var HARD_STOP_MESSAGE, CONTINUE_PROMPT, wrapUpNote, wrapUpInstruction, WRITE_UP_REQUEST, timeBudgetInstruction, turnGuardInstruction, SOFT_STOP_INSTRUCTION, turnGuardPace, MODEL_CALL_IN_FLIGHT, timeBudgetNote, toolCutNote, turnGuardNote, softStopNote, hardStopNote, windDownFailureNote, finaleTimedOutNote, wrapUpUndeliveredNote, wrapUpWriteFailedNote, wrapUpName, finaleAbortReason, noWriteUp, timeBudgetAnswer, turnGuardAnswer, softStopAnswer;
36687
37009
  var init_windDown = __esm({
36688
37010
  "../../src/core/harness/windDown.ts"() {
36689
37011
  "use strict";
@@ -36698,6 +37020,7 @@ var init_windDown = __esm({
36698
37020
  turnGuardPace = (turns, elapsedMs) => `${turns} model turn${turns === 1 ? "" : "s"} in ${elapsedMinutes(elapsedMs)}`;
36699
37021
  MODEL_CALL_IN_FLIGHT = "a model call was in flight";
36700
37022
  timeBudgetNote = (doing) => doing ? `the loop's time is up while ${doing} \u2014 writing up findings so far inside the lease` : "the loop's time is up \u2014 writing up findings so far inside the lease";
37023
+ toolCutNote = (doing) => `the loop's end cut the command in flight (${doing}) so the write-up keeps its allowance`;
36701
37024
  turnGuardNote = (pace) => `turn guard fired: ${pace}, a pace that looks like a loop \u2014 writing up findings so far`;
36702
37025
  softStopNote = () => "soft stop \u2014 no further steps, writing up findings so far";
36703
37026
  hardStopNote = () => "hard stop \u2014 run aborted, no summary written";
@@ -37205,6 +37528,10 @@ var init_bridge2 = __esm({
37205
37528
  /** The calls under way: their span, their tool, and whether their end is
37206
37529
  * judged against the gate (harness-pi item 7). */
37207
37530
  openTools = /* @__PURE__ */ new Map();
37531
+ /** The open calls an abort was sent for (`markOpenCallsCut`): their end, when
37532
+ * pi answers it, is the abort's cut and not a settle — the result is marked
37533
+ * `cut`, and the workspace's release reads the command as one that may run on. */
37534
+ cutCalls = /* @__PURE__ */ new Set();
37208
37535
  /** The calls the gate saw — the extension asked `/harness/authorize` for
37209
37536
  * them (`gateSaw`) — not yet ended. */
37210
37537
  vetted = /* @__PURE__ */ new Set();
@@ -37364,6 +37691,7 @@ var init_bridge2 = __esm({
37364
37691
  const callId = str5(event.toolCallId);
37365
37692
  const open2 = this.openTools.get(callId);
37366
37693
  this.openTools.delete(callId);
37694
+ const cut = this.cutCalls.delete(callId);
37367
37695
  const tool = open2?.tool ?? str5(event.toolName);
37368
37696
  const isError = event.isError === true;
37369
37697
  const text = piResultText(event.result);
@@ -37376,7 +37704,8 @@ var init_bridge2 = __esm({
37376
37704
  callId,
37377
37705
  ...exit.exitCode !== void 0 ? { exitCode: exit.exitCode } : {},
37378
37706
  ...prepareToolResult(text),
37379
- ...open2?.span ? { spanId: open2.span.id } : {}
37707
+ ...open2?.span ? { spanId: open2.span.id } : {},
37708
+ ...cut ? { cut: true } : {}
37380
37709
  });
37381
37710
  open2?.span?.end(ok2 ? "ok" : "error", {
37382
37711
  callId,
@@ -37452,17 +37781,35 @@ var init_bridge2 = __esm({
37452
37781
  if (open2.length > 0) return `running ${open2.join(", ")}`;
37453
37782
  return this.modelCallOpen ? MODEL_CALL_IN_FLIGHT : void 0;
37454
37783
  }
37784
+ /** An abort is being sent to pi: every call open now is cut by it, not
37785
+ * settled — its end, when pi answers, lands marked `cut` (harness.md item 13:
37786
+ * the command behind it may run on, and the workspace's release reads it so). */
37787
+ markOpenCallsCut() {
37788
+ for (const callId of this.openTools.keys()) this.cutCalls.add(callId);
37789
+ }
37455
37790
  /** End whatever tool spans a stopped pi left open, so no span outlives the
37456
37791
  * run; `reason` is each call's result summary — one for all, or the note
37457
37792
  * each call is settled with (harness-pi item 16: the restart note, said of
37458
- * the container). */
37459
- closeOpenSpans(reason2) {
37793
+ * the container). `cut` marks each result when the caller is the session's
37794
+ * end, where pi is gone but its tree persists and a relayed command may run
37795
+ * on — the workspace's release reads it as in flight (harness.md item 13);
37796
+ * the container-replaced verdict leaves it unmarked, the old container's
37797
+ * calls gone and the relayed ones taken over by the relaunch. */
37798
+ closeOpenSpans(reason2, opts = {}) {
37460
37799
  for (const [callId, open2] of this.openTools) {
37461
37800
  open2.span?.end("error", { callId, ok: false });
37462
37801
  const summary = typeof reason2 === "string" ? reason2 : reason2({ callId, tool: open2.tool });
37463
- this.emit({ type: "tool_result", tool: open2.tool, ok: false, callId, summary: redactAndCap(summary) });
37802
+ this.emit({
37803
+ type: "tool_result",
37804
+ tool: open2.tool,
37805
+ ok: false,
37806
+ callId,
37807
+ summary: redactAndCap(summary),
37808
+ ...opts.cut ? { cut: true } : {}
37809
+ });
37464
37810
  }
37465
37811
  this.openTools.clear();
37812
+ this.cutCalls.clear();
37466
37813
  this.vetted.clear();
37467
37814
  }
37468
37815
  };
@@ -37529,54 +37876,48 @@ var init_transport = __esm({
37529
37876
  send(command2) {
37530
37877
  void this.write(command2);
37531
37878
  }
37532
- /** Send, and say how the write came out once the chain reaches it (harness-pi
37533
- * item 16): `landed` on pi's FIFO; `failed` — the reset's, the command kept
37534
- * as `pendingSend` for the re-attach to resolve; `held` — never attempted
37535
- * here, kept for `takeUnsent` and the fresh transport. What the gate's
37536
- * `onLanded` waits on: a clock stamped at the landing (the finale's) starts
37537
- * when pi has the steer, not when a chain that only held it was handed it. */
37879
+ /** Send, and say how the write came out once the chain reaches it (`Landing`,
37880
+ * harness-pi item 16). What the gate's `onLanded` waits on: a clock stamped
37881
+ * at the landing (the finale's) starts when pi has the steer, not when a
37882
+ * chain that only held it was handed it. A write to a closed transport is
37883
+ * `dropped`: nothing keeps it, so nothing will re-send it.
37884
+ *
37885
+ * An abort takes the same step with three differences (the one way in: the
37886
+ * gate passes it by type, `HeldSends.send`). It is never queued, so no
37887
+ * re-attach can replay it from `takeUnsent`; a spent chain does not stop it
37888
+ * — the reset that failed a prompt a moment after the abort was asked for
37889
+ * must not swallow the stop, and a teardown's abort still reaches pi past
37890
+ * an earlier failure; and its own failure is nobody's — the transport is
37891
+ * being left and the process ended by `kill`. Behind whatever is in flight,
37892
+ * never a second writer: a line is a separate exec into the FIFO and a line
37893
+ * over PIPE_BUF is several `write(2)`s. On a transport abandoned to a
37894
+ * re-attach by the time its step runs it is `dropped`, not held: never
37895
+ * queued, nothing re-sends it, and its sender is told — the pi harness
37896
+ * notes a dropped abort as its own error, so a stop swallowed there is
37897
+ * seen, never assumed. Nothing chains one there today: every abort's
37898
+ * sender in the pi harness is the loop or the turn, both suspended in the
37899
+ * recovery that abandons, and the OpenCode bridge never writes on its
37900
+ * transport. */
37538
37901
  write(command2) {
37539
- if (this.closed) return Promise.resolve("held");
37540
- if (command2.type === "abort") {
37541
- this.sendAbort();
37542
- return this.sending.then(() => "landed");
37543
- }
37902
+ if (this.closed) return Promise.resolve("dropped");
37903
+ const abort = command2.type === "abort";
37544
37904
  const line3 = JSON.stringify(command2);
37545
- this.queued.push(command2);
37905
+ if (!abort) this.queued.push(command2);
37546
37906
  const step = this.sending.then(() => {
37547
- if (this.sendError !== void 0 || this.heldForReattach) return "held";
37548
- this.queued.shift();
37907
+ if (this.heldForReattach) return abort ? "dropped" : "held";
37908
+ if (!abort && this.sendError !== void 0) return "held";
37909
+ if (!abort) this.queued.shift();
37549
37910
  return this.deps.container.writeLine(this.deps.paths, line3).then(() => "landed");
37550
37911
  }).catch((err2) => {
37551
- this.sendError ??= err2 instanceof Error ? err2 : new Error(String(err2));
37552
- this.pendingSend ??= command2;
37912
+ if (!abort) {
37913
+ this.sendError ??= err2 instanceof Error ? err2 : new Error(String(err2));
37914
+ this.pendingSend ??= command2;
37915
+ }
37553
37916
  return "failed";
37554
37917
  });
37555
37918
  this.sending = step.then(() => void 0);
37556
37919
  return step;
37557
37920
  }
37558
- /** The abort — a hard stop's, the finale's, a gate bypass's — the one write
37559
- * that never waits for the gate and never waits for a re-attach (harness-pi
37560
- * item 16): ONE step on the transport's chain that a spent chain does not
37561
- * stop. Nothing in flight — an idle chain, or one a failure already spent —
37562
- * and the step runs at once: that is the direct write. A write in flight or
37563
- * queued on a live chain, and the abort follows it: whichever prompt the
37564
- * stop was asked for during, pi has the prompt first and the abort after,
37565
- * never an idle session aborted and then handed the prompt to run unwatched.
37566
- * Never two writers: a line is a separate exec into the FIFO and a line over
37567
- * PIPE_BUF is several `write(2)`s, so a direct write beside an in-flight one
37568
- * could land inside pi's line and cost both. The step writes even when the
37569
- * write before it spent the chain (the reset that failed a prompt a moment
37570
- * after the abort was asked for must not swallow the stop), and skips only
37571
- * on a transport abandoned to a re-attach — its fresh transport is the one
37572
- * writer then; never queued for `takeUnsent`, so no re-attach replays it; a
37573
- * failed write is nobody's — the transport is being left, the process
37574
- * ended by `kill`. */
37575
- sendAbort() {
37576
- if (this.closed) return;
37577
- const line3 = JSON.stringify({ type: "abort" });
37578
- this.sending = this.sending.then(() => this.heldForReattach ? void 0 : this.deps.container.writeLine(this.deps.paths, line3)).catch(() => void 0);
37579
- }
37580
37921
  /** Whether the reader is at the log's end as far as it knows: every record
37581
37922
  * read has been handed out and the last read was short. The gate reads its
37582
37923
  * clock only then (`HeldSends.quiet`): a catch-up burst still being handed
@@ -37608,7 +37949,7 @@ var init_transport = __esm({
37608
37949
  /** Close for teardown: a write already queued still lands — unless an
37609
37950
  * earlier write failed, when it is held (`takeUnsent`) and this transport
37610
37951
  * delivers nothing more of the chain. An abort is the one exception: its
37611
- * step ignores the spent chain (`sendAbort`), so a teardown's abort still
37952
+ * step ignores the spent chain (`write`'s abort step), so a teardown's abort still
37612
37953
  * reaches pi even then; the process is ended by `kill` regardless, so
37613
37954
  * teardown owes nothing to a held write. */
37614
37955
  close() {
@@ -37716,7 +38057,7 @@ function resolveControlResetWrite(command2, echoed = () => false) {
37716
38057
  message: `a ${type} command was in flight when the resident's control plane reset under the run; its outcome is unknown and it has no echo to resolve it by, so the run cannot continue safely`
37717
38058
  };
37718
38059
  }
37719
- var MAX_INPLACE_REATTACHES, PROMPT_ECHO_WAIT_MS, WORD_ALIVE_REATTACH_NOTE, CONTROL_RESET_RESUMED_NOTE, RESEND_AS_IS, HeldSends;
38060
+ var MAX_INPLACE_REATTACHES, PROMPT_ECHO_WAIT_MS, WORD_ALIVE_REATTACH_NOTE, CONTROL_RESET_RESUMED_NOTE, RESEND_AS_IS, PASSES_HOLD, HeldSends;
37720
38061
  var init_reattach = __esm({
37721
38062
  "../../src/core/harness/reattach.ts"() {
37722
38063
  "use strict";
@@ -37727,6 +38068,7 @@ var init_reattach = __esm({
37727
38068
  WORD_ALIVE_REATTACH_NOTE = "the executor said replaced; the row's pi answers alive in this container; re-attached";
37728
38069
  CONTROL_RESET_RESUMED_NOTE = "the resident's control plane reset under the run (a deploy); the container and pi are unchanged; re-attached";
37729
38070
  RESEND_AS_IS = /* @__PURE__ */ new Set(["set_auto_retry", "get_state", "extension_ui_response"]);
38071
+ PASSES_HOLD = /* @__PURE__ */ new Set(["extension_ui_response", "abort"]);
37730
38072
  HeldSends = class {
37731
38073
  constructor(deliver) {
37732
38074
  this.deliver = deliver;
@@ -37747,13 +38089,14 @@ var init_reattach = __esm({
37747
38089
  return this.queue.length > 0;
37748
38090
  }
37749
38091
  /** Send now — or, while a prompt in doubt holds the gate, hold behind it in
37750
- * order. A gate reply never holds: the rule lives here, by the command's
37751
- * type, so no caller can get it wrong. `onLanded` runs when the write has
37752
- * settled at the transport — told `landed`, or `failed` with the reset —
37753
- * never at the hand-off to a chain that only holds it. */
38092
+ * order. A gate reply and an abort never hold (`PASSES_HOLD`): the rule
38093
+ * lives here, by the command's type, so no caller can get it wrong.
38094
+ * `onLanded` runs when the write has settled at the transport — told
38095
+ * `landed`, or `failed` with the reset — never at the hand-off to a chain
38096
+ * that only holds it, and never for a write nothing kept. */
37754
38097
  send(command2, onLanded) {
37755
38098
  if (onLanded) this.onLanded.set(command2, onLanded);
37756
- if (this.holding && command2.type !== "extension_ui_response") {
38099
+ if (this.holding && !PASSES_HOLD.has(String(command2.type))) {
37757
38100
  this.queue.push({ command: command2, state: "send" });
37758
38101
  return;
37759
38102
  }
@@ -37814,7 +38157,11 @@ var init_reattach = __esm({
37814
38157
  }
37815
38158
  /** Hand a write to the transport; its `onLanded`, if any, runs once the write
37816
38159
  * settled there. A `held` landing leaves the callback in place: the fresh
37817
- * transport's re-send of the same object (through `send`) runs it. */
38160
+ * transport's re-send of the same object (through `send`) runs it. Every
38161
+ * other landing is the write's end and the callback is told which —
38162
+ * `dropped` included: a closed transport kept nothing, no re-send will come,
38163
+ * and a clock or a label waiting on the write must give up now rather than
38164
+ * wait for ever. */
37818
38165
  dispatch(command2) {
37819
38166
  void this.deliver(command2).then((landing) => {
37820
38167
  if (landing === "held") return;
@@ -38314,7 +38661,8 @@ var init_tracingExecutor = __esm({
38314
38661
  const innerRelease = inner.release?.bind(inner);
38315
38662
  if (innerRelease) this.release = (mode) => this.timed("exec.release", (s) => innerRelease(mode, { span: s }));
38316
38663
  const innerMoveTo = inner.moveTo?.bind(inner);
38317
- if (innerMoveTo) this.moveTo = (sha) => this.timed("exec.move_to", (s) => innerMoveTo(sha, { span: s }));
38664
+ if (innerMoveTo)
38665
+ this.moveTo = (sha, opts) => this.timed("exec.move_to", (s) => innerMoveTo(sha, { ...opts, span: s }));
38318
38666
  const innerReadBytes = inner.readBytes?.bind(inner);
38319
38667
  if (innerReadBytes)
38320
38668
  this.readBytes = (path) => this.timed("exec.read_bytes", (s) => innerReadBytes(path, { span: s }));
@@ -38906,6 +39254,8 @@ async function runPiHarnessOpen(deps, run2) {
38906
39254
  let windingDown = "run";
38907
39255
  let hardStopped = false;
38908
39256
  let bypass;
39257
+ let abortSent = false;
39258
+ let settled = false;
38909
39259
  let replaced;
38910
39260
  const toolsBlocked = () => {
38911
39261
  if (!writeUp) return void 0;
@@ -38990,7 +39340,8 @@ async function runPiHarnessOpen(deps, run2) {
38990
39340
  sessionEnded = true;
38991
39341
  transport?.close();
38992
39342
  bridge.closeOpenSpans(
38993
- hardStopped ? "the run was hard-stopped" : bypass ? "the run was stopped: a tool call bypassed the gate" : "the run ended"
39343
+ hardStopped ? "the run was hard-stopped" : bypass ? "the run was stopped: a tool call bypassed the gate" : "the run ended",
39344
+ { cut: abortSent || !settled }
38994
39345
  );
38995
39346
  save();
38996
39347
  if (replaced !== void 0) return;
@@ -39168,14 +39519,21 @@ async function runPiHarnessOpen(deps, run2) {
39168
39519
  else if (run2.resume) sends.send({ id: ids.prompt, type: "prompt", message: CONTINUE_PROMPT });
39169
39520
  else sends.send({ id: ids.prompt, type: "prompt", ...promptOf(run2.messages) });
39170
39521
  let warned = false;
39171
- let settled = false;
39172
39522
  let stopMode;
39173
39523
  let finaleAborted = false;
39174
39524
  let writeUpFailed;
39525
+ const abortPi = () => {
39526
+ abortSent = true;
39527
+ bridge.markOpenCallsCut();
39528
+ sends.send({ type: "abort" }, (landing) => {
39529
+ if (landing === "dropped")
39530
+ note("harness_error", "the abort was dropped: no transport kept it, so pi was never told to stop");
39531
+ });
39532
+ };
39175
39533
  const hardStop = () => {
39176
39534
  if (hardStopped) return;
39177
39535
  hardStopped = true;
39178
- transport?.sendAbort();
39536
+ abortPi();
39179
39537
  };
39180
39538
  const startWriteUp = (kind, instruction) => {
39181
39539
  writeUp = kind;
@@ -39185,20 +39543,26 @@ async function runPiHarnessOpen(deps, run2) {
39185
39543
  const steer = { type: "steer", message: instruction };
39186
39544
  writeUpSteer = steer;
39187
39545
  sends.send(steer, (landing) => {
39546
+ if (writeUpSteer !== steer) return;
39188
39547
  if (landing === "landed") {
39189
39548
  writeUpAt = now();
39190
39549
  return;
39191
39550
  }
39192
- note("wrap_up", wrapUpWriteFailedNote(kind.kind, windingDown));
39193
- ask();
39551
+ if (landing === "failed") {
39552
+ note("wrap_up", wrapUpWriteFailedNote(kind.kind, windingDown));
39553
+ ask();
39554
+ return;
39555
+ }
39194
39556
  });
39195
39557
  };
39196
39558
  ask();
39197
39559
  };
39198
39560
  const dropHeld = (closes) => {
39199
39561
  const dropped = sends.dropHeld();
39200
- if (writeUp === void 0 || writeUpSteer === void 0 || !dropped.includes(writeUpSteer)) return;
39201
- note("wrap_up", wrapUpUndeliveredNote(writeUp.kind, closes));
39562
+ const undelivered = writeUp !== void 0 && writeUpSteer !== void 0 && dropped.includes(writeUpSteer) ? writeUp : void 0;
39563
+ writeUpSteer = void 0;
39564
+ if (undelivered === void 0) return;
39565
+ note("wrap_up", wrapUpUndeliveredNote(undelivered.kind, closes));
39202
39566
  clearWriteUp();
39203
39567
  };
39204
39568
  let steers = Promise.resolve();
@@ -39232,7 +39596,7 @@ async function runPiHarnessOpen(deps, run2) {
39232
39596
  note("follow_up", `staging the follow-up's files failed: ${redactSecrets(stagedLine)}`);
39233
39597
  }
39234
39598
  }
39235
- if (settled || loopEnded) {
39599
+ if (loopEnded) {
39236
39600
  run2.inbox?.requeue(inputs);
39237
39601
  return;
39238
39602
  }
@@ -39269,7 +39633,7 @@ ${stagedLine}` : prompt;
39269
39633
  writeUpAt = void 0;
39270
39634
  finaleAborted = true;
39271
39635
  run2.onProgress?.(finaleTimedOutNote());
39272
- transport.sendAbort();
39636
+ abortPi();
39273
39637
  }
39274
39638
  return;
39275
39639
  }
@@ -39280,8 +39644,13 @@ ${stagedLine}` : prompt;
39280
39644
  return;
39281
39645
  }
39282
39646
  if (now() >= loopEnd) {
39283
- note("time_budget_exhausted", timeBudgetNote(bridge.doingNow()));
39647
+ const doing = bridge.doingNow();
39648
+ note("time_budget_exhausted", timeBudgetNote(doing));
39284
39649
  startWriteUp({ kind: "time" }, timeBudgetInstruction());
39650
+ if (doing !== void 0 && doing.startsWith("running ")) {
39651
+ note("tool_cut", toolCutNote(doing));
39652
+ abortPi();
39653
+ }
39285
39654
  return;
39286
39655
  }
39287
39656
  if (bridge.turns >= run2.agent.maxTurns) {
@@ -39350,7 +39719,7 @@ ${stagedLine}` : prompt;
39350
39719
  if (now() >= until) {
39351
39720
  const msg = "the write in flight when the resident's control plane reset did not settle before the deadline; the run cannot continue";
39352
39721
  note("harness_error", msg);
39353
- transport.sendAbort();
39722
+ abortPi();
39354
39723
  throw new Error(msg, { cause: err2 });
39355
39724
  }
39356
39725
  }
@@ -39426,7 +39795,7 @@ ${stagedLine}` : prompt;
39426
39795
  if (obs.gateBypassed) {
39427
39796
  bypass = new GateBypassed(obs.gateBypassed.tool, obs.gateBypassed.callId);
39428
39797
  note("harness_error", `${bypass.message} \u2014 the run is stopped`);
39429
- transport.sendAbort();
39798
+ abortPi();
39430
39799
  break;
39431
39800
  }
39432
39801
  if (obs.response) {
@@ -39598,7 +39967,7 @@ ${stagedLine}` : prompt;
39598
39967
  writeUpAt = void 0;
39599
39968
  finaleAborted = true;
39600
39969
  run2.onProgress?.(finaleTimedOutNote("turn"));
39601
- transport.sendAbort();
39970
+ abortPi();
39602
39971
  }
39603
39972
  return;
39604
39973
  }
@@ -39666,7 +40035,7 @@ ${stagedLine}` : prompt;
39666
40035
  if (obs.gateBypassed) {
39667
40036
  bypass = new GateBypassed(obs.gateBypassed.tool, obs.gateBypassed.callId);
39668
40037
  note("harness_error", `${bypass.message} \u2014 the turn is stopped`);
39669
- transport.sendAbort();
40038
+ abortPi();
39670
40039
  break;
39671
40040
  }
39672
40041
  if (obs.response) noteEcho(obs.response);
@@ -39784,7 +40153,7 @@ var init_harness = __esm({
39784
40153
  this.name = "PromptRefused";
39785
40154
  }
39786
40155
  };
39787
- GateBypassed = class extends Error {
40156
+ GateBypassed = class extends HarnessGateBypassedError {
39788
40157
  constructor(tool, callId) {
39789
40158
  super(`the gate was bypassed: pi ran ${tool} (call ${callId}) without asking the bot`);
39790
40159
  this.name = "GateBypassed";
@@ -40824,7 +41193,9 @@ async function runLoop(deps, ctx) {
40824
41193
  });
40825
41194
  }
40826
41195
  let answer;
40827
- let verdict = typeof restored.verdict === "object" && restored.verdict !== null ? parseVerdictInput(restored.verdict) ?? void 0 : void 0;
41196
+ let verdict = typeof restored.verdict === "object" && restored.verdict !== null ? parseVerdictInput(restored.verdict, {
41197
+ addressSeverity: ctx.addressSeverity.level
41198
+ }) ?? void 0 : void 0;
40828
41199
  const onVerdict = (v) => {
40829
41200
  verdict = v;
40830
41201
  ledgerRun?.setState({ verdict: v });
@@ -40863,14 +41234,38 @@ async function runLoop(deps, ctx) {
40863
41234
  let reviewPost;
40864
41235
  let carried;
40865
41236
  let runFailed = false;
41237
+ let gateBypassed = false;
41238
+ let commandInFlight = false;
41239
+ const recordEvents = () => registry.snapshot(run2.id, run2.token)?.events ?? [];
40866
41240
  let failure2;
40867
41241
  let interrupted;
41242
+ const statusNow = () => {
41243
+ const stopped = run2.control.requested;
41244
+ return interrupted ? "interrupted" : runFailed ? "failed" : stopped === "hard" ? "stopped_hard" : stopped === "soft" ? "stopped_soft" : "completed";
41245
+ };
41246
+ const readCallsInFlight = () => {
41247
+ const calls = callsInFlight(recordEvents(), statusNow());
41248
+ commandInFlight = calls.length > 0;
41249
+ if (commandInFlight)
41250
+ registry.publish(run2.id, {
41251
+ type: "run_note",
41252
+ kind: "workspace_torn_down",
41253
+ summary: redactAndCap(
41254
+ `a command may still be running in the workspace, so it is torn down rather than paired: ${calls.map((c) => c.summary).join("; ")}`,
41255
+ ENDING_NOTE_MAX
41256
+ ),
41257
+ at: clock4()
41258
+ });
41259
+ };
40868
41260
  let runDiagnosis;
40869
41261
  let harnessSession;
40870
41262
  const releaseWorkspace = (span) => {
40871
- const pushed = pushedBranchesOf(registry.snapshot(run2.id, run2.token)?.events ?? []);
41263
+ const events = recordEvents();
41264
+ const pushed = pushedBranchesOf(events);
40872
41265
  return round2.release({
40873
41266
  hardStopped: run2.control.requested === "hard",
41267
+ commandInFlight,
41268
+ gateBypassed,
40874
41269
  ...span ? { span } : {},
40875
41270
  ...pushed.length > 0 ? { pushed } : {}
40876
41271
  });
@@ -40916,6 +41311,7 @@ async function runLoop(deps, ctx) {
40916
41311
  ...wait ? { wait } : {},
40917
41312
  ...session ? { session } : {},
40918
41313
  onVerdict,
41314
+ addressSeverity: ctx.addressSeverity.level,
40919
41315
  onDigest,
40920
41316
  onPrDescription,
40921
41317
  onHandoff,
@@ -41088,6 +41484,7 @@ async function runLoop(deps, ctx) {
41088
41484
  replaced: err2,
41089
41485
  facts: lastFacts,
41090
41486
  binding: workspaceBinding,
41487
+ stopSignal: run2.control.hardSignal,
41091
41488
  saveFacts
41092
41489
  });
41093
41490
  } catch (failed) {
@@ -41099,6 +41496,15 @@ async function runLoop(deps, ctx) {
41099
41496
  onEvent({ type: "run_note", kind: "sandbox_restarted", summary: decision.interruption.message });
41100
41497
  throw decision.interruption;
41101
41498
  }
41499
+ if (decision.kind === "stopped") {
41500
+ harnessDeps.registry.forget(run2.id);
41501
+ onEvent({
41502
+ type: "run_note",
41503
+ kind: "sandbox_restarted",
41504
+ summary: "the container was replaced under the run, and the run was stopped while its workspace was being re-attached in the replacement"
41505
+ });
41506
+ break;
41507
+ }
41102
41508
  if (decision.round !== void 0) {
41103
41509
  executor = decision.round.selection.executor;
41104
41510
  toolContext.executor = executor;
@@ -41112,7 +41518,7 @@ async function runLoop(deps, ctx) {
41112
41518
  harnessResume = decision.resume;
41113
41519
  }
41114
41520
  }
41115
- answer = harnessSession.answer;
41521
+ answer = harnessSession?.answer ?? HARD_STOP_MESSAGE;
41116
41522
  }
41117
41523
  if (isPrReview && repoCtx.repo && repoCtx.pr !== void 0 && run2.control.requested !== "hard" && postedBefore === void 0) {
41118
41524
  const settled = await settleReviewedHead({
@@ -41255,6 +41661,7 @@ async function runLoop(deps, ctx) {
41255
41661
  }
41256
41662
  }
41257
41663
  await harnessSession?.end();
41664
+ readCallsInFlight();
41258
41665
  const leftBehind = isCodingPrRun && run2.control.requested !== "hard" ? workLeftBehindOf({ uncommittedChanges: observedUncommitted, unpushedCommits: observedUnpushed }) : void 0;
41259
41666
  if (leftBehind) {
41260
41667
  registry.publish(run2.id, {
@@ -41328,14 +41735,16 @@ async function runLoop(deps, ctx) {
41328
41735
  if (err2 instanceof HarnessInterruptedError) interrupted = err2;
41329
41736
  else {
41330
41737
  runFailed = true;
41738
+ gateBypassed = err2 instanceof HarnessGateBypassedError;
41331
41739
  if (err2 instanceof ModelPolicyRefusedError) failure2 = { kind: "policy_refusal" };
41332
41740
  registry.publish(run2.id, {
41333
41741
  type: "run_note",
41334
41742
  kind: "run_failed",
41335
- summary: redactAndCap(err2 instanceof Error ? err2.message : String(err2), RUN_FAILED_NOTE_MAX)
41743
+ summary: redactAndCap(err2 instanceof Error ? err2.message : String(err2), ENDING_NOTE_MAX)
41336
41744
  });
41337
41745
  }
41338
41746
  await harnessSession?.end();
41747
+ readCallsInFlight();
41339
41748
  await root.span("post.workspace_release", (span) => releaseWorkspace(span));
41340
41749
  if (!interrupted) throw err2;
41341
41750
  return {
@@ -41347,8 +41756,7 @@ async function runLoop(deps, ctx) {
41347
41756
  };
41348
41757
  } finally {
41349
41758
  clearInterval(heartbeat);
41350
- const stopped = run2.control.requested;
41351
- const status3 = interrupted ? "interrupted" : runFailed ? "failed" : stopped === "hard" ? "stopped_hard" : stopped === "soft" ? "stopped_soft" : "completed";
41759
+ const status3 = statusNow();
41352
41760
  registry.finish(run2.id, status3);
41353
41761
  const snap = registry.snapshot(run2.id, run2.token);
41354
41762
  const events = snap?.events ?? [];
@@ -41447,7 +41855,7 @@ function unknownHarnessWordOf(state) {
41447
41855
  container: typeof container === "string" ? container : "unknown"
41448
41856
  };
41449
41857
  }
41450
- var RUN_FAILED_NOTE_MAX;
41858
+ var ENDING_NOTE_MAX;
41451
41859
  var init_runLoop = __esm({
41452
41860
  "../../src/core/dispatch/runLoop.ts"() {
41453
41861
  "use strict";
@@ -41490,7 +41898,7 @@ var init_runLoop = __esm({
41490
41898
  init_reply();
41491
41899
  init_staging();
41492
41900
  init_run2();
41493
- RUN_FAILED_NOTE_MAX = 500;
41901
+ ENDING_NOTE_MAX = 500;
41494
41902
  }
41495
41903
  });
41496
41904
 
@@ -42207,9 +42615,9 @@ async function runShipBranch(deps, msg, io, ctx) {
42207
42615
  }
42208
42616
  const scopes = deps.config.scopes(msg.channelId, msg.userId);
42209
42617
  const addressSeverity = resolveAddressSeverity({
42210
- org: deps.config.config.ship?.addressSeverity,
42211
- channel: scopes.channel.ship?.addressSeverity,
42212
- user: scopes.user.ship?.addressSeverity,
42618
+ org: deps.config.config.review?.addressSeverity,
42619
+ channel: scopes.channel.review?.addressSeverity,
42620
+ user: scopes.user.review?.addressSeverity,
42213
42621
  run: directives.severity
42214
42622
  });
42215
42623
  const grant2 = resolveGrant2({
@@ -42341,14 +42749,14 @@ var init_codingChild = __esm({
42341
42749
 
42342
42750
  // ../../src/core/dispatch/settle.ts
42343
42751
  function settleThread(deps, ctx) {
42344
- const { msg, admitted, runLoopStarted, control } = ctx;
42752
+ const { msg, admitted, stopCounts, control } = ctx;
42345
42753
  const admission = deps.admission ?? defaultAdmission;
42346
42754
  const released = admitted ? admission.release(msg.threadKey, admitted) : [];
42347
42755
  const pending2 = released.filter(fromPerson);
42348
42756
  const fromRuns = released.length - pending2.length;
42349
42757
  if (fromRuns > 0)
42350
42758
  console.log(`[dispatch] ${msg.threadKey} ${fromRuns} steer(s) from a parent run never read \u2014 not run fresh`);
42351
- const stopMode = runLoopStarted ? control?.requested : void 0;
42759
+ const stopMode = stopCounts ? control?.requested : void 0;
42352
42760
  if (pending2.length > 0 && stopMode) {
42353
42761
  console.log(`[dispatch] ${msg.threadKey} ${pending2.length} follow-up(s) dropped: run stopped (${stopMode})`);
42354
42762
  return { kind: "dropped", stopMode, pending: pending2 };
@@ -43007,6 +43415,7 @@ async function dispatch(deps, msg, io, opts = {}) {
43007
43415
  let reserved;
43008
43416
  let requestRow;
43009
43417
  let fencedWhileAttaching = false;
43418
+ let stoppedWhileAttaching;
43010
43419
  let resumeRowClosed = false;
43011
43420
  let restartRequest;
43012
43421
  const reservationHooks = {
@@ -43321,9 +43730,30 @@ async function dispatch(deps, msg, io, opts = {}) {
43321
43730
  profile,
43322
43731
  repoCtx,
43323
43732
  root,
43324
- ...reattach !== void 0 ? { reattach } : {}
43733
+ ...reattach !== void 0 ? { reattach } : {},
43734
+ // The run's control exists from the registry row above: a stop relayed
43735
+ // during the attach ends its wake wait at once (execution.md item 9).
43736
+ ...registered ? { stopSignal: registered.control.hardSignal } : {}
43325
43737
  });
43326
43738
  if (attach.kind === "refused") return ended;
43739
+ if (attach.kind === "stopped") {
43740
+ if (!fencedWhileAttaching) {
43741
+ stoppedWhileAttaching = registered?.control.requested ?? "hard";
43742
+ await root.span(
43743
+ "dispatch.stop",
43744
+ () => card.done(
43745
+ shell.close({
43746
+ kind: "not_started",
43747
+ icon: "\u26D4",
43748
+ reason: "stopped before the run started",
43749
+ ...closeLines(clock4(), false)
43750
+ })
43751
+ ),
43752
+ { attrs: { outcome: "stopped_while_attaching" } }
43753
+ );
43754
+ }
43755
+ return ended;
43756
+ }
43327
43757
  if (attach.kind === "reattach_refused") {
43328
43758
  if (resume) {
43329
43759
  const request2 = await abandonLostWorkspace({
@@ -43520,9 +43950,17 @@ async function dispatch(deps, msg, io, opts = {}) {
43520
43950
  inbox: admitted.inbox
43521
43951
  }
43522
43952
  );
43953
+ const scopes = deps.config.scopes(msg.channelId, msg.userId);
43954
+ const addressSeverity = resolveAddressSeverity({
43955
+ org: deps.config.config.review?.addressSeverity,
43956
+ channel: scopes.channel.review?.addressSeverity,
43957
+ user: scopes.user.review?.addressSeverity,
43958
+ run: directives.severity
43959
+ });
43523
43960
  const ran = await runLoop(deps, {
43524
43961
  msg,
43525
43962
  io,
43963
+ addressSeverity,
43526
43964
  agent,
43527
43965
  profile,
43528
43966
  resolved,
@@ -43644,17 +44082,21 @@ async function dispatch(deps, msg, io, opts = {}) {
43644
44082
  if (registered) deps.runBearers?.revoke(registered.id);
43645
44083
  if (resume && ledgerRun && !runLoopStarted && !resumeRowClosed) {
43646
44084
  const adopted = ledgerRun;
43647
- await root.span(
43648
- "post.history_write",
43649
- () => closeResumedRow(adopted, resume, "the resumed dispatch ended before the run started")
43650
- );
43651
- console.log(
43652
- `[resume] ${msg.threadKey} run ${resume.row.runId} closed interrupted: the resumed dispatch ended before the run started`
43653
- );
44085
+ const why = stoppedWhileAttaching !== void 0 ? "the resumed run was stopped before it started" : "the resumed dispatch ended before the run started";
44086
+ const status3 = stoppedWhileAttaching === "hard" ? "stopped_hard" : stoppedWhileAttaching === "soft" ? "stopped_soft" : "interrupted";
44087
+ await root.span("post.history_write", () => closeResumedRow(adopted, resume, why, status3));
44088
+ console.log(`[resume] ${msg.threadKey} run ${resume.row.runId} closed ${status3}: ${why}`);
43654
44089
  }
43655
- const settled = settleThread(deps, { msg, admitted, runLoopStarted, control: registered?.control });
44090
+ const settled = settleThread(deps, {
44091
+ msg,
44092
+ admitted,
44093
+ // A stop that ended the attach counts as the loop's stop would: the
44094
+ // request ends `stopped`, and a follow-up queued during the wait is told.
44095
+ stopCounts: runLoopStarted || stoppedWhileAttaching !== void 0,
44096
+ control: registered?.control
44097
+ });
43656
44098
  if (settled.kind === "dropped") await tellDropped(root, settled.pending);
43657
- const stopMode = settled.kind === "handed-on" ? void 0 : settled.stopMode;
44099
+ const stopMode = (settled.kind === "handed-on" ? void 0 : settled.stopMode) ?? stoppedWhileAttaching;
43658
44100
  ended.status = caught ? "failed" : refused7 ? "refused" : stopMode ? "stopped" : "completed";
43659
44101
  root.end(caught ? "error" : "ok", { status: ended.status });
43660
44102
  if (restartRequest) {
@@ -43787,6 +44229,7 @@ var init_dispatcher = __esm({
43787
44229
  init_record();
43788
44230
  init_ship();
43789
44231
  init_shipPipeline();
44232
+ init_reviewVerdict();
43790
44233
  init_contract();
43791
44234
  init_codingChild();
43792
44235
  init_settle();
@@ -45565,6 +46008,38 @@ let pending = Promise.resolve();
45565
46008
  let backoffMs = MIN_BACKOFF_MS;
45566
46009
  let connections = 0;
45567
46010
 
46011
+ // A write the feed refuses is not thrown from the write: node reports it on the
46012
+ // stream's error event \u2014 for the file the seam appends the feed to (EBADF: a
46013
+ // descriptor opened without write) as for a pipe whose reader is gone (EPIPE)
46014
+ // \u2014 and an error event nobody listens for is an uncaught exception that kills
46015
+ // the tailer with no note anywhere. The listener is the guard: the refused
46016
+ // record is lost to the feed and said on stderr. A feed gone for good \u2014 its
46017
+ // descriptor bad (EBADF), its reader gone (EPIPE): every later write fails the
46018
+ // same way and the stream is not destroyed \u2014 ends the tailer non-zero once the
46019
+ // note is out, so its death is visible to the seam and a re-attach restarts it
46020
+ // (a mute tailer alive at its pid would be waited on to the run's silence
46021
+ // bound): the exit code is set first, the exit follows the note's write or a
46022
+ // short unref'd timer, whichever comes first, so a stderr that never drains
46023
+ // cannot keep the mute tailer alive either. Any other refusal \u2014 a full disk
46024
+ // (ENOSPC), which space can cure \u2014 is noted and the tailer goes on. A write on
46025
+ // stderr that fails has nothing left to say it to.
46026
+ const FEED_GONE = new Set(["EBADF", "EPIPE"]);
46027
+ const FEED_GONE_EXIT_MS = 1000;
46028
+ process.stdout.on("error", (err) => {
46029
+ const gone = FEED_GONE.has(err && err.code);
46030
+ if (gone) {
46031
+ process.exitCode = 1;
46032
+ setTimeout(() => process.exit(1), FEED_GONE_EXIT_MS).unref();
46033
+ }
46034
+ process.stderr.write(
46035
+ "tailer: a record could not be written" + (gone ? "; the feed is gone, exiting" : "") + ": " + detailOf(err) + "\\n",
46036
+ () => {
46037
+ if (gone) process.exit(1);
46038
+ },
46039
+ );
46040
+ });
46041
+ process.stderr.on("error", () => {});
46042
+
45568
46043
  function emit(record) {
45569
46044
  process.stdout.write(JSON.stringify(record) + "\\n");
45570
46045
  }
@@ -45601,15 +46076,21 @@ async function getJson(path) {
45601
46076
  return JSON.parse(res.body);
45602
46077
  }
45603
46078
 
45604
- /** The session's pending asks, whole; then its messages, the changed ones only. */
46079
+ /** The session's pending asks, whole, emitted the moment their read answers;
46080
+ * its messages, the changed ones only, after the store's pages. Both reads go
46081
+ * out at once: an unanswered ask holds the server's turn, so it waits on
46082
+ * nothing \u2014 least of all a long store's pagination. An ask read ahead of the
46083
+ * rows that name its call is the bridge's to hold until they come. */
45605
46084
  async function refill(sessionID, reason) {
45606
46085
  const at = Date.now();
45607
- try {
45608
- const answer = await getJson("/api/session/" + sessionID + "/permission");
45609
- emit({ feed: "permissions", at, sessionID, reason, data: Array.isArray(answer.data) ? answer.data : [] });
45610
- } catch (err) {
45611
- note("permission refill failed", { sessionID, reason, detail: detailOf(err) });
45612
- }
46086
+ // Neither read nor what is done with its answer can reject out of here: the
46087
+ // asks' chain catches its read's refusal and a malformed answer alike (null
46088
+ // is JSON with no data), the messages' try its pages and its record \u2014 so
46089
+ // refill never rejects and the queue below never ends (a write cannot throw:
46090
+ // emit).
46091
+ const asks = getJson("/api/session/" + sessionID + "/permission")
46092
+ .then((answer) => emit({ feed: "permissions", at, sessionID, reason, data: Array.isArray(answer.data) ? answer.data : [] }))
46093
+ .catch((err) => note("permission refill failed", { sessionID, reason, detail: detailOf(err) }));
45613
46094
  try {
45614
46095
  const all = [];
45615
46096
  let cursor;
@@ -45638,10 +46119,16 @@ async function refill(sessionID, reason) {
45638
46119
  if (!known.has(sessionID)) known.set(sessionID, new Map());
45639
46120
  note("message refill failed", { sessionID, reason, detail: detailOf(err) });
45640
46121
  }
46122
+ await asks;
45641
46123
  }
45642
46124
 
46125
+ // One refill after another; a refill that still managed to reject is noted
46126
+ // and the chain goes on \u2014 a rejected chain would skip every later refill for
46127
+ // every session, silently, for the rest of the tailer's life.
45643
46128
  function queueRefill(sessionID, reason) {
45644
- pending = pending.then(() => refill(sessionID, reason));
46129
+ pending = pending
46130
+ .then(() => refill(sessionID, reason))
46131
+ .catch((err) => note("refill failed", { sessionID, reason, detail: detailOf(err) }));
45645
46132
  }
45646
46133
 
45647
46134
  function onEvent(event) {
@@ -46105,6 +46592,12 @@ function isBudgetRefusal(error) {
46105
46592
  return TURN_BUDGET_REFUSAL.test(error.message);
46106
46593
  }
46107
46594
  function judgeOpenCodeAsk(action, resources, rules, relayedToolNames) {
46595
+ if (action === "doom_loop")
46596
+ return {
46597
+ reply: "reject",
46598
+ tool: "doom_loop",
46599
+ message: "OpenCode's repeat guard tripped: the same call has been made over and over, and the run does not go on looping \u2014 change approach, or write up what stands"
46600
+ };
46108
46601
  const word = openCodeToolWord(action);
46109
46602
  const tool = TOOL_NAME_WORD[action] ?? word;
46110
46603
  if (relayedToolNames.has(action) || relayedToolNames.has(word)) return { reply: "once", tool: action };
@@ -46128,14 +46621,42 @@ function judgeOpenCodeAsk(action, resources, rules, relayedToolNames) {
46128
46621
  if (verdict.verdict !== "allowed") return refuse3(verdict.reason);
46129
46622
  return { reply: "once", tool };
46130
46623
  }
46131
- function askInput(request2) {
46624
+ function foldInputs(asks, own) {
46625
+ let input = own;
46626
+ for (const ask of asks) input = openInput(ask, input);
46627
+ return input;
46628
+ }
46629
+ function openInput(request2, own) {
46132
46630
  const resources = Array.isArray(request2.resources) ? request2.resources.filter((r) => typeof r === "string") : [];
46133
- if (resources.length === 0) return void 0;
46631
+ if (request2.action === "doom_loop") return own;
46632
+ if (resources.length === 0) return own;
46633
+ if (request2.action === "external_directory") {
46634
+ const ownPath = typeof own?.path === "string" ? own.path : typeof own?.filePath === "string" ? own.filePath : void 0;
46635
+ const before = Array.isArray(own?.directories) ? own.directories.filter((d) => typeof d === "string") : [];
46636
+ const directories = [.../* @__PURE__ */ new Set([...before, ...resources])];
46637
+ return { ...own ?? {}, path: ownPath ?? directories[0], directory: directories[0], directories };
46638
+ }
46134
46639
  return openCodeToolNameWord(request2.action) === "bash" ? { command: resources.join(" ") } : { path: resources[0] };
46135
46640
  }
46641
+ function describeOpenCodeToolCall(tool, input) {
46642
+ const line3 = describePiToolCall(tool, input);
46643
+ const reached = Array.isArray(input?.directories) ? input.directories.filter((d) => typeof d === "string" && d !== input.path) : [];
46644
+ return reached.length === 0 ? line3 : `${line3} (reaching ${reached.join(", ")})`;
46645
+ }
46136
46646
  function unattributableDetail(tool, callId) {
46137
46647
  return `${tool} (call ${callId}): an ask pending at the bot's death was answered while the bot was away; the run cannot tell by whom`;
46138
46648
  }
46649
+ function toolPartsOf(message4) {
46650
+ if (message4.type !== "assistant") return [];
46651
+ const content = message4.content;
46652
+ const parts = [];
46653
+ for (const part of Array.isArray(content) ? content : []) {
46654
+ if (!isRecord9(part) || part.type !== "tool") continue;
46655
+ const input = isRecord9(part.state) && isRecord9(part.state.input) ? part.state.input : void 0;
46656
+ parts.push({ id: str6(part.id), name: str6(part.name), ...input ? { input } : {} });
46657
+ }
46658
+ return parts;
46659
+ }
46139
46660
  function ledgerTurnCount(ledger) {
46140
46661
  let turns = 0;
46141
46662
  for (const row2 of ledger) {
@@ -46307,6 +46828,7 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
46307
46828
  let providerError;
46308
46829
  let writeUpFailed;
46309
46830
  let settled = false;
46831
+ let interruptPosted = false;
46310
46832
  let replacedBy;
46311
46833
  let transportLost;
46312
46834
  const probe = {
@@ -46352,13 +46874,24 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
46352
46874
  post("the write-up steer", sessionRoutes["session.prompt"], { text: instruction, delivery: "steer" });
46353
46875
  };
46354
46876
  const turnCount = () => deps.bearers?.grantOf(run2.runId)?.turns ?? bridge.turns;
46877
+ const interrupt = () => {
46878
+ interruptPosted = true;
46879
+ post("the interrupt", sessionRoutes["session.interrupt"]);
46880
+ };
46355
46881
  const check = () => {
46356
46882
  const requested = run2.control?.requested;
46357
46883
  if (requested === "hard") {
46358
46884
  if (ended === void 0) {
46359
46885
  ended = "hard";
46360
46886
  note("stopped", hardStopNote(), "hard");
46361
- post("the interrupt", sessionRoutes["session.interrupt"]);
46887
+ interrupt();
46888
+ }
46889
+ return;
46890
+ }
46891
+ if (conn.failure.error !== void 0) {
46892
+ if (ended === void 0) {
46893
+ ended = "failed";
46894
+ interrupt();
46362
46895
  }
46363
46896
  return;
46364
46897
  }
@@ -46374,7 +46907,7 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
46374
46907
  writeUpFailed ??= reason2;
46375
46908
  run2.onProgress?.(finaleTimedOutNote());
46376
46909
  note("harness_error", windDownFailureNote(reason2));
46377
- post("the interrupt", sessionRoutes["session.interrupt"]);
46910
+ interrupt();
46378
46911
  }
46379
46912
  return;
46380
46913
  }
@@ -46480,7 +47013,7 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
46480
47013
  if (failure2 !== void 0) {
46481
47014
  replyFailed = new OpenCodeReplyFailedError(reply.requestID, reply.callId, reply.reply, failure2);
46482
47015
  note("harness_error", `${replyFailed.message} \u2014 the run is stopped`);
46483
- post("the interrupt", sessionRoutes["session.interrupt"]);
47016
+ interrupt();
46484
47017
  return replyFailed;
46485
47018
  }
46486
47019
  }
@@ -46607,7 +47140,7 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
46607
47140
  if (await postReplies(obs) !== void 0) break;
46608
47141
  if (obs.bypass) {
46609
47142
  bypass = obs.bypass;
46610
- post("the interrupt", sessionRoutes["session.interrupt"]);
47143
+ interrupt();
46611
47144
  break;
46612
47145
  }
46613
47146
  if (bridge.observing === "catching-up") {
@@ -46636,7 +47169,7 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
46636
47169
  conn.boundaries?.left();
46637
47170
  transport.close();
46638
47171
  agentSpan?.end(
46639
- ended === "hard" || ended === "silent" || bypass || replyFailed || replacedBy || transportLost || refused7 || providerError !== void 0 ? "error" : "ok"
47172
+ ended === "hard" || ended === "silent" || ended === "failed" || bypass || replyFailed || replacedBy || transportLost || refused7 || providerError !== void 0 ? "error" : "ok"
46640
47173
  );
46641
47174
  }
46642
47175
  if (replacedBy !== void 0) {
@@ -46651,6 +47184,11 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
46651
47184
  note("sandbox_restarted", replaced.message);
46652
47185
  throw replaced;
46653
47186
  }
47187
+ if (interruptPosted)
47188
+ bridge.closeOpenSpans(
47189
+ (open2) => `${open2.tool} was still running when the loop left on its interrupt; its outcome never reached the record`,
47190
+ { cut: true }
47191
+ );
46654
47192
  if (transportLost !== void 0 && ended !== "hard") {
46655
47193
  note(
46656
47194
  "harness_error",
@@ -46674,18 +47212,19 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
46674
47212
  boundMs: FIRST_EVENT_BOUND_MS
46675
47213
  });
46676
47214
  note("harness_error", `${silent.message} \u2014 the run is stopped`);
46677
- post("the interrupt", sessionRoutes["session.interrupt"]);
47215
+ interrupt();
46678
47216
  throw silent;
46679
47217
  }
46680
47218
  const remaining = () => deadline - now();
46681
47219
  const handOver = () => ({ storeIds: bridge.storeIds() });
46682
- if (ended === "hard") return { answer: HARD_STOP_MESSAGE, remainingMs: remaining, ...handOver() };
47220
+ if (ended === "failed" && conn.failure.error !== void 0) throw conn.failure.error;
47221
+ if (ended === "hard") return { answer: HARD_STOP_MESSAGE, remainingMs: remaining, hardStopped: true, ...handOver() };
46683
47222
  if (providerError !== void 0) throw new Error(`the model call failed: ${providerError}`);
46684
47223
  if (!settled && ended !== "finale") throw new Error("the OpenCode run ended before its execution settled");
46685
47224
  await bridge.flush();
46686
47225
  const text = bridge.answer() ?? "";
46687
47226
  const answer = writeUpAnswer(writeUp, text, run2.agent.maxMinutes, writeUpFailed);
46688
- return { answer, remainingMs: remaining, ...handOver() };
47227
+ return { answer, remainingMs: remaining, hardStopped: false, ...handOver() };
46689
47228
  }
46690
47229
  function writeUpAnswer(writeUp, text, maxMinutes, writeUpFailed) {
46691
47230
  if (writeUp?.kind === "time") return timeBudgetAnswer(text, maxMinutes, writeUpFailed);
@@ -46711,7 +47250,7 @@ var init_bridge3 = __esm({
46711
47250
  init_dispositions();
46712
47251
  init_session2();
46713
47252
  init_process2();
46714
- OpenCodeGateBypassedError = class extends Error {
47253
+ OpenCodeGateBypassedError = class extends HarnessGateBypassedError {
46715
47254
  constructor(detail) {
46716
47255
  super(`the gate was bypassed: ${detail}`);
46717
47256
  this.detail = detail;
@@ -46845,10 +47384,17 @@ var init_bridge3 = __esm({
46845
47384
  * the fake sends the whole store — and the projection reads the whole map,
46846
47385
  * so a turn lands once whichever shape a refill has (the join by message id). */
46847
47386
  store = /* @__PURE__ */ new Map();
46848
- /** The store's rows as the last refill showed them, by id — set the moment a
46849
- * refill is read, ahead of the mirror's chain, for what must be named at
46850
- * once: the tool of a call a permission's `source` points at (`toolOfPart`). */
46851
- latest = /* @__PURE__ */ new Map();
47387
+ /** Every tool part the store has shown, by the part's id (the call id): the
47388
+ * tool's name and its own input — filled the moment a refill is read, ahead
47389
+ * of the mirror's chain, for what must be named at once: the call a
47390
+ * permission's `source` points at. */
47391
+ partTools = /* @__PURE__ */ new Map();
47392
+ /** Resource permissions answered for a call the store has not named yet, by
47393
+ * call id, each call's asks in the order they came — two can name one call
47394
+ * (`external_directory`, then `doom_loop`) and each is kept: the call is
47395
+ * opened once, when a refill names its part, with every held ask's input
47396
+ * folded in, or at its settle under the first ask's own name if none did. */
47397
+ unnamedAsks = /* @__PURE__ */ new Map();
46852
47398
  /** How many store messages the projection skips: the seed and the request's
46853
47399
  * echo on a fresh run, the import on a rebuild; none on a re-attach, where
46854
47400
  * the ledger's rows are skipped by turn instead (`adoptStore`). */
@@ -46934,18 +47480,14 @@ var init_bridge3 = __esm({
46934
47480
  const pendingCalls = new Set(pending2.map((r) => r.source?.id ?? r.id));
46935
47481
  for (const row2 of ledger)
46936
47482
  for (const part of row2.content) if (part.type === "tool_result") this.ledgerResults.add(part.toolUseId);
46937
- for (const message4 of messages) {
46938
- if (message4.type !== "assistant") continue;
46939
- const content = message4.content;
46940
- for (const part of Array.isArray(content) ? content : []) {
46941
- if (!isRecord9(part) || part.type !== "tool") continue;
46942
- const id = str6(part.id);
46943
- const name = str6(part.name);
47483
+ for (const message4 of messages)
47484
+ for (const part of toolPartsOf(message4)) {
47485
+ const { id, name } = part;
46944
47486
  this.toolNames.set(id, name);
47487
+ this.partTools.set(id, part);
46945
47488
  if (this.ledgerResults.has(id) || pendingCalls.has(id) || this.deps.relayedToolNames.has(name)) continue;
46946
47489
  this.unattributableCalls.add(id);
46947
47490
  }
46948
- }
46949
47491
  this.storeSeed = 0;
46950
47492
  this.fedTurns = Math.min(ledgerTurnCount(ledger), projectStore(messages, 0).length);
46951
47493
  this.mirrorChain = this.mirrorChain.then(() => this.syncMirror(messages));
@@ -46966,12 +47508,17 @@ var init_bridge3 = __esm({
46966
47508
  storeIds() {
46967
47509
  return [...this.store.keys()];
46968
47510
  }
46969
- /** Settle every call still open when the container went under a running call
46970
- * (the replaced verdict): each open span ends `error` and its call is put on
46971
- * the record as a failed `tool_result` carrying `reason` — the restart note,
46972
- * said of the container — so no span outlives the run and the record holds a
46973
- * result for every call at the death, exactly as pi's `closeOpenSpans` does. */
46974
- closeOpenSpans(reason2) {
47511
+ /** Settle every call still open when the loop is done with the session: each
47512
+ * open span ends `error` and its call is put on the record as a failed
47513
+ * `tool_result` carrying `reason`, so no span outlives the run and the record
47514
+ * holds a result for every call, exactly as pi's `closeOpenSpans` does. On
47515
+ * the container-replaced verdict (the restart note, said of the container)
47516
+ * the results are unmarked: the old container's calls are gone and the
47517
+ * relaunch takes the relayed ones over, nothing the workspace's release must
47518
+ * hold for. At every other exit the loop left the calls interrupted or
47519
+ * unread, and `cut` marks each result as a call ended and not settled, whose
47520
+ * command may still be running (harness.md item 13). */
47521
+ closeOpenSpans(reason2, opts = {}) {
46975
47522
  for (const [callId, open2] of this.openTools) {
46976
47523
  open2.span?.end("error", { callId, ok: false });
46977
47524
  this.emit({
@@ -46979,7 +47526,8 @@ var init_bridge3 = __esm({
46979
47526
  tool: open2.tool,
46980
47527
  ok: false,
46981
47528
  callId,
46982
- summary: redactAndCap(reason2({ callId, tool: open2.tool }), COMMAND_CAP)
47529
+ summary: redactAndCap(reason2({ callId, tool: open2.tool }), COMMAND_CAP),
47530
+ ...opts.cut ? { cut: true } : {}
46983
47531
  });
46984
47532
  }
46985
47533
  this.openTools.clear();
@@ -47141,6 +47689,7 @@ ${data.text}` : data.text;
47141
47689
  }
47142
47690
  onToolCalled(data) {
47143
47691
  const callId = str6(data.id);
47692
+ if (this.openTools.has(callId)) return;
47144
47693
  this.openCall(callId, this.toolNames.get(callId) ?? "tool", isRecord9(data.input) ? data.input : void 0);
47145
47694
  }
47146
47695
  /** The call opened on the record — its span, its `tool_call` with the
@@ -47161,7 +47710,7 @@ ${data.text}` : data.text;
47161
47710
  this.emit({
47162
47711
  type: "tool_call",
47163
47712
  tool,
47164
- summary: redactAndCap(describePiToolCall(tool, input)),
47713
+ summary: redactAndCap(describeOpenCodeToolCall(tool, input)),
47165
47714
  callId,
47166
47715
  ...command2,
47167
47716
  ...span ? { spanId: span.id } : {}
@@ -47179,6 +47728,16 @@ ${data.text}` : data.text;
47179
47728
  );
47180
47729
  return;
47181
47730
  }
47731
+ const held2 = this.takeHeldAsks(callId);
47732
+ const first = held2[0];
47733
+ if (first !== void 0 && !this.openTools.has(callId) && !this.toolNames.has(callId)) {
47734
+ this.note(
47735
+ "tool_unnamed",
47736
+ `OpenCode asked ${redactAndCap(held2.map((h) => h.action).join(", "), 60)} for call ${callId} of step ${redactAndCap(first.source?.messageID ?? "", 80)}, and the store showed no part naming the call's tool before it settled; the record opens the call under the permission's name`
47737
+ );
47738
+ this.toolNames.set(callId, first.action);
47739
+ this.openCall(callId, first.action, foldInputs(held2, void 0));
47740
+ }
47182
47741
  const open2 = this.openTools.get(callId);
47183
47742
  this.openTools.delete(callId);
47184
47743
  const tool = open2?.tool ?? openCodeToolNameWord(this.toolNames.get(callId) ?? "tool");
@@ -47225,16 +47784,45 @@ ${data.text}` : data.text;
47225
47784
  );
47226
47785
  }
47227
47786
  }
47228
- /** The tool of the call a permission's `source` names, read from the store's
47229
- * rows as last refilled: the assistant message's tool part with that id —
47230
- * its `name` — or nothing when the store holds no such part yet. */
47231
- toolOfPart(messageID, partId) {
47232
- const message4 = this.latest.get(messageID);
47233
- if (message4 === void 0 || message4.type !== "assistant") return void 0;
47234
- const content = message4.content;
47235
- if (!Array.isArray(content)) return void 0;
47236
- const part = content.find((p) => isRecord9(p) && p.type === "tool" && p.id === partId);
47237
- return typeof part?.name === "string" ? part.name : void 0;
47787
+ /** Take the resource permissions held for a call, in the order they came:
47788
+ * the hold is over once they are read, whoever reads them. */
47789
+ takeHeldAsks(callId) {
47790
+ const held2 = this.unnamedAsks.get(callId) ?? [];
47791
+ this.unnamedAsks.delete(callId);
47792
+ return held2;
47793
+ }
47794
+ /** An `external_directory` ask answered once the call's line is already on
47795
+ * the record: the line cannot be amended, so the directory the call reached
47796
+ * is said in a note naming the call (harness.md item 13) rather than lost. */
47797
+ noteDirectoryReached(callId, asks) {
47798
+ const directories = [
47799
+ ...new Set(
47800
+ asks.filter((ask) => ask.action === "external_directory").flatMap((ask) => Array.isArray(ask.resources) ? ask.resources : []).filter((r) => typeof r === "string")
47801
+ )
47802
+ ];
47803
+ if (directories.length === 0) return;
47804
+ const tool = openCodeToolNameWord(this.toolNames.get(callId) ?? "tool");
47805
+ this.note(
47806
+ "directory_reached",
47807
+ `${tool} (call ${callId}) reached ${redactAndCap(directories.join(", "), 200)} after its line was written; the line does not name it`
47808
+ );
47809
+ }
47810
+ /** A resource permission's call, opened once the store names its part: under
47811
+ * the tool's word, with the tool's own input and the resources of every ask
47812
+ * held before the open — and of `ask`, one arriving with the part already in
47813
+ * hand — folded in (every directory the `external_directory` asks named, the
47814
+ * first as `directory`, all in `directories`); an ask that arrives after the
47815
+ * call opened is answered but cannot amend the `tool_call` already on the
47816
+ * record — the directory it named is said in a `directory_reached` note. */
47817
+ openNamedCall(callId, part, ask) {
47818
+ const held2 = this.takeHeldAsks(callId);
47819
+ if (ask !== void 0) held2.push(ask);
47820
+ if (this.openTools.has(callId) || this.toolNames.has(callId)) {
47821
+ this.noteDirectoryReached(callId, held2);
47822
+ return;
47823
+ }
47824
+ this.toolNames.set(callId, part.name);
47825
+ this.openCall(callId, part.name, foldInputs(held2, part.input));
47238
47826
  }
47239
47827
  bypass(out, detail) {
47240
47828
  out.bypass = new OpenCodeGateBypassedError(detail);
@@ -47243,20 +47831,24 @@ ${data.text}` : data.text;
47243
47831
  onPermissionAsked(request2, out) {
47244
47832
  const callId = request2.source?.id ?? request2.id;
47245
47833
  if (typeof request2.source?.messageID === "string") this.ownSteps.add(request2.source.messageID);
47246
- if (this.answered.has(callId)) return;
47247
47834
  if (this.decidedReplies.has(request2.id)) return;
47248
47835
  const source2 = request2.source;
47249
47836
  if (source2?.type === "tool" && this.observing === "own" && !this.openTools.has(callId) && !this.toolNames.has(callId)) {
47250
- const resource = RESOURCE_PERMISSIONS.has(request2.action);
47251
- const named = resource ? this.toolOfPart(source2.messageID, source2.id) : void 0;
47252
- if (resource && named === void 0)
47253
- this.note(
47254
- "tool_unnamed",
47255
- `OpenCode asked ${redactAndCap(request2.action, 40)} for call ${callId} of step ${redactAndCap(source2.messageID, 80)}, and the store holds no part naming the call's tool yet; the record opens the call under the permission's name`
47256
- );
47257
- const word = named !== void 0 ? openCodeToolNameWord(named) : resource ? request2.action : openCodeToolWord(request2.action);
47258
- this.toolNames.set(callId, word);
47259
- this.openCall(callId, word, resource ? void 0 : askInput(request2));
47837
+ if (RESOURCE_PERMISSIONS.has(request2.action)) {
47838
+ const part = this.partTools.get(source2.id);
47839
+ if (part !== void 0) this.openNamedCall(callId, part, request2);
47840
+ else {
47841
+ const held2 = this.unnamedAsks.get(callId);
47842
+ if (held2 === void 0) this.unnamedAsks.set(callId, [request2]);
47843
+ else held2.push(request2);
47844
+ }
47845
+ } else {
47846
+ const raw = OPENCODE_ACTION_TO_TOOL_WORD[request2.action] !== void 0 ? request2.action : openCodeToolWord(request2.action);
47847
+ this.toolNames.set(callId, raw);
47848
+ this.openCall(callId, raw, openInput(request2, void 0));
47849
+ }
47850
+ } else if (source2?.type === "tool" && this.observing === "own" && request2.action === "external_directory") {
47851
+ this.noteDirectoryReached(callId, [request2]);
47260
47852
  }
47261
47853
  if (this.observing === "catching-up" && !this.pendingAtReattach.has(request2.id)) {
47262
47854
  const name = this.toolNames.get(callId) ?? request2.action;
@@ -47271,7 +47863,7 @@ ${data.text}` : data.text;
47271
47863
  this.deps.rules,
47272
47864
  this.deps.relayedToolNames
47273
47865
  );
47274
- this.answered.set(callId, verdict.reply);
47866
+ if (this.answered.get(callId) !== "reject") this.answered.set(callId, verdict.reply);
47275
47867
  this.decidedReplies.set(request2.id, { reply: verdict.reply, callId, echoed: false });
47276
47868
  if (verdict.reply === "reject")
47277
47869
  this.note("tool_refused", `${verdict.tool} refused: ${redactAndCap(verdict.message ?? "", 300)}`);
@@ -47326,7 +47918,11 @@ ${data.text}` : data.text;
47326
47918
  return out;
47327
47919
  }
47328
47920
  onMessagesRefill(messages) {
47329
- for (const message4 of messages) this.latest.set(message4.id, message4);
47921
+ for (const message4 of messages)
47922
+ for (const part of toolPartsOf(message4)) {
47923
+ this.partTools.set(part.id, part);
47924
+ if (this.unnamedAsks.has(part.id)) this.openNamedCall(part.id, part);
47925
+ }
47330
47926
  this.mirrorChain = this.mirrorChain.then(() => this.syncMirror(messages));
47331
47927
  return { replies: [], settled: false };
47332
47928
  }
@@ -47618,6 +48214,7 @@ async function openOpenCodeRun(deps, run2, settings) {
47618
48214
  knownMessageIds: known,
47619
48215
  writes: { seq: 0, ownPrompts: /* @__PURE__ */ new Map(), imported },
47620
48216
  boundaries: new StepBoundaries(),
48217
+ failure: {},
47621
48218
  paths: server.paths,
47622
48219
  port: server.port,
47623
48220
  password: server.password,
@@ -47641,16 +48238,17 @@ async function openOpenCodeRun(deps, run2, settings) {
47641
48238
  );
47642
48239
  let answer;
47643
48240
  let remainingMs;
48241
+ let hardStopped;
47644
48242
  let drained;
47645
48243
  try {
47646
48244
  let storeIds;
47647
- ({ answer, remainingMs, storeIds } = await driveOpenCode(deps, run2, conn));
48245
+ ({ answer, remainingMs, storeIds, hardStopped } = await driveOpenCode(deps, run2, conn));
47648
48246
  for (const id of storeIds) known.add(id);
47649
48247
  } finally {
47650
48248
  draining = false;
47651
48249
  drained = await drainer;
47652
48250
  }
47653
- if (drained instanceof OpenCodeWriteUnresolvedError) throw drained;
48251
+ if (drained instanceof OpenCodeWriteUnresolvedError && !hardStopped) throw drained;
47654
48252
  return {
47655
48253
  answer,
47656
48254
  followUp: async (input) => {
@@ -47788,9 +48386,18 @@ function drainFollowUps(deps, run2, conn, running, emit, note) {
47788
48386
  const routes = openCodeSessionRoutes(conn.sessionID);
47789
48387
  return (async () => {
47790
48388
  const deferred = [];
48389
+ const senderOf = (input) => input.from !== void 0 ? `run:${input.from.runId}` : `user:${input.userId}`;
47791
48390
  while (running()) {
47792
48391
  const inputs = run2.inbox?.drain() ?? [];
47793
48392
  for (const input of inputs) {
48393
+ if (input.from === void 0 && deferred.some((held2) => senderOf(held2) === senderOf(input))) {
48394
+ deferred.push(input);
48395
+ note(
48396
+ "follow_up",
48397
+ `follow-up held for a fresh turn behind an earlier one from the same sender the server did not take: ${redactSecrets(followUpSnippet(input))}`
48398
+ );
48399
+ continue;
48400
+ }
47794
48401
  let stagedLine = "";
47795
48402
  if (run2.stageFollowUps) {
47796
48403
  try {
@@ -47891,8 +48498,8 @@ ${stagedLine}` : prompt;
47891
48498
  } catch (err2) {
47892
48499
  if (err2 instanceof OpenCodeWriteUnresolvedError) {
47893
48500
  note("harness_error", `${err2.message} \u2014 the run is stopped`);
47894
- run2.control?.requestStop("hard");
47895
- run2.inbox?.requeue([...deferred.splice(0), ...inputs.slice(inputs.indexOf(input) + 1)]);
48501
+ conn.failure.error = err2;
48502
+ run2.inbox?.requeue([...deferred.splice(0), ...inputs.slice(inputs.indexOf(input))]);
47896
48503
  }
47897
48504
  throw err2;
47898
48505
  }
@@ -47906,8 +48513,8 @@ ${stagedLine}` : prompt;
47906
48513
  run2.inbox?.requeue([...deferred.splice(0), ...rest]);
47907
48514
  return;
47908
48515
  }
47909
- deferred.push(...rest);
47910
- break;
48516
+ deferred.push(input);
48517
+ continue;
47911
48518
  }
47912
48519
  const source2 = {
47913
48520
  ...input.sourceUrl ? { url: input.sourceUrl } : {},
@@ -50623,6 +51230,17 @@ function clickSlackIO(client, click, opts = {}) {
50623
51230
  { ...opts, offerMessage: click.offer }
50624
51231
  );
50625
51232
  }
51233
+ function takenOfferNote(kind, userId) {
51234
+ const button = kind === "confirm" ? "Run" : "Cancel";
51235
+ const doing = kind === "confirm" ? "running\u2026" : "cancelling\u2026";
51236
+ return { mrkdwn: `*${button}* clicked by <@${userId}> \xB7 ${doing}`, plain: `${button} clicked \xB7 ${doing}` };
51237
+ }
51238
+ function takenOfferBlocks(blocks, kind, userId) {
51239
+ return [
51240
+ ...blocks.filter((b) => b.type !== "actions"),
51241
+ { type: "context", elements: [{ type: "mrkdwn", text: takenOfferNote(kind, userId).mrkdwn }] }
51242
+ ];
51243
+ }
50626
51244
  async function handleConfirmClick(deps, { client, statusClient }, { ack, body, action }, botUserId) {
50627
51245
  await ack();
50628
51246
  const kind = action.action_id === "confirm.run" ? "confirm" : action.action_id === "confirm.cancel" ? "cancel" : void 0;
@@ -50637,6 +51255,19 @@ async function handleConfirmClick(deps, { client, statusClient }, { ack, body, a
50637
51255
  return;
50638
51256
  }
50639
51257
  const threadTs = message4.thread_ts ?? container?.thread_ts ?? message4.ts;
51258
+ try {
51259
+ await client.chat.update({
51260
+ channel,
51261
+ ts: message4.ts,
51262
+ text: `${message4.text ?? ""}
51263
+ ${takenOfferNote(kind, body.user.id).plain}`,
51264
+ blocks: takenOfferBlocks(message4.blocks ?? [], kind, body.user.id)
51265
+ });
51266
+ } catch (err2) {
51267
+ console.error(
51268
+ `[confirm] ${kind} click on ${id} in ${channel}:${message4.ts}: the buttons could not be taken down \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`
51269
+ );
51270
+ }
50640
51271
  let io;
50641
51272
  try {
50642
51273
  const requester = await resolveSlackRequester(client, {
@@ -51683,6 +52314,500 @@ var init_scheduledPanel = __esm({
51683
52314
  }
51684
52315
  });
51685
52316
 
52317
+ // ../../src/channels/accessAuth.ts
52318
+ import { createPublicKey, verify as cryptoVerify } from "node:crypto";
52319
+ function isServiceToken(identity2) {
52320
+ return identity2.sub === "" && typeof identity2.commonName === "string" && identity2.commonName !== "";
52321
+ }
52322
+ function decodeBase64Url(part) {
52323
+ if (part === "" || /[^A-Za-z0-9_-]/.test(part)) return null;
52324
+ return Buffer.from(part, "base64url");
52325
+ }
52326
+ function decodeJsonSegment(part) {
52327
+ const buf2 = decodeBase64Url(part);
52328
+ if (!buf2) return null;
52329
+ try {
52330
+ const parsed = JSON.parse(buf2.toString("utf8"));
52331
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
52332
+ return parsed;
52333
+ } catch {
52334
+ return null;
52335
+ }
52336
+ }
52337
+ async function resolveKey(kid, config, deps) {
52338
+ const cache2 = deps.cache ?? new JwksCache();
52339
+ const cached = cache2.get(kid, deps.now());
52340
+ if (cached) return cached;
52341
+ const ttlMs = (deps.ttlSeconds ?? DEFAULT_JWKS_TTL_SECONDS) * 1e3;
52342
+ const minRefetchIntervalMs = deps.minRefetchIntervalMs ?? DEFAULT_MIN_REFETCH_INTERVAL_MS;
52343
+ const certsUrl = `https://${config.teamDomain}${CERTS_PATH}`;
52344
+ await cache2.refresh(() => deps.fetchJwks(certsUrl), deps.now, ttlMs, minRefetchIntervalMs);
52345
+ return cache2.get(kid, deps.now());
52346
+ }
52347
+ function claimsValid(claims, config, now) {
52348
+ const nowSec = Math.floor(now / 1e3);
52349
+ if (claims.iss !== `https://${config.teamDomain}`) return false;
52350
+ const aud = claims.aud;
52351
+ const audOk = typeof aud === "string" ? aud === config.aud : Array.isArray(aud) && aud.includes(config.aud);
52352
+ if (!audOk) return false;
52353
+ if (typeof claims.exp !== "number" || claims.exp <= nowSec) return false;
52354
+ if (typeof claims.nbf === "number" && claims.nbf > nowSec + SKEW_SECONDS) return false;
52355
+ if (typeof claims.iat === "number" && claims.iat > nowSec + SKEW_SECONDS) return false;
52356
+ return hasSubject(claims);
52357
+ }
52358
+ function hasSubject(claims) {
52359
+ if (typeof claims.sub === "string" && claims.sub !== "") return true;
52360
+ const subEmpty = claims.sub === void 0 || claims.sub === "";
52361
+ return subEmpty && typeof claims.common_name === "string" && claims.common_name !== "";
52362
+ }
52363
+ async function verifyAccessJwt(token2, config, deps) {
52364
+ if (typeof token2 !== "string") return null;
52365
+ const parts = token2.split(".");
52366
+ if (parts.length !== 3) return null;
52367
+ const [rawHeader, rawPayload, rawSignature] = parts;
52368
+ const header4 = decodeJsonSegment(rawHeader);
52369
+ const payload = decodeJsonSegment(rawPayload);
52370
+ if (!header4 || !payload) return null;
52371
+ if (header4.alg !== "RS256") return null;
52372
+ if (typeof header4.kid !== "string" || header4.kid === "") return null;
52373
+ const signature = decodeBase64Url(rawSignature);
52374
+ if (!signature) return null;
52375
+ const jwk = await resolveKey(header4.kid, config, deps);
52376
+ if (!jwk) return null;
52377
+ let signatureOk;
52378
+ try {
52379
+ const key = createPublicKey({ format: "jwk", key: jwk });
52380
+ signatureOk = cryptoVerify("RSA-SHA256", Buffer.from(`${rawHeader}.${rawPayload}`), key, signature);
52381
+ } catch {
52382
+ return null;
52383
+ }
52384
+ if (!signatureOk) return null;
52385
+ if (!claimsValid(payload, config, deps.now())) return null;
52386
+ if (typeof payload.sub !== "string" || payload.sub === "") {
52387
+ return { sub: "", commonName: payload.common_name };
52388
+ }
52389
+ return {
52390
+ sub: payload.sub,
52391
+ email: typeof payload.email === "string" ? payload.email : void 0
52392
+ };
52393
+ }
52394
+ function normalizeTeamDomain(raw) {
52395
+ return raw.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
52396
+ }
52397
+ function parseAccessConfig(env) {
52398
+ const teamDomain = normalizeTeamDomain(env.ACCESS_TEAM_DOMAIN ?? "");
52399
+ const aud = (env.ACCESS_AUD ?? "").trim();
52400
+ if (teamDomain === "" || aud === "") return null;
52401
+ return { teamDomain, aud };
52402
+ }
52403
+ var CERTS_PATH, DEFAULT_JWKS_TTL_SECONDS, DEFAULT_MIN_REFETCH_INTERVAL_MS, SKEW_SECONDS, httpJwksFetcher, JwksCache;
52404
+ var init_accessAuth = __esm({
52405
+ "../../src/channels/accessAuth.ts"() {
52406
+ "use strict";
52407
+ CERTS_PATH = "/cdn-cgi/access/certs";
52408
+ DEFAULT_JWKS_TTL_SECONDS = 3600;
52409
+ DEFAULT_MIN_REFETCH_INTERVAL_MS = 3e4;
52410
+ SKEW_SECONDS = 60;
52411
+ httpJwksFetcher = async (certsUrl) => {
52412
+ const res = await fetch(certsUrl);
52413
+ if (!res.ok) throw new Error(`JWKS fetch failed: HTTP ${res.status}`);
52414
+ const body = await res.json();
52415
+ const keys = body?.keys;
52416
+ if (!Array.isArray(keys)) throw new Error("JWKS response missing keys[]");
52417
+ return keys;
52418
+ };
52419
+ JwksCache = class {
52420
+ entries = /* @__PURE__ */ new Map();
52421
+ /** Ms of the last SUCCESSFUL JWKS fetch, or null before the first one. Drives
52422
+ * the negative-cache interval in `refresh`; a failed fetch does not set it. */
52423
+ lastFetchAt = null;
52424
+ /** The in-flight refresh shared by concurrent callers (single-flight); null
52425
+ * when no fetch is running. It never rejects — `runFetch` swallows failures to
52426
+ * fail closed — so both the owner and any joiners resolve cleanly. */
52427
+ inFlight = null;
52428
+ /** The fresh JWK for `kid`, or null if absent/expired (expired entries are
52429
+ * evicted lazily). `now` is milliseconds. */
52430
+ get(kid, now) {
52431
+ const hit = this.entries.get(kid);
52432
+ if (!hit) return null;
52433
+ if (hit.expiresAt <= now) {
52434
+ this.entries.delete(kid);
52435
+ return null;
52436
+ }
52437
+ return hit.jwk;
52438
+ }
52439
+ /** Store every kid'd key from a freshly fetched JWKS with the given absolute
52440
+ * expiry (ms). Keyless entries are skipped. */
52441
+ put(keys, expiresAt) {
52442
+ for (const jwk of keys) {
52443
+ if (typeof jwk.kid === "string" && jwk.kid !== "") {
52444
+ this.entries.set(jwk.kid, { jwk, expiresAt });
52445
+ }
52446
+ }
52447
+ }
52448
+ /**
52449
+ * Refresh the JWKS to (try to) learn an unknown kid, with two guards against
52450
+ * the unknown-kid amplification vector:
52451
+ * - single-flight: if a refresh is already running, join it instead of
52452
+ * firing another — N concurrent unknown kids ⇒ at most one `fetch` call.
52453
+ * - negative cache: once a fetch has succeeded, another is suppressed until
52454
+ * `minRefetchIntervalMs` has elapsed, so an unknown kid resolves to null
52455
+ * WITHOUT a network round-trip inside that window. A genuinely rotated kid
52456
+ * is still picked up on the first refresh after the interval.
52457
+ * Fail-closed: a fetch failure leaves the store unchanged and does not advance
52458
+ * the last-fetch time. `now` is read lazily (ms) so the injected clock can move
52459
+ * across the await.
52460
+ */
52461
+ async refresh(fetch2, now, ttlMs, minRefetchIntervalMs) {
52462
+ if (this.inFlight) {
52463
+ await this.inFlight;
52464
+ return;
52465
+ }
52466
+ if (this.lastFetchAt !== null && now() - this.lastFetchAt < minRefetchIntervalMs) {
52467
+ return;
52468
+ }
52469
+ this.inFlight = this.runFetch(fetch2, now, ttlMs);
52470
+ try {
52471
+ await this.inFlight;
52472
+ } finally {
52473
+ this.inFlight = null;
52474
+ }
52475
+ }
52476
+ /** Perform one JWKS fetch and fold the result into the store. Never rejects:
52477
+ * on any failure the store is left unchanged (fail closed) and `lastFetchAt`
52478
+ * is not advanced, so `resolveKey` returns null and a later verify may retry. */
52479
+ async runFetch(fetch2, now, ttlMs) {
52480
+ try {
52481
+ const keys = await fetch2();
52482
+ const fetchedAt = now();
52483
+ this.put(keys, fetchedAt + ttlMs);
52484
+ this.lastFetchAt = fetchedAt;
52485
+ } catch {
52486
+ }
52487
+ }
52488
+ };
52489
+ }
52490
+ });
52491
+
52492
+ // ../../src/channels/commandHttp.ts
52493
+ function normalizePath2(pathname) {
52494
+ let decoded = pathname;
52495
+ try {
52496
+ decoded = decodeURIComponent(pathname);
52497
+ } catch {
52498
+ }
52499
+ return decoded.replace(/\/{2,}/g, "/");
52500
+ }
52501
+ function isCommandPath(pathname) {
52502
+ const p = normalizePath2(pathname).toLowerCase();
52503
+ return p === API_ROOT || p === `${API_ROOT}/` || p.startsWith(`${API_ROOT}/`);
52504
+ }
52505
+ function commandIdFromPath(pathname) {
52506
+ const p = normalizePath2(pathname);
52507
+ if (!p.startsWith(`${API_ROOT}/`)) return void 0;
52508
+ const rest = p.slice(API_ROOT.length + 1);
52509
+ return COMMAND_ID.test(rest) ? rest : void 0;
52510
+ }
52511
+ function callerIdFor(identity2) {
52512
+ return isServiceToken(identity2) ? `access:svc:${identity2.commonName}` : `access:${identity2.sub}`;
52513
+ }
52514
+ function accessActor(identity2, grantsFor) {
52515
+ return isServiceToken(identity2) ? resolveActor({ surface: "access-service", subjectId: identity2.commonName }, grantsFor) : resolveActor({ surface: "access-browser", subjectId: identity2.sub }, grantsFor);
52516
+ }
52517
+ function serviceTokenAllowed(pathname, identity2) {
52518
+ return !isServiceToken(identity2) || isCommandPath(pathname);
52519
+ }
52520
+ async function resolveAccessActor(identity2, opts) {
52521
+ const actor = accessActor(identity2, opts.grantsFor);
52522
+ if (isServiceToken(identity2)) return actor;
52523
+ if (identity2.viewAs !== void 0 && holdsAll(actor) && isViewablePerson(identity2.viewAs))
52524
+ return viewingAs(actor, identity2.viewAs, opts);
52525
+ if (!identity2.email || !opts.personByEmail) return actor;
52526
+ const person = await opts.personByEmail(identity2.email).catch(() => void 0);
52527
+ if (!person || !person.id.startsWith("slack:")) return actor;
52528
+ const memberOf = opts.channelsOf ? await boundedChannels(opts.channelsOf(person.id), opts.channelsOfTimeoutMs ?? CHANNEL_DIRECTORY_TIMEOUT_MS) : "unknown";
52529
+ return {
52530
+ ...actor,
52531
+ self: [actor.id, person.id],
52532
+ asUser: { id: person.id, ...person.name ? { name: person.name } : {} },
52533
+ ...memberOf === "unknown" ? {} : { memberOf }
52534
+ };
52535
+ }
52536
+ async function viewingAs(admin, personId, opts) {
52537
+ const [memberOf, name] = await Promise.all([
52538
+ opts.channelsOf ? boundedChannels(opts.channelsOf(personId), opts.channelsOfTimeoutMs ?? CHANNEL_DIRECTORY_TIMEOUT_MS) : Promise.resolve("unknown"),
52539
+ opts.personName ? opts.personName(personId).catch(() => void 0) : Promise.resolve(void 0)
52540
+ ]);
52541
+ const person = {
52542
+ kind: "user",
52543
+ id: personId,
52544
+ // The browser baseline under a subject no config names — what the person's own session
52545
+ // holds, whose `sub` this process does not know (an entry keyed to that sub is the one
52546
+ // thing view-as cannot see; record 0053 names the boundary).
52547
+ grants: opts.grantsFor(`access:${personId}`),
52548
+ self: [personId],
52549
+ asUser: { id: personId, ...name ? { name } : {} },
52550
+ ...memberOf === "unknown" ? {} : { memberOf }
52551
+ };
52552
+ return {
52553
+ ...admin,
52554
+ onBehalfOf: person,
52555
+ asUser: person.asUser,
52556
+ viewingAs: { id: personId, ...name ? { name } : {} }
52557
+ };
52558
+ }
52559
+ async function boundedChannels(lookup2, timeoutMs) {
52560
+ let timer;
52561
+ try {
52562
+ const answer = await Promise.race([
52563
+ lookup2.catch(() => "unknown"),
52564
+ new Promise((resolve13) => {
52565
+ timer = setTimeout(() => resolve13(LOOKUP_TIMED_OUT), timeoutMs);
52566
+ })
52567
+ ]);
52568
+ return answer === LOOKUP_TIMED_OUT ? "unknown" : answer;
52569
+ } finally {
52570
+ if (timer) clearTimeout(timer);
52571
+ }
52572
+ }
52573
+ async function callerFor(identity2, opts) {
52574
+ return {
52575
+ kind: "access",
52576
+ id: callerIdFor(identity2),
52577
+ actor: await resolveAccessActor(identity2, opts),
52578
+ ...identity2.email && !isServiceToken(identity2) ? { email: identity2.email } : {}
52579
+ };
52580
+ }
52581
+ function hostOf(url) {
52582
+ if (!url) return void 0;
52583
+ try {
52584
+ return new URL(url).host.toLowerCase();
52585
+ } catch {
52586
+ return void 0;
52587
+ }
52588
+ }
52589
+ function header2(req, name) {
52590
+ const v = req.headers[name];
52591
+ return Array.isArray(v) ? v[0] : v;
52592
+ }
52593
+ function originAllowed(req, opts) {
52594
+ const site = header2(req, "sec-fetch-site");
52595
+ if (site !== void 0 && site !== "same-origin" && site !== "none") return false;
52596
+ const origin = header2(req, "origin");
52597
+ if (origin === void 0) return true;
52598
+ const expected = originOf(opts.publicBaseUrl);
52599
+ if (expected !== void 0) return originOf(origin) === expected;
52600
+ const host = header2(req, "host")?.toLowerCase();
52601
+ const actual = hostOf(origin);
52602
+ return host !== void 0 && actual !== void 0 && actual === host;
52603
+ }
52604
+ function originOf(url) {
52605
+ if (!url) return void 0;
52606
+ try {
52607
+ const o = new URL(url).origin;
52608
+ return o === "null" ? void 0 : o.toLowerCase();
52609
+ } catch {
52610
+ return void 0;
52611
+ }
52612
+ }
52613
+ function isJsonContentType(req) {
52614
+ const ct = header2(req, "content-type");
52615
+ return ct !== void 0 && ct.split(";")[0].trim().toLowerCase() === "application/json";
52616
+ }
52617
+ function createCommandHttpHandler(commands, opts) {
52618
+ const maxBytes = opts.maxBodyBytes ?? MAX_BODY_BYTES2;
52619
+ const send = (res, status3, body, extra = {}) => {
52620
+ res.writeHead(status3, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...extra });
52621
+ res.end(JSON.stringify(body));
52622
+ };
52623
+ const refuse3 = (res, status3, code, error, extra = {}) => send(res, status3, { error, code }, extra);
52624
+ return async (req, res, identity2) => {
52625
+ const url = new URL(req.url ?? "/", "http://placeholder.invalid");
52626
+ const id = commandIdFromPath(url.pathname);
52627
+ const cmd = id === void 0 ? void 0 : commands.get(id);
52628
+ if (!cmd || !CommandRegistry.exposedTo(cmd, "access")) {
52629
+ refuse3(res, 404, "not_found", "unknown command");
52630
+ return;
52631
+ }
52632
+ const method = (req.method ?? "GET").toUpperCase();
52633
+ const allow = cmd.effect === "write" ? "POST" : "GET, POST";
52634
+ if (method !== "POST" && (cmd.effect === "write" || method !== "GET")) {
52635
+ refuse3(res, 405, "method_not_allowed", `${cmd.id} accepts ${allow}`, { allow });
52636
+ return;
52637
+ }
52638
+ if (method === "POST" && !isJsonContentType(req)) {
52639
+ refuse3(res, 415, "unsupported_media_type", "content-type must be application/json");
52640
+ return;
52641
+ }
52642
+ if (cmd.effect === "write" && !originAllowed(req, opts)) {
52643
+ refuse3(res, 403, "forbidden_origin", "cross-origin writes are refused");
52644
+ return;
52645
+ }
52646
+ const caller = await callerFor(identity2, opts);
52647
+ if (CommandRegistry.refuses(cmd, caller)) {
52648
+ refuse3(res, ERROR_STATUS.unauthorized, "unauthorized", `${caller.id} is not allowed to run ${cmd.id}`);
52649
+ return;
52650
+ }
52651
+ let named;
52652
+ let keys;
52653
+ if (method === "GET") {
52654
+ named = Object.fromEntries(url.searchParams);
52655
+ keys = "kebab";
52656
+ } else {
52657
+ const read = await readBody(req, maxBytes);
52658
+ if (!read.ok) {
52659
+ refuse3(res, 413, "payload_too_large", "request body too large");
52660
+ req.destroy();
52661
+ return;
52662
+ }
52663
+ let body;
52664
+ try {
52665
+ body = read.body.trim() === "" ? {} : JSON.parse(read.body);
52666
+ } catch {
52667
+ refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
52668
+ return;
52669
+ }
52670
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
52671
+ refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
52672
+ return;
52673
+ }
52674
+ named = body;
52675
+ keys = "camel";
52676
+ }
52677
+ const input = namedToInput(cmd, named, keys);
52678
+ if ("error" in input) {
52679
+ refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", input.error);
52680
+ return;
52681
+ }
52682
+ const result = await commands.invoke(cmd.id, input, caller);
52683
+ if (!result.ok) {
52684
+ refuse3(res, result.status, result.error, result.message);
52685
+ return;
52686
+ }
52687
+ send(res, 200, result.value);
52688
+ };
52689
+ }
52690
+ var API_ROOT, LOOKUP_TIMED_OUT;
52691
+ var init_commandHttp = __esm({
52692
+ "../../src/channels/commandHttp.ts"() {
52693
+ "use strict";
52694
+ init_actor();
52695
+ init_commandRegistry();
52696
+ init_commandSurface();
52697
+ init_record();
52698
+ init_accessAuth();
52699
+ init_viewAs();
52700
+ init_http();
52701
+ API_ROOT = "/api";
52702
+ LOOKUP_TIMED_OUT = /* @__PURE__ */ Symbol("channelsOf timed out");
52703
+ }
52704
+ });
52705
+
52706
+ // ../../src/channels/viewAs.ts
52707
+ function viewAsFromCookie(header4) {
52708
+ const raw = Array.isArray(header4) ? header4.join("; ") : header4;
52709
+ if (!raw) return void 0;
52710
+ for (const part of raw.split(";")) {
52711
+ const eq = part.indexOf("=");
52712
+ if (eq < 0) continue;
52713
+ if (part.slice(0, eq).trim() !== VIEW_AS_COOKIE) continue;
52714
+ try {
52715
+ const value = decodeURIComponent(part.slice(eq + 1).trim());
52716
+ if (value !== "") return value;
52717
+ } catch {
52718
+ }
52719
+ }
52720
+ return void 0;
52721
+ }
52722
+ function isViewAsPath(pathname) {
52723
+ return pathname === VIEW_AS_PATH || pathname === VIEW_AS_EXIT_PATH;
52724
+ }
52725
+ function requestersOf(rows) {
52726
+ const seen = /* @__PURE__ */ new Map();
52727
+ for (const r of rows) {
52728
+ if (!r.userId || !isViewablePerson(r.userId) || seen.has(r.userId)) continue;
52729
+ seen.set(r.userId, { id: r.userId, ...r.userName ? { name: r.userName } : {} });
52730
+ }
52731
+ return [...seen.values()].sort((a, b) => (a.name ?? a.id).localeCompare(b.name ?? b.id));
52732
+ }
52733
+ function refuseWhileViewing(res, person) {
52734
+ res.writeHead(403, JSON_NO_STORE);
52735
+ res.end(JSON.stringify({ error: "unauthorized", message: viewingRefusal(person) }));
52736
+ return true;
52737
+ }
52738
+ function cookieHeader(value, deps, clear) {
52739
+ const attrs = [
52740
+ "Path=/",
52741
+ "HttpOnly",
52742
+ "SameSite=Strict",
52743
+ ...deps.secure ? ["Secure"] : [],
52744
+ ...clear ? ["Max-Age=0"] : []
52745
+ ];
52746
+ return `${VIEW_AS_COOKIE}=${encodeURIComponent(value)}; ${attrs.join("; ")}`;
52747
+ }
52748
+ function createViewAsHandler(deps) {
52749
+ const json3 = (res, status3, body, extra = {}) => {
52750
+ res.writeHead(status3, { ...JSON_NO_STORE, ...extra });
52751
+ res.end(JSON.stringify(body));
52752
+ };
52753
+ return (req, res, ctx) => {
52754
+ const url = new URL(req.url ?? "/", "http://localhost");
52755
+ if (!isViewAsPath(url.pathname)) return false;
52756
+ if ((req.method ?? "GET").toUpperCase() !== "POST") {
52757
+ json3(res, 405, { error: "method_not_allowed" }, { allow: "POST" });
52758
+ return true;
52759
+ }
52760
+ if (!originAllowed(req, { publicBaseUrl: deps.publicBaseUrl })) {
52761
+ json3(res, 403, { error: "forbidden_origin" });
52762
+ req.destroy();
52763
+ return true;
52764
+ }
52765
+ if (!holdsAll(ctx.actor)) {
52766
+ json3(res, 403, { error: "unauthorized", message: "Only a session holding every grant may view as a person." });
52767
+ req.destroy();
52768
+ return true;
52769
+ }
52770
+ if (url.pathname === VIEW_AS_EXIT_PATH) {
52771
+ res.writeHead(204, { "set-cookie": cookieHeader("", deps, true), "cache-control": "no-store" });
52772
+ res.end();
52773
+ return true;
52774
+ }
52775
+ readBody(req, MAX_BODY_BYTES3).then((read) => {
52776
+ if (!read.ok) {
52777
+ json3(res, 413, { error: "invalid_input", message: "body too large" });
52778
+ return;
52779
+ }
52780
+ let person;
52781
+ try {
52782
+ person = JSON.parse(read.body).person;
52783
+ } catch {
52784
+ person = void 0;
52785
+ }
52786
+ if (typeof person !== "string" || !isViewablePerson(person)) {
52787
+ json3(res, 400, { error: "invalid_input", message: "person must be a Slack person id" });
52788
+ return;
52789
+ }
52790
+ res.writeHead(204, { "set-cookie": cookieHeader(person, deps, false), "cache-control": "no-store" });
52791
+ res.end();
52792
+ }).catch(() => json3(res, 400, { error: "invalid_input", message: "body unreadable" }));
52793
+ return true;
52794
+ };
52795
+ }
52796
+ var VIEW_AS_COOKIE, VIEW_AS_PATH, VIEW_AS_EXIT_PATH, MAX_BODY_BYTES3, JSON_NO_STORE;
52797
+ var init_viewAs2 = __esm({
52798
+ "../../src/channels/viewAs.ts"() {
52799
+ "use strict";
52800
+ init_viewAs();
52801
+ init_commandHttp();
52802
+ init_http();
52803
+ VIEW_AS_COOKIE = "sb-view-as";
52804
+ VIEW_AS_PATH = "/runs/view-as";
52805
+ VIEW_AS_EXIT_PATH = "/runs/view-as/exit";
52806
+ MAX_BODY_BYTES3 = 1024;
52807
+ JSON_NO_STORE = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
52808
+ }
52809
+ });
52810
+
51686
52811
  // ../../src/channels/liveView/html.ts
51687
52812
  function escapeHtml(s) {
51688
52813
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -51808,7 +52933,11 @@ function pageFavicon(seed) {
51808
52933
  }
51809
52934
  }
51810
52935
  function makeShellRenderer(assets, capabilities) {
51811
- return (title, seed) => renderShell(title, { ...seed, capabilities }, assets);
52936
+ return (viewer, title, seed) => renderShell(
52937
+ title,
52938
+ { ...seed, capabilities, ...viewer?.viewingAs ? { viewingAs: viewer.viewingAs } : {} },
52939
+ assets
52940
+ );
51812
52941
  }
51813
52942
  var PAGE_CSP, FORM_PAGE_CSP, WEB_HTML_HEADERS;
51814
52943
  var init_webShell = __esm({
@@ -52166,10 +53295,10 @@ function createLiveViewHandler(deps) {
52166
53295
  return token2 === void 0 ? v : { ...v, token: token2 };
52167
53296
  };
52168
53297
  const liveTokens = () => new Map(index.listActive().map((s) => [s.id, s.token]));
52169
- const notFoundPage = (res) => {
53298
+ const notFoundPage = (res, viewer) => {
52170
53299
  res.writeHead(404, WEB_HTML_HEADERS);
52171
53300
  res.end(
52172
- deps.shell("Run not found", {
53301
+ deps.shell(viewer, "Run not found", {
52173
53302
  page: "runNotFound",
52174
53303
  retentionDays: deps.retention ? deps.retention.retentionDays : null
52175
53304
  })
@@ -52224,6 +53353,9 @@ function createLiveViewHandler(deps) {
52224
53353
  all,
52225
53354
  mine,
52226
53355
  ...ctx.actor.asUser ? { asUser: ctx.actor.asUser } : {},
53356
+ // A session holding `all` may view the dashboard as a person (record 0053): the
53357
+ // picker offers the requesters this page names; the seed says nothing to anyone else.
53358
+ ...holdsAll(ctx.actor) ? { viewAs: { people: requestersOf(page2.rows) } } : {},
52227
53359
  retentionDays: deps.retention ? deps.retention.retentionDays : null,
52228
53360
  now: now(),
52229
53361
  rows: [...page2.rows],
@@ -52232,7 +53364,7 @@ function createLiveViewHandler(deps) {
52232
53364
  ...page2.olderThan !== void 0 ? { olderThan: page2.olderThan } : {}
52233
53365
  };
52234
53366
  res.writeHead(200, WEB_HTML_HEADERS);
52235
- res.end(deps.shell(title, seed));
53367
+ res.end(deps.shell(ctx.actor, title, seed));
52236
53368
  };
52237
53369
  if (!all) {
52238
53370
  run2(
@@ -52248,7 +53380,7 @@ function createLiveViewHandler(deps) {
52248
53380
  const scheduled = deps.scheduled;
52249
53381
  if (!scheduled) {
52250
53382
  res.writeHead(200, WEB_HTML_HEADERS);
52251
- res.end(deps.shell("Scheduled runs", { page: "scheduled", now: now(), rows: null }));
53383
+ res.end(deps.shell(ctx.actor, "Scheduled runs", { page: "scheduled", now: now(), rows: null }));
52252
53384
  return true;
52253
53385
  }
52254
53386
  const visibleTo = readableRuns(ctx.actor);
@@ -52262,7 +53394,7 @@ function createLiveViewHandler(deps) {
52262
53394
  ...firings.ok ? {} : { firingsUnavailable: firings.reason }
52263
53395
  };
52264
53396
  res.writeHead(200, WEB_HTML_HEADERS);
52265
- res.end(deps.shell("Scheduled runs", seed));
53397
+ res.end(deps.shell(ctx.actor, "Scheduled runs", seed));
52266
53398
  };
52267
53399
  if (!scheduled.store) {
52268
53400
  render3({ ok: false, reason: NO_STORE_REASON });
@@ -52276,7 +53408,7 @@ function createLiveViewHandler(deps) {
52276
53408
  run2(res, async () => {
52277
53409
  const found = await service.listUnitRuns(route3.key, visibleTo);
52278
53410
  if (!found.ok) {
52279
- notFoundPage(res);
53411
+ notFoundPage(res, ctx.actor);
52280
53412
  return;
52281
53413
  }
52282
53414
  const findings = found.value.pr ? await service.listFindings({ repo: found.value.instance.repo, number: found.value.pr.number }, visibleTo) : void 0;
@@ -52292,7 +53424,7 @@ function createLiveViewHandler(deps) {
52292
53424
  retentionDays: deps.retention ? deps.retention.retentionDays : null
52293
53425
  };
52294
53426
  res.writeHead(200, WEB_HTML_HEADERS);
52295
- res.end(deps.shell(`Unit ${found.value.id}`, seed));
53427
+ res.end(deps.shell(ctx.actor, `Unit ${found.value.id}`, seed));
52296
53428
  });
52297
53429
  return true;
52298
53430
  }
@@ -52307,7 +53439,7 @@ function createLiveViewHandler(deps) {
52307
53439
  });
52308
53440
  res.writeHead(200, WEB_HTML_HEADERS);
52309
53441
  res.end(
52310
- deps.shell("Live run", {
53442
+ deps.shell(ctx.actor, "Live run", {
52311
53443
  page: "run",
52312
53444
  mode: "live",
52313
53445
  id: route3.id,
@@ -52349,11 +53481,12 @@ function createLiveViewHandler(deps) {
52349
53481
  finished: snap.finished,
52350
53482
  window: { start: snap.receivedAt ?? snap.startedAt, end: snap.finishedAt ?? now() }
52351
53483
  });
52352
- res.writeHead(200, JSON_NO_STORE);
53484
+ res.writeHead(200, JSON_NO_STORE2);
52353
53485
  res.end(JSON.stringify({ id: route3.id, finished: snap.finished, diagnosis }));
52354
53486
  return true;
52355
53487
  }
52356
53488
  if (route3.kind === "stop") {
53489
+ if (ctx.actor.viewingAs) return refuseWhileViewing(res, ctx.actor.viewingAs);
52357
53490
  const mode = parseStopMode(url.searchParams.get("mode"));
52358
53491
  if (!mode) {
52359
53492
  text(res, 400, "mode must be soft or hard");
@@ -52365,7 +53498,7 @@ function createLiveViewHandler(deps) {
52365
53498
  else text(res, 404, NOT_FOUND2);
52366
53499
  return true;
52367
53500
  }
52368
- res.writeHead(200, JSON_NO_STORE);
53501
+ res.writeHead(200, JSON_NO_STORE2);
52369
53502
  res.end(JSON.stringify({ id: route3.id, mode: result.mode, state: "stopping" }));
52370
53503
  return true;
52371
53504
  }
@@ -52380,6 +53513,7 @@ function createLiveViewHandler(deps) {
52380
53513
  const actor = ctx.actor;
52381
53514
  const servable = (view) => view.finished || view.ownerGen !== void 0;
52382
53515
  if (route3.kind === "stop") {
53516
+ if (ctx.actor.viewingAs) return refuseWhileViewing(res, ctx.actor.viewingAs);
52383
53517
  const mode = parseStopMode(url.searchParams.get("mode"));
52384
53518
  if (!mode) {
52385
53519
  text(res, 400, "mode must be soft or hard");
@@ -52401,7 +53535,7 @@ function createLiveViewHandler(deps) {
52401
53535
  else text(res, 404, NOT_FOUND2);
52402
53536
  return;
52403
53537
  }
52404
- res.writeHead(200, JSON_NO_STORE);
53538
+ res.writeHead(200, JSON_NO_STORE2);
52405
53539
  res.end(JSON.stringify(stopped.value));
52406
53540
  });
52407
53541
  return true;
@@ -52414,7 +53548,7 @@ function createLiveViewHandler(deps) {
52414
53548
  text(res, 404, NOT_FOUND2);
52415
53549
  return;
52416
53550
  }
52417
- res.writeHead(200, JSON_NO_STORE);
53551
+ res.writeHead(200, JSON_NO_STORE2);
52418
53552
  res.end(JSON.stringify(friction.value));
52419
53553
  });
52420
53554
  return true;
@@ -52428,7 +53562,7 @@ function createLiveViewHandler(deps) {
52428
53562
  sink.write(NOT_FOUND2);
52429
53563
  sink.end();
52430
53564
  } else if (route3.kind === "page") {
52431
- notFoundPage(res);
53565
+ notFoundPage(res, ctx.actor);
52432
53566
  } else text(res, 404, NOT_FOUND2);
52433
53567
  return;
52434
53568
  }
@@ -52456,7 +53590,7 @@ function createLiveViewHandler(deps) {
52456
53590
  const tokens = liveTokens();
52457
53591
  res.writeHead(200, WEB_HTML_HEADERS);
52458
53592
  res.end(
52459
- deps.shell("Run", {
53593
+ deps.shell(ctx.actor, "Run", {
52460
53594
  page: "run",
52461
53595
  mode: "history",
52462
53596
  id: route3.id,
@@ -52492,7 +53626,7 @@ function createLiveViewHandler(deps) {
52492
53626
  return true;
52493
53627
  };
52494
53628
  }
52495
- var RESERVED_IDS, INDEX_PAGE_SIZE, RUNS_GET, RUNS_STOP, NOT_FOUND2, TEXT, JSON_NO_STORE, NO_STORE_REASON;
53629
+ var RESERVED_IDS, INDEX_PAGE_SIZE, RUNS_GET, RUNS_STOP, NOT_FOUND2, TEXT, JSON_NO_STORE2, NO_STORE_REASON;
52496
53630
  var init_liveView = __esm({
52497
53631
  "../../src/channels/liveView.ts"() {
52498
53632
  "use strict";
@@ -52508,19 +53642,21 @@ var init_liveView = __esm({
52508
53642
  init_runsService();
52509
53643
  init_commandRegistry();
52510
53644
  init_scheduledPanel();
53645
+ init_viewAs();
53646
+ init_viewAs2();
52511
53647
  init_webShell();
52512
53648
  init_favicon();
52513
53649
  init_sse();
52514
53650
  init_html();
52515
53651
  init_sse();
52516
53652
  init_webSeed();
52517
- RESERVED_IDS = /* @__PURE__ */ new Set(["scheduled", "artifacts", "unit"]);
53653
+ RESERVED_IDS = /* @__PURE__ */ new Set(["scheduled", "artifacts", "unit", "view-as"]);
52518
53654
  INDEX_PAGE_SIZE = 25;
52519
53655
  RUNS_GET = { type: "command", id: "runs.get" };
52520
53656
  RUNS_STOP = { type: "command", id: "runs.stop" };
52521
53657
  NOT_FOUND2 = "run not found";
52522
53658
  TEXT = { "content-type": "text/plain; charset=utf-8" };
52523
- JSON_NO_STORE = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
53659
+ JSON_NO_STORE2 = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
52524
53660
  NO_STORE_REASON = "schedules.worker is not configured";
52525
53661
  }
52526
53662
  });
@@ -52792,7 +53928,7 @@ function createResidentsViewHandler(deps) {
52792
53928
  runs: live
52793
53929
  };
52794
53930
  res.writeHead(200, WEB_HTML_HEADERS);
52795
- res.end(shell("Resident repos", seed));
53931
+ res.end(shell(ctx.actor, "Resident repos", seed));
52796
53932
  finish2(200);
52797
53933
  return;
52798
53934
  }
@@ -52802,7 +53938,7 @@ function createResidentsViewHandler(deps) {
52802
53938
  return;
52803
53939
  }
52804
53940
  res.writeHead(200, WEB_HTML_HEADERS);
52805
- res.end(shell(route3.slug, { page: "resident", slug: route3.slug, record: record2 }));
53941
+ res.end(shell(ctx.actor, route3.slug, { page: "resident", slug: route3.slug, record: record2 }));
52806
53942
  finish2(200);
52807
53943
  });
52808
53944
  return true;
@@ -52827,368 +53963,6 @@ var init_residentsView = __esm({
52827
53963
  }
52828
53964
  });
52829
53965
 
52830
- // ../../src/channels/accessAuth.ts
52831
- import { createPublicKey, verify as cryptoVerify } from "node:crypto";
52832
- function isServiceToken(identity2) {
52833
- return identity2.sub === "" && typeof identity2.commonName === "string" && identity2.commonName !== "";
52834
- }
52835
- function decodeBase64Url(part) {
52836
- if (part === "" || /[^A-Za-z0-9_-]/.test(part)) return null;
52837
- return Buffer.from(part, "base64url");
52838
- }
52839
- function decodeJsonSegment(part) {
52840
- const buf2 = decodeBase64Url(part);
52841
- if (!buf2) return null;
52842
- try {
52843
- const parsed = JSON.parse(buf2.toString("utf8"));
52844
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
52845
- return parsed;
52846
- } catch {
52847
- return null;
52848
- }
52849
- }
52850
- async function resolveKey(kid, config, deps) {
52851
- const cache2 = deps.cache ?? new JwksCache();
52852
- const cached = cache2.get(kid, deps.now());
52853
- if (cached) return cached;
52854
- const ttlMs = (deps.ttlSeconds ?? DEFAULT_JWKS_TTL_SECONDS) * 1e3;
52855
- const minRefetchIntervalMs = deps.minRefetchIntervalMs ?? DEFAULT_MIN_REFETCH_INTERVAL_MS;
52856
- const certsUrl = `https://${config.teamDomain}${CERTS_PATH}`;
52857
- await cache2.refresh(() => deps.fetchJwks(certsUrl), deps.now, ttlMs, minRefetchIntervalMs);
52858
- return cache2.get(kid, deps.now());
52859
- }
52860
- function claimsValid(claims, config, now) {
52861
- const nowSec = Math.floor(now / 1e3);
52862
- if (claims.iss !== `https://${config.teamDomain}`) return false;
52863
- const aud = claims.aud;
52864
- const audOk = typeof aud === "string" ? aud === config.aud : Array.isArray(aud) && aud.includes(config.aud);
52865
- if (!audOk) return false;
52866
- if (typeof claims.exp !== "number" || claims.exp <= nowSec) return false;
52867
- if (typeof claims.nbf === "number" && claims.nbf > nowSec + SKEW_SECONDS) return false;
52868
- if (typeof claims.iat === "number" && claims.iat > nowSec + SKEW_SECONDS) return false;
52869
- return hasSubject(claims);
52870
- }
52871
- function hasSubject(claims) {
52872
- if (typeof claims.sub === "string" && claims.sub !== "") return true;
52873
- const subEmpty = claims.sub === void 0 || claims.sub === "";
52874
- return subEmpty && typeof claims.common_name === "string" && claims.common_name !== "";
52875
- }
52876
- async function verifyAccessJwt(token2, config, deps) {
52877
- if (typeof token2 !== "string") return null;
52878
- const parts = token2.split(".");
52879
- if (parts.length !== 3) return null;
52880
- const [rawHeader, rawPayload, rawSignature] = parts;
52881
- const header4 = decodeJsonSegment(rawHeader);
52882
- const payload = decodeJsonSegment(rawPayload);
52883
- if (!header4 || !payload) return null;
52884
- if (header4.alg !== "RS256") return null;
52885
- if (typeof header4.kid !== "string" || header4.kid === "") return null;
52886
- const signature = decodeBase64Url(rawSignature);
52887
- if (!signature) return null;
52888
- const jwk = await resolveKey(header4.kid, config, deps);
52889
- if (!jwk) return null;
52890
- let signatureOk;
52891
- try {
52892
- const key = createPublicKey({ format: "jwk", key: jwk });
52893
- signatureOk = cryptoVerify("RSA-SHA256", Buffer.from(`${rawHeader}.${rawPayload}`), key, signature);
52894
- } catch {
52895
- return null;
52896
- }
52897
- if (!signatureOk) return null;
52898
- if (!claimsValid(payload, config, deps.now())) return null;
52899
- if (typeof payload.sub !== "string" || payload.sub === "") {
52900
- return { sub: "", commonName: payload.common_name };
52901
- }
52902
- return {
52903
- sub: payload.sub,
52904
- email: typeof payload.email === "string" ? payload.email : void 0
52905
- };
52906
- }
52907
- function normalizeTeamDomain(raw) {
52908
- return raw.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
52909
- }
52910
- function parseAccessConfig(env) {
52911
- const teamDomain = normalizeTeamDomain(env.ACCESS_TEAM_DOMAIN ?? "");
52912
- const aud = (env.ACCESS_AUD ?? "").trim();
52913
- if (teamDomain === "" || aud === "") return null;
52914
- return { teamDomain, aud };
52915
- }
52916
- var CERTS_PATH, DEFAULT_JWKS_TTL_SECONDS, DEFAULT_MIN_REFETCH_INTERVAL_MS, SKEW_SECONDS, httpJwksFetcher, JwksCache;
52917
- var init_accessAuth = __esm({
52918
- "../../src/channels/accessAuth.ts"() {
52919
- "use strict";
52920
- CERTS_PATH = "/cdn-cgi/access/certs";
52921
- DEFAULT_JWKS_TTL_SECONDS = 3600;
52922
- DEFAULT_MIN_REFETCH_INTERVAL_MS = 3e4;
52923
- SKEW_SECONDS = 60;
52924
- httpJwksFetcher = async (certsUrl) => {
52925
- const res = await fetch(certsUrl);
52926
- if (!res.ok) throw new Error(`JWKS fetch failed: HTTP ${res.status}`);
52927
- const body = await res.json();
52928
- const keys = body?.keys;
52929
- if (!Array.isArray(keys)) throw new Error("JWKS response missing keys[]");
52930
- return keys;
52931
- };
52932
- JwksCache = class {
52933
- entries = /* @__PURE__ */ new Map();
52934
- /** Ms of the last SUCCESSFUL JWKS fetch, or null before the first one. Drives
52935
- * the negative-cache interval in `refresh`; a failed fetch does not set it. */
52936
- lastFetchAt = null;
52937
- /** The in-flight refresh shared by concurrent callers (single-flight); null
52938
- * when no fetch is running. It never rejects — `runFetch` swallows failures to
52939
- * fail closed — so both the owner and any joiners resolve cleanly. */
52940
- inFlight = null;
52941
- /** The fresh JWK for `kid`, or null if absent/expired (expired entries are
52942
- * evicted lazily). `now` is milliseconds. */
52943
- get(kid, now) {
52944
- const hit = this.entries.get(kid);
52945
- if (!hit) return null;
52946
- if (hit.expiresAt <= now) {
52947
- this.entries.delete(kid);
52948
- return null;
52949
- }
52950
- return hit.jwk;
52951
- }
52952
- /** Store every kid'd key from a freshly fetched JWKS with the given absolute
52953
- * expiry (ms). Keyless entries are skipped. */
52954
- put(keys, expiresAt) {
52955
- for (const jwk of keys) {
52956
- if (typeof jwk.kid === "string" && jwk.kid !== "") {
52957
- this.entries.set(jwk.kid, { jwk, expiresAt });
52958
- }
52959
- }
52960
- }
52961
- /**
52962
- * Refresh the JWKS to (try to) learn an unknown kid, with two guards against
52963
- * the unknown-kid amplification vector:
52964
- * - single-flight: if a refresh is already running, join it instead of
52965
- * firing another — N concurrent unknown kids ⇒ at most one `fetch` call.
52966
- * - negative cache: once a fetch has succeeded, another is suppressed until
52967
- * `minRefetchIntervalMs` has elapsed, so an unknown kid resolves to null
52968
- * WITHOUT a network round-trip inside that window. A genuinely rotated kid
52969
- * is still picked up on the first refresh after the interval.
52970
- * Fail-closed: a fetch failure leaves the store unchanged and does not advance
52971
- * the last-fetch time. `now` is read lazily (ms) so the injected clock can move
52972
- * across the await.
52973
- */
52974
- async refresh(fetch2, now, ttlMs, minRefetchIntervalMs) {
52975
- if (this.inFlight) {
52976
- await this.inFlight;
52977
- return;
52978
- }
52979
- if (this.lastFetchAt !== null && now() - this.lastFetchAt < minRefetchIntervalMs) {
52980
- return;
52981
- }
52982
- this.inFlight = this.runFetch(fetch2, now, ttlMs);
52983
- try {
52984
- await this.inFlight;
52985
- } finally {
52986
- this.inFlight = null;
52987
- }
52988
- }
52989
- /** Perform one JWKS fetch and fold the result into the store. Never rejects:
52990
- * on any failure the store is left unchanged (fail closed) and `lastFetchAt`
52991
- * is not advanced, so `resolveKey` returns null and a later verify may retry. */
52992
- async runFetch(fetch2, now, ttlMs) {
52993
- try {
52994
- const keys = await fetch2();
52995
- const fetchedAt = now();
52996
- this.put(keys, fetchedAt + ttlMs);
52997
- this.lastFetchAt = fetchedAt;
52998
- } catch {
52999
- }
53000
- }
53001
- };
53002
- }
53003
- });
53004
-
53005
- // ../../src/channels/commandHttp.ts
53006
- function normalizePath2(pathname) {
53007
- let decoded = pathname;
53008
- try {
53009
- decoded = decodeURIComponent(pathname);
53010
- } catch {
53011
- }
53012
- return decoded.replace(/\/{2,}/g, "/");
53013
- }
53014
- function isCommandPath(pathname) {
53015
- const p = normalizePath2(pathname).toLowerCase();
53016
- return p === API_ROOT || p === `${API_ROOT}/` || p.startsWith(`${API_ROOT}/`);
53017
- }
53018
- function commandIdFromPath(pathname) {
53019
- const p = normalizePath2(pathname);
53020
- if (!p.startsWith(`${API_ROOT}/`)) return void 0;
53021
- const rest = p.slice(API_ROOT.length + 1);
53022
- return COMMAND_ID.test(rest) ? rest : void 0;
53023
- }
53024
- function callerIdFor(identity2) {
53025
- return isServiceToken(identity2) ? `access:svc:${identity2.commonName}` : `access:${identity2.sub}`;
53026
- }
53027
- function accessActor(identity2, grantsFor) {
53028
- return isServiceToken(identity2) ? resolveActor({ surface: "access-service", subjectId: identity2.commonName }, grantsFor) : resolveActor({ surface: "access-browser", subjectId: identity2.sub }, grantsFor);
53029
- }
53030
- function serviceTokenAllowed(pathname, identity2) {
53031
- return !isServiceToken(identity2) || isCommandPath(pathname);
53032
- }
53033
- async function resolveAccessActor(identity2, opts) {
53034
- const actor = accessActor(identity2, opts.grantsFor);
53035
- if (isServiceToken(identity2) || !identity2.email || !opts.personByEmail) return actor;
53036
- const person = await opts.personByEmail(identity2.email).catch(() => void 0);
53037
- if (!person || !person.id.startsWith("slack:")) return actor;
53038
- const memberOf = opts.channelsOf ? await boundedChannels(opts.channelsOf(person.id), opts.channelsOfTimeoutMs ?? CHANNEL_DIRECTORY_TIMEOUT_MS) : "unknown";
53039
- return {
53040
- ...actor,
53041
- self: [actor.id, person.id],
53042
- asUser: { id: person.id, ...person.name ? { name: person.name } : {} },
53043
- ...memberOf === "unknown" ? {} : { memberOf }
53044
- };
53045
- }
53046
- async function boundedChannels(lookup2, timeoutMs) {
53047
- let timer;
53048
- try {
53049
- const answer = await Promise.race([
53050
- lookup2.catch(() => "unknown"),
53051
- new Promise((resolve13) => {
53052
- timer = setTimeout(() => resolve13(LOOKUP_TIMED_OUT), timeoutMs);
53053
- })
53054
- ]);
53055
- return answer === LOOKUP_TIMED_OUT ? "unknown" : answer;
53056
- } finally {
53057
- if (timer) clearTimeout(timer);
53058
- }
53059
- }
53060
- async function callerFor(identity2, opts) {
53061
- return {
53062
- kind: "access",
53063
- id: callerIdFor(identity2),
53064
- actor: await resolveAccessActor(identity2, opts),
53065
- ...identity2.email && !isServiceToken(identity2) ? { email: identity2.email } : {}
53066
- };
53067
- }
53068
- function hostOf(url) {
53069
- if (!url) return void 0;
53070
- try {
53071
- return new URL(url).host.toLowerCase();
53072
- } catch {
53073
- return void 0;
53074
- }
53075
- }
53076
- function header2(req, name) {
53077
- const v = req.headers[name];
53078
- return Array.isArray(v) ? v[0] : v;
53079
- }
53080
- function originAllowed(req, opts) {
53081
- const site = header2(req, "sec-fetch-site");
53082
- if (site !== void 0 && site !== "same-origin" && site !== "none") return false;
53083
- const origin = header2(req, "origin");
53084
- if (origin === void 0) return true;
53085
- const expected = originOf(opts.publicBaseUrl);
53086
- if (expected !== void 0) return originOf(origin) === expected;
53087
- const host = header2(req, "host")?.toLowerCase();
53088
- const actual = hostOf(origin);
53089
- return host !== void 0 && actual !== void 0 && actual === host;
53090
- }
53091
- function originOf(url) {
53092
- if (!url) return void 0;
53093
- try {
53094
- const o = new URL(url).origin;
53095
- return o === "null" ? void 0 : o.toLowerCase();
53096
- } catch {
53097
- return void 0;
53098
- }
53099
- }
53100
- function isJsonContentType(req) {
53101
- const ct = header2(req, "content-type");
53102
- return ct !== void 0 && ct.split(";")[0].trim().toLowerCase() === "application/json";
53103
- }
53104
- function createCommandHttpHandler(commands, opts) {
53105
- const maxBytes = opts.maxBodyBytes ?? MAX_BODY_BYTES2;
53106
- const send = (res, status3, body, extra = {}) => {
53107
- res.writeHead(status3, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...extra });
53108
- res.end(JSON.stringify(body));
53109
- };
53110
- const refuse3 = (res, status3, code, error, extra = {}) => send(res, status3, { error, code }, extra);
53111
- return async (req, res, identity2) => {
53112
- const url = new URL(req.url ?? "/", "http://placeholder.invalid");
53113
- const id = commandIdFromPath(url.pathname);
53114
- const cmd = id === void 0 ? void 0 : commands.get(id);
53115
- if (!cmd || !CommandRegistry.exposedTo(cmd, "access")) {
53116
- refuse3(res, 404, "not_found", "unknown command");
53117
- return;
53118
- }
53119
- const method = (req.method ?? "GET").toUpperCase();
53120
- const allow = cmd.effect === "write" ? "POST" : "GET, POST";
53121
- if (method !== "POST" && (cmd.effect === "write" || method !== "GET")) {
53122
- refuse3(res, 405, "method_not_allowed", `${cmd.id} accepts ${allow}`, { allow });
53123
- return;
53124
- }
53125
- if (method === "POST" && !isJsonContentType(req)) {
53126
- refuse3(res, 415, "unsupported_media_type", "content-type must be application/json");
53127
- return;
53128
- }
53129
- if (cmd.effect === "write" && !originAllowed(req, opts)) {
53130
- refuse3(res, 403, "forbidden_origin", "cross-origin writes are refused");
53131
- return;
53132
- }
53133
- const caller = await callerFor(identity2, opts);
53134
- if (CommandRegistry.refuses(cmd, caller)) {
53135
- refuse3(res, ERROR_STATUS.unauthorized, "unauthorized", `${caller.id} is not allowed to run ${cmd.id}`);
53136
- return;
53137
- }
53138
- let named;
53139
- let keys;
53140
- if (method === "GET") {
53141
- named = Object.fromEntries(url.searchParams);
53142
- keys = "kebab";
53143
- } else {
53144
- const read = await readBody(req, maxBytes);
53145
- if (!read.ok) {
53146
- refuse3(res, 413, "payload_too_large", "request body too large");
53147
- req.destroy();
53148
- return;
53149
- }
53150
- let body;
53151
- try {
53152
- body = read.body.trim() === "" ? {} : JSON.parse(read.body);
53153
- } catch {
53154
- refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
53155
- return;
53156
- }
53157
- if (typeof body !== "object" || body === null || Array.isArray(body)) {
53158
- refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
53159
- return;
53160
- }
53161
- named = body;
53162
- keys = "camel";
53163
- }
53164
- const input = namedToInput(cmd, named, keys);
53165
- if ("error" in input) {
53166
- refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", input.error);
53167
- return;
53168
- }
53169
- const result = await commands.invoke(cmd.id, input, caller);
53170
- if (!result.ok) {
53171
- refuse3(res, result.status, result.error, result.message);
53172
- return;
53173
- }
53174
- send(res, 200, result.value);
53175
- };
53176
- }
53177
- var API_ROOT, LOOKUP_TIMED_OUT;
53178
- var init_commandHttp = __esm({
53179
- "../../src/channels/commandHttp.ts"() {
53180
- "use strict";
53181
- init_actor();
53182
- init_commandRegistry();
53183
- init_commandSurface();
53184
- init_record();
53185
- init_accessAuth();
53186
- init_http();
53187
- API_ROOT = "/api";
53188
- LOOKUP_TIMED_OUT = /* @__PURE__ */ Symbol("channelsOf timed out");
53189
- }
53190
- });
53191
-
53192
53966
  // ../../src/channels/web.ts
53193
53967
  import { randomBytes as randomBytes6 } from "node:crypto";
53194
53968
  function parseThreadsRoute(pathname) {
@@ -53428,6 +54202,11 @@ function createWebChatHandler(deps) {
53428
54202
  req.destroy();
53429
54203
  return;
53430
54204
  }
54205
+ if (ctx.actor.viewingAs) {
54206
+ answer(403, { error: "unauthorized", message: viewingRefusal(ctx.actor.viewingAs) });
54207
+ req.destroy();
54208
+ return;
54209
+ }
53431
54210
  const sub = subOf(ctx.actor);
53432
54211
  const threadKey = threadKeyFor(sub, id);
53433
54212
  if (!ownLane(sub, threadKey)) {
@@ -53512,7 +54291,7 @@ function createWebChatHandler(deps) {
53512
54291
  }
53513
54292
  const render3 = (title, seed) => {
53514
54293
  res.writeHead(200, WEB_HTML_HEADERS);
53515
- res.end(deps.shell(title, seed));
54294
+ res.end(deps.shell(ctx.actor, title, seed));
53516
54295
  };
53517
54296
  const sub = subOf(ctx.actor);
53518
54297
  if (route3.kind === "new") {
@@ -53524,7 +54303,7 @@ function createWebChatHandler(deps) {
53524
54303
  turnsOf(threadKey, ctx.actor).then(async (open2) => {
53525
54304
  if (open2.runs.length === 0 && !ownLane(sub, threadKey)) {
53526
54305
  res.writeHead(404, WEB_HTML_HEADERS);
53527
- res.end(deps.shell("Run not found", { page: "runNotFound", retentionDays }));
54306
+ res.end(deps.shell(ctx.actor, "Run not found", { page: "runNotFound", retentionDays }));
53528
54307
  return;
53529
54308
  }
53530
54309
  const seed = await seedFor(ctx, route3.id, threadKey, open2);
@@ -53549,6 +54328,7 @@ var init_web2 = __esm({
53549
54328
  init_http();
53550
54329
  init_viewer();
53551
54330
  init_webShell();
54331
+ init_viewAs();
53552
54332
  PLATFORM5 = "web";
53553
54333
  CONVERSATION_ID_BYTES = 10;
53554
54334
  CONVERSATION_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
@@ -53681,7 +54461,7 @@ function createCostsViewHandler(service, shell, opts = {}) {
53681
54461
  Promise.all([orNone(service.report(group, days)), by]).then(([report2, byReport2]) => {
53682
54462
  res.writeHead(200, WEB_HTML_HEADERS);
53683
54463
  res.end(
53684
- shell(`${report2?.label ?? group} spend`, {
54464
+ shell(ctx.actor, `${report2?.label ?? group} spend`, {
53685
54465
  page: "costs",
53686
54466
  group,
53687
54467
  report: report2,
@@ -53786,7 +54566,7 @@ function createDeliveryViewHandler(deps, shell) {
53786
54566
  res.end(body);
53787
54567
  return;
53788
54568
  }
53789
- const html = shell(`${report2.repo} delivery`, { page: "delivery", report: report2, repos });
54569
+ const html = shell(ctx.actor, `${report2.repo} delivery`, { page: "delivery", report: report2, repos });
53790
54570
  res.writeHead(200, WEB_HTML_HEADERS);
53791
54571
  res.end(html);
53792
54572
  }).catch((err2) => {
@@ -53963,9 +54743,9 @@ function createSettingsViewHandler(deps, shell) {
53963
54743
  }
53964
54744
  plain4(res, 502, `settings unavailable: ${reason2}`);
53965
54745
  };
53966
- const render3 = (seed) => {
54746
+ const render3 = (viewer, seed) => {
53967
54747
  res.writeHead(200, WEB_HTML_HEADERS);
53968
- res.end(shell("Settings", seed));
54748
+ res.end(shell(viewer, "Settings", seed));
53969
54749
  };
53970
54750
  const channel = "channel" in route3 ? route3.channel : void 0;
53971
54751
  deps.callerFor(ctx.identity).then(async (caller) => {
@@ -53981,11 +54761,12 @@ function createSettingsViewHandler(deps, shell) {
53981
54761
  plain4(res, 403, "forbidden");
53982
54762
  return;
53983
54763
  }
53984
- render3({ ...base, installation: deps.installation() });
54764
+ render3(caller.actor, { ...base, installation: deps.installation() });
53985
54765
  return;
53986
54766
  }
53987
54767
  const part = tab === "mcps" ? await mcpsSeed(caller, channel) : await channelsSeed(caller, channel);
53988
54768
  render3(
54769
+ caller.actor,
53989
54770
  tab === "mcps" ? { ...base, mcps: part } : { ...base, channels: part }
53990
54771
  );
53991
54772
  }).catch(failed);
@@ -55148,7 +55929,7 @@ function handleAdminModelProxyBearer(req, res, deps) {
55148
55929
  req.destroy();
55149
55930
  return;
55150
55931
  }
55151
- const read = await readBody(req, MAX_BODY_BYTES3);
55932
+ const read = await readBody(req, MAX_BODY_BYTES4);
55152
55933
  if (!read.ok) {
55153
55934
  json3(413, { ok: false, error: "request body too large" });
55154
55935
  return;
@@ -55189,7 +55970,7 @@ function handleAdminModelProxyBearer(req, res, deps) {
55189
55970
  });
55190
55971
  })();
55191
55972
  }
55192
- var MODEL_PROXY_BEARER_PATH, MODEL_PROXY_BEARER_SCOPE, MAX_BODY_BYTES3;
55973
+ var MODEL_PROXY_BEARER_PATH, MODEL_PROXY_BEARER_SCOPE, MAX_BODY_BYTES4;
55193
55974
  var init_adminModelProxy = __esm({
55194
55975
  "../../src/channels/adminModelProxy.ts"() {
55195
55976
  "use strict";
@@ -55199,7 +55980,7 @@ var init_adminModelProxy = __esm({
55199
55980
  init_modelProxy();
55200
55981
  MODEL_PROXY_BEARER_PATH = "/admin/model-proxy/bearer";
55201
55982
  MODEL_PROXY_BEARER_SCOPE = "deploy:write";
55202
- MAX_BODY_BYTES3 = 4096;
55983
+ MAX_BODY_BYTES4 = 4096;
55203
55984
  }
55204
55985
  });
55205
55986
 
@@ -55473,7 +56254,8 @@ ${task}` : task;
55473
56254
  ...prior ? { prior } : {}
55474
56255
  });
55475
56256
  const contract = await contractFor(instance, unit, readers);
55476
- return { preset: "review", prompt: `${prUrl(instance.repo, brief.pr)}
56257
+ const severity = `severity:${instance.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY}`;
56258
+ return { preset: "review", prompt: `${prUrl(instance.repo, brief.pr)} ${severity}
55477
56259
 
55478
56260
  ${turn}`, contract };
55479
56261
  }
@@ -56758,7 +57540,7 @@ function createGithubWebhookHandler(deps) {
56758
57540
  res.end(JSON.stringify(body2));
56759
57541
  };
56760
57542
  if (req.method !== "POST") return answer(405, { error: "method_not_allowed" });
56761
- const body = await readBody(req, MAX_BODY_BYTES4);
57543
+ const body = await readBody(req, MAX_BODY_BYTES5);
56762
57544
  if (!body.ok) return answer(413, { error: "too_large" });
56763
57545
  const result = await handleCheckRunIntake(
56764
57546
  { event: one(req.headers[GITHUB_EVENT_HEADER]), signature: one(req.headers[GITHUB_SIGNATURE_HEADER]) },
@@ -56768,14 +57550,14 @@ function createGithubWebhookHandler(deps) {
56768
57550
  answer(result.status, result.body);
56769
57551
  };
56770
57552
  }
56771
- var GITHUB_WEBHOOK_PATH, MAX_BODY_BYTES4, one;
57553
+ var GITHUB_WEBHOOK_PATH, MAX_BODY_BYTES5, one;
56772
57554
  var init_githubWebhook = __esm({
56773
57555
  "../../src/channels/githubWebhook.ts"() {
56774
57556
  "use strict";
56775
57557
  init_checksIntake();
56776
57558
  init_http();
56777
57559
  GITHUB_WEBHOOK_PATH = "/webhooks/github";
56778
- MAX_BODY_BYTES4 = 1024 * 1024;
57560
+ MAX_BODY_BYTES5 = 1024 * 1024;
56779
57561
  one = (v) => Array.isArray(v) ? v[0] : v;
56780
57562
  }
56781
57563
  });
@@ -56892,6 +57674,7 @@ async function runBot() {
56892
57674
  };
56893
57675
  let slackChannelsOf;
56894
57676
  const channelsOf = (actorId) => slackChannelsOf ? slackChannelsOf(actorId) : Promise.resolve("unknown");
57677
+ const personName = (personId) => names.person(personId);
56895
57678
  const mcpWiring = buildMcp(config, processSecrets, {
56896
57679
  publicBaseUrl: process.env.PUBLIC_BASE_URL,
56897
57680
  resolveEmail: (userId) => slackEmailLookup ? slackEmailLookup(userId) : Promise.resolve(void 0),
@@ -57228,7 +58011,7 @@ async function runBot() {
57228
58011
  {
57229
58012
  commands,
57230
58013
  names,
57231
- callerFor: (identity2) => callerFor(identity2, { grantsFor: (id) => config.grantsFor(id), personByEmail, channelsOf }),
58014
+ callerFor: (identity2) => callerFor(identity2, { grantsFor: (id) => config.grantsFor(id), personByEmail, channelsOf, personName }),
57232
58015
  installation: () => installationSettings(config.config, capabilities),
57233
58016
  vocabulary: {
57234
58017
  agents: Object.keys(AGENTS),
@@ -57292,8 +58075,10 @@ async function runBot() {
57292
58075
  grantsFor: (id) => config.grantsFor(id),
57293
58076
  personByEmail,
57294
58077
  channelsOf,
58078
+ personName,
57295
58079
  publicBaseUrl
57296
58080
  });
58081
+ const viewAsRoutes = createViewAsHandler({ publicBaseUrl, secure: publicBaseUrl?.startsWith("https://") ?? false });
57297
58082
  const commandHttpState = `GET|POST /api/<group>.<verb> (${commands.list().length} commands)`;
57298
58083
  const mcpConnectView = createMcpConnectViewHandler({
57299
58084
  registry: () => mcpWiring.service,
@@ -57374,19 +58159,23 @@ async function runBot() {
57374
58159
  res.end("forbidden");
57375
58160
  return;
57376
58161
  }
57377
- if (isCommandPath(path)) return commandHttp(req, res, gate.identity);
57378
- const actor = await resolveAccessActor(gate.identity, {
58162
+ const viewAs = viewAsFromCookie(req.headers.cookie);
58163
+ const identity2 = viewAs !== void 0 ? { ...gate.identity, viewAs } : gate.identity;
58164
+ if (isCommandPath(path)) return commandHttp(req, res, identity2);
58165
+ const actor = await resolveAccessActor(identity2, {
57379
58166
  grantsFor: (id) => config.grantsFor(id),
57380
58167
  personByEmail,
57381
- channelsOf
58168
+ channelsOf,
58169
+ personName
57382
58170
  });
58171
+ if (viewAsRoutes(req, res, { actor })) return;
57383
58172
  if (liveView2(req, res, { actor })) return;
57384
- if (webChat(req, res, { actor, identity: gate.identity })) return;
58173
+ if (webChat(req, res, { actor, identity: identity2 })) return;
57385
58174
  if (mcpConnectView(req, res, gate.identity)) return;
57386
58175
  if (residentsView(req, res, { actor })) return;
57387
- if (costsView(req, res, { identity: gate.identity, actor })) return;
58176
+ if (costsView(req, res, { identity: identity2, actor })) return;
57388
58177
  if (deliveryView(req, res, { actor })) return;
57389
- if (settingsView(req, res, { identity: gate.identity })) return;
58178
+ if (settingsView(req, res, { identity: identity2 })) return;
57390
58179
  res.writeHead(200, { "content-type": "text/plain" });
57391
58180
  res.end("ok");
57392
58181
  }).catch((err2) => {
@@ -57629,6 +58418,7 @@ var init_index = __esm({
57629
58418
  init_source();
57630
58419
  init_confirmations();
57631
58420
  init_mcpConnectView();
58421
+ init_viewAs2();
57632
58422
  init_lookups();
57633
58423
  init_slackNames();
57634
58424
  init_names();