@coreplane/switchboard 1.246.0 → 1.247.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 (144) hide show
  1. package/dist/assets/config/config.example.yaml +1 -1
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +14 -4
  3. package/dist/assets/deploy/cloudflare-resident/worker.ts +1 -1
  4. package/dist/assets/package-lock.json +3 -3
  5. package/dist/assets/package.json +5 -3
  6. package/dist/assets/project.json +10 -2
  7. package/dist/assets/source.json +3 -3
  8. package/dist/assets/src/agents/registry.ts +1 -1
  9. package/dist/assets/src/core/budgets.ts +19 -7
  10. package/dist/assets/src/core/runEvents.ts +5 -1
  11. package/dist/assets/src/core/ship/contract.ts +7 -0
  12. package/dist/assets/src/core/ship/coordinator.ts +1 -1
  13. package/dist/assets/src/core/trace/attrs.ts +9 -0
  14. package/dist/assets/web/dist/.vite/manifest.json +474 -485
  15. package/dist/assets/web/dist/assets/AppShell-DAtHiuI6.js +1 -0
  16. package/dist/assets/web/dist/assets/CostsPage-BoKzYa4B.js +2 -0
  17. package/dist/assets/web/dist/assets/DeliveryPage-NP4g6bQd.js +1 -0
  18. package/dist/assets/web/dist/assets/HomePage-DYxC0izY.js +2 -0
  19. package/dist/assets/web/dist/assets/InputMenu-C6aPpe30.js +1 -0
  20. package/dist/assets/web/dist/assets/NotFoundPage-DtE-GgTk.js +1 -0
  21. package/dist/assets/web/dist/assets/PendingTurnRow-BuRre8it.js +1 -0
  22. package/dist/assets/web/dist/assets/ResidentDetailPage-DLIpWYOc.js +1 -0
  23. package/dist/assets/web/dist/assets/ResidentsIndexPage-6LipuDjR.js +1 -0
  24. package/dist/assets/web/dist/assets/RunFoldRow-V-iSy64e.js +1 -0
  25. package/dist/assets/web/dist/assets/RunRoutePage-DUalB1u2.js +9 -0
  26. package/dist/assets/web/dist/assets/RunsIndexPage-B9Ba1KdD.js +1 -0
  27. package/dist/assets/web/dist/assets/RunsTabs-YSbUu5py.js +1 -0
  28. package/dist/assets/web/dist/assets/ScheduledPage-KdjLtD_7.js +1 -0
  29. package/dist/assets/web/dist/assets/SettingSelect-BIzAsLk1.js +1 -0
  30. package/dist/assets/web/dist/assets/SettingsPage-IT5l_NaL.js +1 -0
  31. package/dist/assets/web/dist/assets/StatusDot-DBHAl4Il.js +1 -0
  32. package/dist/assets/web/dist/assets/Tooltip-_LEjptLV.js +1 -0
  33. package/dist/assets/web/dist/assets/UnitRoutePage-DPBsvGPR.js +1 -0
  34. package/dist/assets/web/dist/assets/{angular-html-D_3slZqf.js → angular-html-BjeQZdCq.js} +1 -1
  35. package/dist/assets/web/dist/assets/{angular-ts-D1Fv2cpN.js → angular-ts-ZGdORrL2.js} +1 -1
  36. package/dist/assets/web/dist/assets/{apl-60Nm_trN.js → apl-DhvV_X93.js} +1 -1
  37. package/dist/assets/web/dist/assets/{astro-CvyaM3Q9.js → astro-DiRSE4Ug.js} +1 -1
  38. package/dist/assets/web/dist/assets/{blade-yan3VBls.js → blade-D8Fr5Wvq.js} +1 -1
  39. package/dist/assets/web/dist/assets/{seed-BhxEVasY.js → browser-B6T5hjfR.js} +1 -1
  40. package/dist/assets/web/dist/assets/{c-Ddgepap2.js → c-BwbC64D4.js} +1 -1
  41. package/dist/assets/web/dist/assets/{chapel-CgNY6dxC.js → chapel-DgmqRK97.js} +1 -1
  42. package/dist/assets/web/dist/assets/{cobol-C7q82cdP.js → cobol-DJod2RNK.js} +1 -1
  43. package/dist/assets/web/dist/assets/{coffee-DgRMF_ni.js → coffee-D1Y2CgBY.js} +1 -1
  44. package/dist/assets/web/dist/assets/{cpp-B8Lf-aeo.js → cpp-CsO97YOM.js} +1 -1
  45. package/dist/assets/web/dist/assets/{crystal-CkA_dhEO.js → crystal-CJ7m2tOc.js} +1 -1
  46. package/dist/assets/web/dist/assets/{css-Cyu-C_oA.js → css-B2M-NKoj.js} +1 -1
  47. package/dist/assets/web/dist/assets/{dist-DS-gMjLZ.js → dist-BcYPGOBL.js} +2 -2
  48. package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +1 -0
  49. package/dist/assets/web/dist/assets/{edge-BxV3rKNI.js → edge-CFVW-m9B.js} +1 -1
  50. package/dist/assets/web/dist/assets/{elixir-pNSEK39d.js → elixir-B2mtRgCr.js} +1 -1
  51. package/dist/assets/web/dist/assets/{elm-zgwWE33W.js → elm-2teVAwCi.js} +1 -1
  52. package/dist/assets/web/dist/assets/{erb-yzcYAHp_.js → erb-LAbeNqen.js} +1 -1
  53. package/dist/assets/web/dist/assets/eventSource-Bj_dZf6M.js +1 -0
  54. package/dist/assets/web/dist/assets/{git-rebase-Dg7Ous1-.js → git-rebase-DnhzH_sp.js} +1 -1
  55. package/dist/assets/web/dist/assets/{glimmer-js-CbZEwX27.js → glimmer-js-DPKjXf1-.js} +1 -1
  56. package/dist/assets/web/dist/assets/{glimmer-ts-D3NIqALy.js → glimmer-ts-CABXnt5z.js} +1 -1
  57. package/dist/assets/web/dist/assets/{glsl-y4-3WrI2.js → glsl-Dz4-3-gJ.js} +1 -1
  58. package/dist/assets/web/dist/assets/{graphql-D5d0al7K.js → graphql-ByWUD1DL.js} +1 -1
  59. package/dist/assets/web/dist/assets/{hack-D8fD6kEl.js → hack-CdmOK2K-.js} +1 -1
  60. package/dist/assets/web/dist/assets/{haml-BWeFFc-V.js → haml-DzegMpd2.js} +1 -1
  61. package/dist/assets/web/dist/assets/{handlebars-gIm5FeE_.js → handlebars-B5NeiP0e.js} +1 -1
  62. package/dist/assets/web/dist/assets/{html-fXJTVLSM.js → html-Zg_NHLv9.js} +1 -1
  63. package/dist/assets/web/dist/assets/{html-derivative-B3ySSBrQ.js → html-derivative-DZf9JNzh.js} +1 -1
  64. package/dist/assets/web/dist/assets/{http-DrJfQUmw.js → http-CVFrQZeN.js} +1 -1
  65. package/dist/assets/web/dist/assets/{hurl-Bvbnsf55.js → hurl-L5brWAJe.js} +1 -1
  66. package/dist/assets/web/dist/assets/{java-CUSpb-i-.js → java-hFphyZ4R.js} +1 -1
  67. package/dist/assets/web/dist/assets/{javascript-BWRH5fbY.js → javascript-CAJO1WKX.js} +1 -1
  68. package/dist/assets/web/dist/assets/{jinja-D3Ymci8y.js → jinja-N4A4srXI.js} +1 -1
  69. package/dist/assets/web/dist/assets/{jison-Gt9SO7ZC.js → jison-D3BZa_iU.js} +1 -1
  70. package/dist/assets/web/dist/assets/{json-Cfcblhgn.js → json-DxD1Qh8W.js} +1 -1
  71. package/dist/assets/web/dist/assets/{jsx-C0366fFM.js → jsx-CHhag69S.js} +1 -1
  72. package/dist/assets/web/dist/assets/{julia-CyBOTbxS.js → julia-CrhkZ6Tt.js} +1 -1
  73. package/dist/assets/web/dist/assets/{just-FPXZcIOd.js → just-D0LFOuhq.js} +1 -1
  74. package/dist/assets/web/dist/assets/{latex-Bd-c88v0.js → latex-BHAlwCXK.js} +1 -1
  75. package/dist/assets/web/dist/assets/{liquid-5tu9Hc0k.js → liquid-CkaLfCWh.js} +1 -1
  76. package/dist/assets/web/dist/assets/{lua-BZ5fdPP8.js → lua-B1h4aAzP.js} +1 -1
  77. package/dist/assets/web/dist/assets/main-DUfSE0dj.js +28 -0
  78. package/dist/assets/web/dist/assets/main-zbP_dTjR.css +1 -0
  79. package/dist/assets/web/dist/assets/{marko-C_mmj4p0.js → marko-D6CG4hvX.js} +1 -1
  80. package/dist/assets/web/dist/assets/{mdc-CtaNqWjY.js → mdc-CVQzimAU.js} +1 -1
  81. package/dist/assets/web/dist/assets/{nginx-Ddm5Mb8J.js → nginx-h-9Ir54h.js} +1 -1
  82. package/dist/assets/web/dist/assets/{nim-COhfroYB.js → nim-DHVI30sp.js} +1 -1
  83. package/dist/assets/web/dist/assets/{org-CCHFgmTb.js → org-AasAXfMF.js} +1 -1
  84. package/dist/assets/web/dist/assets/{perl-CXzOE-Z-.js → perl-BVi5aTEy.js} +1 -1
  85. package/dist/assets/web/dist/assets/{php-1V6jAIhU.js → php-qOkCeUKr.js} +1 -1
  86. package/dist/assets/web/dist/assets/{pug-uS37efts.js → pug-8TnUGvEX.js} +1 -1
  87. package/dist/assets/web/dist/assets/{qml-Bj1A0Ben.js → qml-CpJbhoxn.js} +1 -1
  88. package/dist/assets/web/dist/assets/{r-CMbKbLsS.js → r-JkUNcAJW.js} +1 -1
  89. package/dist/assets/web/dist/assets/{razor-DVRq_Ndk.js → razor-CtU4ws5a.js} +1 -1
  90. package/dist/assets/web/dist/assets/{regexp-AbaOr4oB.js → regexp-J2zA6ceN.js} +1 -1
  91. package/dist/assets/web/dist/assets/{rst-Dq_QWgOF.js → rst-CMCvf-yZ.js} +1 -1
  92. package/dist/assets/web/dist/assets/{ruby-CVSsAj7T.js → ruby-ChLBY_iR.js} +1 -1
  93. package/dist/assets/web/dist/assets/{sas-C-dGfxUi.js → sas-cwn4x2VY.js} +1 -1
  94. package/dist/assets/web/dist/assets/{scss-DxALxYPA.js → scss-jSrJlzLI.js} +1 -1
  95. package/dist/assets/web/dist/assets/{shellscript--qXcvQ5L.js → shellscript-D5nAd0gs.js} +1 -1
  96. package/dist/assets/web/dist/assets/{shellsession-DeqnW8Ux.js → shellsession-BZY4-Tdh.js} +1 -1
  97. package/dist/assets/web/dist/assets/{soy-DreoUIMt.js → soy-COppPOKr.js} +1 -1
  98. package/dist/assets/web/dist/assets/{sql-o3i2VTG6.js → sql-Blc_6dOA.js} +1 -1
  99. package/dist/assets/web/dist/assets/{sseReplay-qF-gpbAY.js → sseReplay-DXC7kGbN.js} +3 -3
  100. package/dist/assets/web/dist/assets/{stata-CGAT41H7.js → stata-DRv3OY-h.js} +1 -1
  101. package/dist/assets/web/dist/assets/{surrealql-CBU0QuGV.js → surrealql-qMv_eeJ6.js} +1 -1
  102. package/dist/assets/web/dist/assets/{svelte-CfHaLZiG.js → svelte-d1Edb6Rz.js} +1 -1
  103. package/dist/assets/web/dist/assets/{templ-D02MUcma.js → templ-CUMSIOXG.js} +1 -1
  104. package/dist/assets/web/dist/assets/{tex-DjprHYXl.js → tex-BGZQA-Ei.js} +1 -1
  105. package/dist/assets/web/dist/assets/{ts-tags-DftDOzP-.js → ts-tags-Z9ulxVsq.js} +1 -1
  106. package/dist/assets/web/dist/assets/{tsx-BOdvPohJ.js → tsx-oI4bI3Vf.js} +1 -1
  107. package/dist/assets/web/dist/assets/{twig-B_yLS56B.js → twig-D_RXwBIX.js} +1 -1
  108. package/dist/assets/web/dist/assets/{typescript-CdPCa4nU.js → typescript-BgqQDYoN.js} +1 -1
  109. package/dist/assets/web/dist/assets/{typst-C1oIFsUl.js → typst-5MW-KRDd.js} +1 -1
  110. package/dist/assets/web/dist/assets/{vue-CcszJEX8.js → vue-CZhHHsPm.js} +1 -1
  111. package/dist/assets/web/dist/assets/{vue-html-DcpxPcuT.js → vue-html-DlYy2d8v.js} +1 -1
  112. package/dist/assets/web/dist/assets/{vue-vine-D26bIKGj.js → vue-vine-CJDnx-qn.js} +1 -1
  113. package/dist/assets/web/dist/assets/wallClock-DmaC1sKA.js +1 -0
  114. package/dist/assets/web/dist/assets/{xml-D8zl6W5P.js → xml-BRBDk65Q.js} +1 -1
  115. package/dist/assets/web/dist/assets/{xsl-D86Bii28.js → xsl-BlCykPxo.js} +1 -1
  116. package/dist/assets/web/dist/assets/{yaml-g6Px4KFU.js → yaml-BqW48szf.js} +1 -1
  117. package/dist/cli.js +1120 -672
  118. package/package.json +1 -1
  119. package/dist/assets/web/dist/assets/AppShell-B4EUiSwP.js +0 -1
  120. package/dist/assets/web/dist/assets/CostsPage-D8VjAtuH.js +0 -2
  121. package/dist/assets/web/dist/assets/DeliveryPage-DpQi5XUI.js +0 -1
  122. package/dist/assets/web/dist/assets/HomePage-CvKuIGqg.js +0 -2
  123. package/dist/assets/web/dist/assets/InputMenu-DmqAxkn4.js +0 -1
  124. package/dist/assets/web/dist/assets/NotFoundPage-DDogyiXA.js +0 -1
  125. package/dist/assets/web/dist/assets/PendingTurnRow-BoSLlR6j.js +0 -1
  126. package/dist/assets/web/dist/assets/ResidentDetailPage-CiBdbzkE.js +0 -1
  127. package/dist/assets/web/dist/assets/ResidentsIndexPage-DgtHtd9K.js +0 -1
  128. package/dist/assets/web/dist/assets/RunFoldRow-IF4xbz-3.js +0 -1
  129. package/dist/assets/web/dist/assets/RunRoutePage-DHYa7rHK.js +0 -9
  130. package/dist/assets/web/dist/assets/RunsIndexPage-aWhNTv8r.js +0 -1
  131. package/dist/assets/web/dist/assets/RunsTabs-CY_tdGTU.js +0 -1
  132. package/dist/assets/web/dist/assets/ScheduledPage-gPEBZjcx.js +0 -1
  133. package/dist/assets/web/dist/assets/SettingSelect-VDanrTZi.js +0 -1
  134. package/dist/assets/web/dist/assets/SettingsPage-CFoltSRD.js +0 -1
  135. package/dist/assets/web/dist/assets/StatusDot-DA3eTVrT.js +0 -1
  136. package/dist/assets/web/dist/assets/Tooltip-BgbCkM12.js +0 -1
  137. package/dist/assets/web/dist/assets/UnitRoutePage-DibegaJJ.js +0 -1
  138. package/dist/assets/web/dist/assets/durationTone-l6aVrHb8.js +0 -1
  139. package/dist/assets/web/dist/assets/eventSource-Ce94ho6M.js +0 -1
  140. package/dist/assets/web/dist/assets/favicon-D1hkyC5j.js +0 -1
  141. package/dist/assets/web/dist/assets/main-B4A6x82d.js +0 -28
  142. package/dist/assets/web/dist/assets/main-CpkwzHX6.css +0 -1
  143. package/dist/assets/web/dist/assets/residentsModel-By1fThvk.js +0 -1
  144. package/dist/assets/web/dist/assets/wallClock-DPTuP2FD.js +0 -1
package/dist/cli.js CHANGED
@@ -1204,6 +1204,22 @@ var init_grants = __esm({
1204
1204
  }
1205
1205
  });
1206
1206
 
1207
+ // ../../src/core/authz/viewAs.ts
1208
+ function holdsAll(actor) {
1209
+ return actor.grants.actions === "all" && actor.grants.channels === "all" && actor.grants.repos === "all";
1210
+ }
1211
+ function isViewablePerson(id) {
1212
+ return /^slack:U[A-Z0-9]+$/.test(id);
1213
+ }
1214
+ function viewingRefusal(person) {
1215
+ return `You are viewing as ${person.name ?? person.id}; writes are your own to make \u2014 exit view-as to write.`;
1216
+ }
1217
+ var init_viewAs = __esm({
1218
+ "../../src/core/authz/viewAs.ts"() {
1219
+ "use strict";
1220
+ }
1221
+ });
1222
+
1207
1223
  // ../../src/configDocument.ts
1208
1224
  import { createHash } from "node:crypto";
1209
1225
  function parseConfigLocation(value) {
@@ -1563,16 +1579,16 @@ var init_budgets = __esm({
1563
1579
  LOOP_PRESETS = ["general", "coding", "review", "research", "explore", "conductor"];
1564
1580
  ASKS = {
1565
1581
  general: 5,
1566
- coding: 45,
1582
+ coding: 90,
1567
1583
  review: 25,
1568
- ship: 120,
1584
+ ship: 240,
1569
1585
  research: 8,
1570
1586
  explore: 120,
1571
1587
  conductor: 120
1572
1588
  };
1573
1589
  PRESET_FLOORS = {
1574
1590
  general: 2,
1575
- coding: 10,
1591
+ coding: 15,
1576
1592
  review: 5,
1577
1593
  research: 3,
1578
1594
  explore: 15,
@@ -1923,7 +1939,7 @@ ${renderGuards(contract.guards)}`
1923
1939
  }
1924
1940
  return { text, chars: text.length, dropped, overBudget: text.length > maxChars };
1925
1941
  }
1926
- var PR_TITLE_GUARD, GUARDS, CONTRACT_HEADING, CONTRACT_SECTION_HEADINGS, DEFAULT_CONTRACT_MAX_CHARS, UNIT_HEADING, ANY_HEADING, BULLET, SPEC_MENTION, ITEM_LIST, NAMED_REF, ANY_MD_MENTION, SPEC_ITEM, TITLE_MAX, APPROACH_DROPPED, DROP_NOTE;
1942
+ var PR_TITLE_GUARD, GUARDS, CONTRACT_HEADING, CONTRACT_SECTION_HEADINGS, DEFAULT_CONTRACT_MAX_CHARS, PLAN_MAX_CHARS, UNIT_HEADING, ANY_HEADING, BULLET, SPEC_MENTION, ITEM_LIST, NAMED_REF, ANY_MD_MENTION, SPEC_ITEM, TITLE_MAX, APPROACH_DROPPED, DROP_NOTE;
1927
1943
  var init_contract = __esm({
1928
1944
  "../../src/core/ship/contract.ts"() {
1929
1945
  "use strict";
@@ -1959,6 +1975,7 @@ var init_contract = __esm({
1959
1975
  guards: "### Guards"
1960
1976
  };
1961
1977
  DEFAULT_CONTRACT_MAX_CHARS = 48e3;
1978
+ PLAN_MAX_CHARS = 2e6;
1962
1979
  UNIT_HEADING = /^### (U\d+)\. (.+?)\s*$/;
1963
1980
  ANY_HEADING = /^#{2,3} /;
1964
1981
  BULLET = /^- \*\*([^*]+)\*\*:(?:\s?(.*))?$/;
@@ -2038,7 +2055,7 @@ var init_registry = __esm({
2038
2055
  init_contract();
2039
2056
  MACHINE_CLASSES = ["none", "blank", "repo-cold", "repo-resident"];
2040
2057
  IDENTITIES = ["none", "read", "write"];
2041
- 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.
2058
+ 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, and on Switchboard's own repository the tool refuses the same titles with the gate's own sentence, so fix the title and resubmit rather than pushing on. 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.
2042
2059
  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.
2043
2060
  - **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.
2044
2061
  - **TL;DR** (\`tldr\`, rendered first, \u2264300): two sentences for a reader with zero context \u2014 what this PR does and why it matters.
@@ -4451,6 +4468,7 @@ var init_config2 = __esm({
4451
4468
  "use strict";
4452
4469
  init_authorize();
4453
4470
  init_grants();
4471
+ init_viewAs();
4454
4472
  init_configDocument();
4455
4473
  init_schedules();
4456
4474
  init_registry();
@@ -4829,6 +4847,11 @@ var init_config2 = __esm({
4829
4847
  grantsFor(actorId) {
4830
4848
  return grantsIn(this.grants, actorId);
4831
4849
  }
4850
+ /** The Slack people the grants table names, in its order — whoever holds anything by name (the
4851
+ * people an admin may want to view the dashboard as, record 0053). Never a credential or a surface. */
4852
+ grantedPeople() {
4853
+ return [...this.grants.grants.keys()].filter(isViewablePerson);
4854
+ }
4832
4855
  /** Who to ask when denied — for actionable error messages: the Slack users
4833
4856
  * who hold everything, in the table's order. Slack only because
4834
4857
  * the hint is a `<@…>` mention in a chat reply; a credential granted
@@ -14525,14 +14548,14 @@ function partition(spans, input) {
14525
14548
  }
14526
14549
  if (backgroundIntervals.some(([s, e]) => s <= a && e >= b)) backgroundOnly += len;
14527
14550
  }
14528
- const counted = buckets.getting_ready + buckets.thinking + buckets.tools + buckets.finishing_up;
14551
+ const counted2 = buckets.getting_ready + buckets.thinking + buckets.tools + buckets.finishing_up;
14529
14552
  return {
14530
14553
  windowMs,
14531
14554
  gettingReadyMs: buckets.getting_ready,
14532
14555
  thinkingMs: buckets.thinking,
14533
14556
  toolsMs: buckets.tools,
14534
14557
  finishingUpMs: buckets.finishing_up,
14535
- overheadMs: Math.max(0, windowMs - counted - notRecorded - notLoaded),
14558
+ overheadMs: Math.max(0, windowMs - counted2 - notRecorded - notLoaded),
14536
14559
  notRecordedMs: notRecorded,
14537
14560
  notLoadedMs: notLoaded,
14538
14561
  backgroundOnlyMs: backgroundOnly
@@ -16805,22 +16828,6 @@ var init_runDuration = __esm({
16805
16828
  }
16806
16829
  });
16807
16830
 
16808
- // ../../src/core/authz/viewAs.ts
16809
- function holdsAll(actor) {
16810
- return actor.grants.actions === "all" && actor.grants.channels === "all" && actor.grants.repos === "all";
16811
- }
16812
- function isViewablePerson(id) {
16813
- return /^slack:U[A-Z0-9]+$/.test(id);
16814
- }
16815
- function viewingRefusal(person) {
16816
- return `You are viewing as ${person.name ?? person.id}; writes are your own to make \u2014 exit view-as to write.`;
16817
- }
16818
- var init_viewAs = __esm({
16819
- "../../src/core/authz/viewAs.ts"() {
16820
- "use strict";
16821
- }
16822
- });
16823
-
16824
16831
  // ../../src/mcp/config.ts
16825
16832
  function parseMcpSettings(raw) {
16826
16833
  if (raw === void 0 || raw === null) return void 0;
@@ -23961,7 +23968,8 @@ _(clipped by Switchboard: body exceeded ${MAX_BODY_CHARS2} chars)_` : body;
23961
23968
  }
23962
23969
  return out;
23963
23970
  }
23964
- async readFile(repo, path, ref) {
23971
+ async readFile(repo, path, ref, opts) {
23972
+ const maxChars = opts?.maxChars ?? MAX_FILE_CHARS;
23965
23973
  const q = ref ? `?ref=${encodeURIComponent(ref)}` : "";
23966
23974
  const res = await this.request("read", "GET", "contents", `/repos/${repo}/contents/${encodePath(path)}${q}`);
23967
23975
  const body = await res.json();
@@ -23981,13 +23989,13 @@ _(clipped by Switchboard: body exceeded ${MAX_BODY_CHARS2} chars)_` : body;
23981
23989
  void 0,
23982
23990
  "application/vnd.github.raw+json"
23983
23991
  );
23984
- content = await readTextCapped(raw, MAX_FILE_CHARS + 1);
23992
+ content = await readTextCapped(raw, maxChars + 1);
23985
23993
  } else content = String(body.content ?? "");
23986
23994
  if (content.includes("\0")) content = `(binary file, ${size} bytes \u2014 not shown)`;
23987
- const truncated = content.length > MAX_FILE_CHARS;
23995
+ const truncated = content.length > maxChars;
23988
23996
  return {
23989
23997
  path,
23990
- content: truncated ? content.slice(0, MAX_FILE_CHARS) : content,
23998
+ content: truncated ? content.slice(0, maxChars) : content,
23991
23999
  size,
23992
24000
  truncated,
23993
24001
  sha: String(body.sha ?? ""),
@@ -27535,6 +27543,8 @@ var init_attrs = __esm({
27535
27543
  dedupe: "string",
27536
27544
  requester: "string",
27537
27545
  outcome: "string",
27546
+ refusal: "string",
27547
+ cause: "string",
27538
27548
  count: "number",
27539
27549
  backend: "string",
27540
27550
  command: "string",
@@ -27612,6 +27622,189 @@ var init_trace = __esm({
27612
27622
  }
27613
27623
  });
27614
27624
 
27625
+ // ../../src/core/confirmations.ts
27626
+ import { randomUUID } from "node:crypto";
27627
+ import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "node:fs";
27628
+ import { dirname as dirname9, resolve as resolve10 } from "node:path";
27629
+ function newConfirmationId() {
27630
+ return randomUUID();
27631
+ }
27632
+ function confirmationFooter(scope) {
27633
+ switch (scope) {
27634
+ case "channel":
27635
+ return "confirmation required by this channel's boundary";
27636
+ case "user":
27637
+ return "confirmation required by your boundary";
27638
+ case "defaults":
27639
+ return "confirmation required by the defaults' boundary";
27640
+ case "built-in":
27641
+ return "confirmation required by the built-in default";
27642
+ default:
27643
+ return `confirmation required by the ${scope} boundary`;
27644
+ }
27645
+ }
27646
+ function renderOffer(offer) {
27647
+ return [offer.line, ...offer.risk ? [offer.risk] : [], offer.footer].join("\n");
27648
+ }
27649
+ function confirmationMessageOf(msg) {
27650
+ const { images: _images, documents: _documents, staged: _staged, ...kept } = msg;
27651
+ return kept;
27652
+ }
27653
+ function isRecord3(v) {
27654
+ return typeof v === "object" && v !== null && !Array.isArray(v);
27655
+ }
27656
+ function isMessage(v) {
27657
+ return isRecord3(v) && typeof v.channelId === "string" && typeof v.userId === "string" && typeof v.threadKey === "string" && typeof v.text === "string";
27658
+ }
27659
+ function isPendingConfirmation(v) {
27660
+ return isRecord3(v) && typeof v.id === "string" && isMessage(v.message) && typeof v.command === "string" && isRecord3(v.input) && typeof v.receipt === "string" && typeof v.risk === "string" && typeof v.footer === "string" && typeof v.model === "string";
27661
+ }
27662
+ function isConfirmation(v) {
27663
+ return isPendingConfirmation(v) && typeof v.expiresAt === "number";
27664
+ }
27665
+ function isRefusal(v) {
27666
+ return typeof v === "string" && CONFIRMATION_REFUSALS.includes(v);
27667
+ }
27668
+ function judge2(rows, id, actorIds, now, kind) {
27669
+ const row2 = rows.get(id);
27670
+ if (!row2) return { ok: false, refused: "used" };
27671
+ if (kind === "consume" && row2.expiresAt <= now) {
27672
+ rows.delete(id);
27673
+ return { ok: false, refused: "expired", row: row2 };
27674
+ }
27675
+ if (!actorIds.includes(row2.message.userId))
27676
+ return kind === "consume" ? { ok: false, refused: "foreign", row: row2 } : { ok: false, refused: "foreign" };
27677
+ rows.delete(id);
27678
+ return kind === "consume" ? { ok: true, row: row2 } : { ok: true };
27679
+ }
27680
+ function replaceThreadRow(rows, row2) {
27681
+ for (const [id, r] of rows) if (r.message.threadKey === row2.message.threadKey) rows.delete(id);
27682
+ rows.set(row2.id, row2);
27683
+ }
27684
+ function buildConfirmationStore(config, secrets, opts) {
27685
+ const worker = config.config.runtimeOverrides?.worker;
27686
+ if (!worker) return new FileConfirmationStore(opts.path);
27687
+ const tokenEnv = worker.tokenEnv ?? "MEMORY_TOKEN";
27688
+ const token2 = secrets.named(tokenEnv);
27689
+ if (!token2) throw new Error(`runtimeOverrides.worker is configured but ${tokenEnv} is not set`);
27690
+ return new WorkerConfirmationStore({
27691
+ baseUrl: worker.baseUrl,
27692
+ token: token2.reveal(),
27693
+ ...opts.fetch ? { fetch: opts.fetch } : {}
27694
+ });
27695
+ }
27696
+ var CONFIRMATION_REFUSALS, UNSHOWABLE_LINE, STORE_UNREACHABLE_NOTE, FileConfirmationStore, CONFIRMATION_WORKER_TIMEOUT_MS, WorkerConfirmationStore;
27697
+ var init_confirmations = __esm({
27698
+ "../../src/core/confirmations.ts"() {
27699
+ "use strict";
27700
+ init_clock();
27701
+ CONFIRMATION_REFUSALS = ["used", "expired", "foreign"];
27702
+ UNSHOWABLE_LINE = "this command carries a value that cannot be shown; type the line yourself";
27703
+ STORE_UNREACHABLE_NOTE = "(the confirmation store could not be reached, so there is no button to press)";
27704
+ FileConfirmationStore = class {
27705
+ path;
27706
+ clock;
27707
+ constructor(path, opts = {}) {
27708
+ this.path = resolve10(path);
27709
+ this.clock = opts.clock ?? systemClock;
27710
+ }
27711
+ read() {
27712
+ const rows = /* @__PURE__ */ new Map();
27713
+ if (!existsSync12(this.path)) return rows;
27714
+ const raw = JSON.parse(readFileSync14(this.path, "utf8"));
27715
+ for (const v of Array.isArray(raw.confirmations) ? raw.confirmations : []) if (isConfirmation(v)) rows.set(v.id, v);
27716
+ return rows;
27717
+ }
27718
+ write(rows) {
27719
+ mkdirSync9(dirname9(this.path), { recursive: true });
27720
+ writeFileSync8(this.path, JSON.stringify({ confirmations: [...rows.values()] }, null, 2));
27721
+ }
27722
+ async put(row2, ttlMs) {
27723
+ const rows = this.read();
27724
+ const stored = { ...row2, expiresAt: this.clock() + ttlMs };
27725
+ replaceThreadRow(rows, stored);
27726
+ this.write(rows);
27727
+ return stored;
27728
+ }
27729
+ async consume(id, actorIds) {
27730
+ const rows = this.read();
27731
+ const out = judge2(rows, id, actorIds, this.clock(), "consume");
27732
+ this.write(rows);
27733
+ return out;
27734
+ }
27735
+ async cancel(id, actorIds) {
27736
+ const rows = this.read();
27737
+ const out = judge2(rows, id, actorIds, this.clock(), "cancel");
27738
+ this.write(rows);
27739
+ return out;
27740
+ }
27741
+ describe() {
27742
+ return `file ${this.path}`;
27743
+ }
27744
+ };
27745
+ CONFIRMATION_WORKER_TIMEOUT_MS = 8e3;
27746
+ WorkerConfirmationStore = class {
27747
+ constructor(opts) {
27748
+ this.opts = opts;
27749
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
27750
+ this.fetchImpl = opts.fetch ?? fetch;
27751
+ }
27752
+ opts;
27753
+ baseUrl;
27754
+ fetchImpl;
27755
+ async put(row2, ttlMs) {
27756
+ const body = await this.post("/config/confirmations/put", {
27757
+ id: row2.id,
27758
+ threadKey: row2.message.threadKey,
27759
+ requester: row2.message.userId,
27760
+ body: row2,
27761
+ ttlMs
27762
+ });
27763
+ if (typeof body.expiresAt !== "number")
27764
+ throw new Error("confirmation store answered a put without the expiry it stamped");
27765
+ return { ...row2, expiresAt: body.expiresAt };
27766
+ }
27767
+ async consume(id, actorIds) {
27768
+ const body = await this.post("/config/confirmations/consume", { id, actorIds });
27769
+ const stored = isRecord3(body.row) ? body.row : void 0;
27770
+ const row2 = stored && isRecord3(stored.body) ? { ...stored.body, expiresAt: stored.expiresAt } : void 0;
27771
+ if (isRefusal(body.refused))
27772
+ return { ok: false, refused: body.refused, ...isConfirmation(row2) ? { row: row2 } : {} };
27773
+ if (!isConfirmation(row2)) throw new Error("confirmation store answered a consume outside its contract");
27774
+ return { ok: true, row: row2 };
27775
+ }
27776
+ async cancel(id, actorIds) {
27777
+ const body = await this.post("/config/confirmations/cancel", { id, actorIds });
27778
+ if (body.ok === true) return { ok: true };
27779
+ if (body.refused === "used" || body.refused === "foreign") return { ok: false, refused: body.refused };
27780
+ throw new Error("confirmation store answered a cancel outside its contract");
27781
+ }
27782
+ describe() {
27783
+ return `state Worker ${this.baseUrl} (ConfigDO confirmations)`;
27784
+ }
27785
+ async post(path, payload) {
27786
+ let res;
27787
+ try {
27788
+ res = await this.fetchImpl(`${this.baseUrl}${path}`, {
27789
+ method: "POST",
27790
+ headers: { authorization: `Bearer ${this.opts.token}`, "content-type": "application/json" },
27791
+ body: JSON.stringify(payload),
27792
+ signal: AbortSignal.timeout(CONFIRMATION_WORKER_TIMEOUT_MS)
27793
+ });
27794
+ } catch (err2) {
27795
+ throw new Error(`confirmation store unreachable: ${err2 instanceof Error ? err2.message : String(err2)}`, {
27796
+ cause: err2
27797
+ });
27798
+ }
27799
+ if (!res.ok) throw new Error(`confirmation store answered HTTP ${res.status} on ${path}`);
27800
+ const body = await res.json().catch(() => void 0);
27801
+ if (!isRecord3(body)) throw new Error(`confirmation store returned a non-JSON body on ${path}`);
27802
+ return body;
27803
+ }
27804
+ };
27805
+ }
27806
+ });
27807
+
27615
27808
  // ../../src/core/markdownDocument.ts
27616
27809
  function toMarkdownDocument(text) {
27617
27810
  return text.replace(/[\uE000\uE001]/g, "").split(/(```[\s\S]*?```)/g).map((seg, i) => i % 2 === 1 ? seg : hardBreaks(convertProse(seg))).join("");
@@ -27802,6 +27995,17 @@ function channelSegment(channelId, channelName) {
27802
27995
  function errorReply(err2) {
27803
27996
  return `\u26A0\uFE0F ${err2 instanceof Error ? err2.message : String(err2)}`;
27804
27997
  }
27998
+ async function renderRefusal(refusal, io) {
27999
+ if (refusal.cause === "policy" && refusal.wayForward) return io.reply(`${refusal.text} ${refusal.wayForward}`);
28000
+ return io.reply(refusal.text);
28001
+ }
28002
+ async function renderConfirmationOffer(io, offer) {
28003
+ if (io.offer) return io.offer(offer);
28004
+ return io.reply(renderOffer(offer));
28005
+ }
28006
+ function aRun(agentName) {
28007
+ return `${/^[aeiou]/i.test(agentName) ? "an" : "a"} *${agentName}* run`;
28008
+ }
27805
28009
  function cardLines(trace, opts) {
27806
28010
  const shape = cardShapeLine(trace.spansSoFar(), {
27807
28011
  window: { start: trace.receivedAt, end: opts.end },
@@ -27910,12 +28114,13 @@ function afterReply(deps, ctx) {
27910
28114
  answer
27911
28115
  });
27912
28116
  }
27913
- var SNIPPET_MAX, RUN_LABEL_MAX, ASSISTANT_TRACE_CAP, STATUS_PREFIXES, LONG_COMMAND_REPLY_CHARS;
28117
+ var SNIPPET_MAX, RUN_LABEL_MAX, ASSISTANT_TRACE_CAP, STATUS_PREFIXES, REFUSAL_SENTENCES, LONG_COMMAND_REPLY_CHARS;
27914
28118
  var init_reply = __esm({
27915
28119
  "../../src/core/dispatch/reply.ts"() {
27916
28120
  "use strict";
27917
28121
  init_reviewVerdict();
27918
28122
  init_channelDirectory();
28123
+ init_confirmations();
27919
28124
  init_commandSurface();
27920
28125
  init_markdownDocument();
27921
28126
  init_runShape();
@@ -27926,10 +28131,73 @@ var init_reply = __esm({
27926
28131
  RUN_LABEL_MAX = 160;
27927
28132
  ASSISTANT_TRACE_CAP = 80;
27928
28133
  STATUS_PREFIXES = ["\u23F3", "\u2705", "\u25D0", "\u25D3", "\u25D1", "\u25D2"];
28134
+ REFUSAL_SENTENCES = {
28135
+ agent_allowlist: (p) => `\u{1F6AB} You're not on the allowlist for the \`${p.agent}\` agent. Ask ${p.adminsHint} for access.`,
28136
+ live_agent_allowlist: (p) => `\u{1F6AB} You're not on the allowlist for the \`${p.agent}\` agent, whose run is in flight in this thread. Ask ${p.adminsHint} for access.`,
28137
+ elsewhere_agent_allowlist: (p) => `\u{1F6AB} You're not on the allowlist for the \`${p.agent}\` agent, whose run is in flight in this thread. Ask ${p.adminsHint} for access.`,
28138
+ repo_not_visible: (p) => `\u{1F4E6} \`${p.slug}\` is not a repository this installation can see \u2014 GitHub answered 404 \u2014 so I did not start ${aRun(p.agent)} for it. The repository is outside the Switchboard GitHub App installation (\`github_repos\` lists the reachable ones), or the name is wrong.`,
28139
+ repo_unverified: (p) => p.via === "github" ? `\u26A0\uFE0F I couldn't verify \`${p.slug}\` against GitHub \u2014 it didn't answer \u2014 so I did not start ${aRun(p.agent)} rather than guess which repository you meant. Try again in a minute.` : `\u26A0\uFE0F I couldn't verify that \`${p.slug}\` is an onboarded repo \u2014 the resident registry didn't answer \u2014 so I did not start a *${p.agent}* run rather than guess which repo you meant. Try again in a minute, or name the repository by URL (https://github.com/${p.slug}) to run in a cold per-thread sandbox.`,
28140
+ repo_not_onboarded: (p) => `\u{1F4E6} \`${p.slug}\` is not onboarded as a resident, so I did not start a *${p.agent}* run for it. ${p.onboardHint} for a warm, deps-ready environment, or name the repository by URL (https://github.com/${p.slug}) to run in a cold per-thread sandbox.`,
28141
+ repo_access: (p) => `\u{1F6AB} You're not on the allowlist for the \`${p.repo}\` repo environment. Ask ${p.adminsHint} for access.`,
28142
+ which_branch: (p) => `\u{1F33F} Which branch of \`${p.repo}\` should this thread work on? No branch is bound yet \u2014 reply naming one (e.g. "on main" or "on branch fix/login") and I'll pick it up from there.`,
28143
+ ship_budget: (p) => `\u{1F6AB} Ship cannot start under a ${p.maxMinutes}-minute budget: the loop it allows (${p.maxRounds} review rounds) needs ${p.need} minutes \u2014 ${p.provision} to provision, the coding child's ${p.coding}, and the reserve for the rounds after it at their floors. Widen the budget or the boundary that clipped it, or run \`agent:coding\` for a single pass without the review loop.`
28144
+ };
27929
28145
  LONG_COMMAND_REPLY_CHARS = 3e3;
27930
28146
  }
27931
28147
  });
27932
28148
 
28149
+ // ../../src/core/refusal.ts
28150
+ function causeOf(code) {
28151
+ return CAUSE_OF[code];
28152
+ }
28153
+ function refusalOf(code, text, extra) {
28154
+ return { cause: causeOf(code), code, text, ...extra ?? {} };
28155
+ }
28156
+ var CAUSE_OF, REFUSAL_CODES;
28157
+ var init_refusal = __esm({
28158
+ "../../src/core/refusal.ts"() {
28159
+ "use strict";
28160
+ CAUSE_OF = {
28161
+ // the dispatch gates (dispatcher.ts `refuse(code)`)
28162
+ agent_allowlist: "policy",
28163
+ profile_bounded: "policy",
28164
+ repo_not_visible: "policy",
28165
+ repo_unverified: "system",
28166
+ repo_not_onboarded: "request",
28167
+ repo_access: "policy",
28168
+ pr_head_unknown: "system",
28169
+ branch_moved: "system",
28170
+ coordinator_thread_live: "system",
28171
+ live_agent_allowlist: "policy",
28172
+ follow_up_refused: "request",
28173
+ elsewhere_agent_allowlist: "policy",
28174
+ elsewhere_follow_up_refused: "request",
28175
+ which_branch: "request",
28176
+ workspace_lost: "system",
28177
+ ship_preflight: "system",
28178
+ ship_budget: "request",
28179
+ setup_failed: "system",
28180
+ // the click on a confirmation (confirm.ts)
28181
+ confirmation_used: "request",
28182
+ confirmation_expired: "request",
28183
+ confirmation_foreign: "policy",
28184
+ confirmation_unreadable: "system",
28185
+ // the references step's eight reasons (record 0037: one sentence, eight codes)
28186
+ reference_over_cap: "request",
28187
+ reference_rate_limited: "request",
28188
+ reference_guest: "policy",
28189
+ reference_not_a_member: "policy",
28190
+ reference_denied: "policy",
28191
+ reference_timed_out: "system",
28192
+ reference_never: "system",
28193
+ reference_fetch_failed: "system",
28194
+ // the catch-all: an uncaught throw in dispatch()
28195
+ uncaught: "system"
28196
+ };
28197
+ REFUSAL_CODES = Object.keys(CAUSE_OF);
28198
+ }
28199
+ });
28200
+
27933
28201
  // ../../src/core/runHistoryWriter.ts
27934
28202
  async function putOnce(sink, record2) {
27935
28203
  try {
@@ -28344,7 +28612,20 @@ async function closeResumedRow(adopted, resume, why, status3 = "interrupted") {
28344
28612
  }
28345
28613
  }
28346
28614
  async function admit(deps, ctx) {
28347
- const { msg, io, directives, agentName, resume, restart, carriedRow, clock: clock4, root, refuse: refuse3, admission } = ctx;
28615
+ const {
28616
+ msg,
28617
+ io,
28618
+ directives,
28619
+ agentName,
28620
+ resume,
28621
+ restart,
28622
+ carriedRow,
28623
+ clock: clock4,
28624
+ root,
28625
+ refuse: refuse3,
28626
+ refuseSilently,
28627
+ admission
28628
+ } = ctx;
28348
28629
  let claim = admission.claim(msg.threadKey, {
28349
28630
  agent: agentName,
28350
28631
  ...carriedRow ? { now: carriedRow.startedAt } : {}
@@ -28391,16 +28672,16 @@ async function admit(deps, ctx) {
28391
28672
  console.log(
28392
28673
  `[dispatch] ${msg.threadKey} coordinator spawn (${ctx.coordinator.idempotencyKey}) refused: the ${claim.live.agent} run ${claim.live.runId ?? "(unnamed)"} is in flight`
28393
28674
  );
28394
- await refuse3("coordinator_thread_live", async () => {
28675
+ await refuseSilently("coordinator_thread_live", async () => {
28395
28676
  });
28396
28677
  return { kind: "refused", reason: "coordinator_thread_live" };
28397
28678
  }
28398
28679
  if (claim.kind === "live") {
28399
28680
  if (!deps.config.canRunAgent(chatActorOf(deps.config, msg), claim.live.agent)) {
28400
28681
  await refuse3(
28401
- "live_agent_allowlist",
28402
- () => io.reply(
28403
- `\u{1F6AB} You're not on the allowlist for the \`${claim.live.agent}\` agent, whose run is in flight in this thread. Ask ${deps.config.adminsHint()} for access.`
28682
+ refusalOf(
28683
+ "live_agent_allowlist",
28684
+ REFUSAL_SENTENCES.live_agent_allowlist({ agent: claim.live.agent, adminsHint: deps.config.adminsHint() })
28404
28685
  )
28405
28686
  );
28406
28687
  return { kind: "refused", reason: "live_agent_allowlist" };
@@ -28410,7 +28691,7 @@ async function admit(deps, ctx) {
28410
28691
  console.log(
28411
28692
  `[dispatch] ${msg.threadKey} follow-up refused (${decision.reason}): ${claim.live.agent} run in flight`
28412
28693
  );
28413
- await refuse3("follow_up_refused", () => io.reply(refusalReply(claim.live, decision, clock4())));
28694
+ await refuse3(refusalOf("follow_up_refused", refusalReply(claim.live, decision, clock4())));
28414
28695
  return { kind: "refused", reason: "follow_up_refused" };
28415
28696
  }
28416
28697
  const at = clock4();
@@ -28442,7 +28723,7 @@ async function admit(deps, ctx) {
28442
28723
  console.log(
28443
28724
  `[dispatch] ${msg.threadKey} coordinator spawn (${ctx.coordinator.idempotencyKey}) refused: run ${elsewhere.runId} is in flight on another generation`
28444
28725
  );
28445
- await refuse3("coordinator_thread_live", async () => {
28726
+ await refuseSilently("coordinator_thread_live", async () => {
28446
28727
  });
28447
28728
  return { kind: "refused", reason: "coordinator_thread_live" };
28448
28729
  }
@@ -28457,8 +28738,11 @@ async function admit(deps, ctx) {
28457
28738
  runId: elsewhere.runId
28458
28739
  };
28459
28740
  if (!deps.config.canRunAgent(chatActorOf(deps.config, msg), far.agent)) {
28460
- await io.reply(
28461
- `\u{1F6AB} You're not on the allowlist for the \`${far.agent}\` agent, whose run is in flight in this thread. Ask ${deps.config.adminsHint()} for access.`
28741
+ await refuse3(
28742
+ refusalOf(
28743
+ "elsewhere_agent_allowlist",
28744
+ REFUSAL_SENTENCES.elsewhere_agent_allowlist({ agent: far.agent, adminsHint: deps.config.adminsHint() })
28745
+ )
28462
28746
  );
28463
28747
  return { kind: "refused", reason: "elsewhere_agent_allowlist" };
28464
28748
  }
@@ -28468,7 +28752,7 @@ async function admit(deps, ctx) {
28468
28752
  console.log(
28469
28753
  `[dispatch] ${msg.threadKey} follow-up refused (${decision.reason}): ${far.agent} run ${far.runId} live on another generation`
28470
28754
  );
28471
- await io.reply(refusalReply(far, decision, now));
28755
+ await refuse3(refusalOf("elsewhere_follow_up_refused", refusalReply(far, decision, now)));
28472
28756
  return { kind: "refused", reason: "elsewhere_follow_up_refused" };
28473
28757
  }
28474
28758
  const seq = await deps.runLedger.pushInbox(elsewhere.runId, durableInboxMessage(msg, directives.text, now));
@@ -28595,6 +28879,8 @@ var init_admission = __esm({
28595
28879
  init_trace();
28596
28880
  init_threadAdmission();
28597
28881
  init_record();
28882
+ init_refusal();
28883
+ init_reply();
28598
28884
  init_inboxMessage();
28599
28885
  defaultAdmission = new ThreadAdmission();
28600
28886
  }
@@ -29105,10 +29391,12 @@ function extractSignals(rawText) {
29105
29391
  if (!t || t.includes("://") || t.toLowerCase().startsWith("github.com/")) continue;
29106
29392
  const prev = i > 0 ? stripPunct(tokens[i - 1]).toLowerCase() : "";
29107
29393
  if (prev === "branch") continue;
29108
- if (prev === "in" && !inCode[i] && !out.addressed) {
29394
+ const prev2 = i > 1 ? stripPunct(tokens[i - 2]).toLowerCase() : "";
29395
+ const inAt = prev === "in" ? i - 1 : DETERMINERS.has(prev) && prev2 === "in" ? i - 2 : -1;
29396
+ if (inAt >= 0 && !inCode[i] && !out.addressed && !DETERMINERS.has(t.toLowerCase())) {
29109
29397
  const slug = slugOf(t);
29110
29398
  if (slug) out.addressed = { slug };
29111
- else if (!t.includes("/") && NAME_RE.test(t) && tokens.slice(0, i - 1).every(isDirectiveOrMention))
29399
+ else if (!t.includes("/") && NAME_RE.test(t) && tokens.slice(0, inAt).every(isDirectiveOrMention))
29112
29400
  out.addressed = { name: t.toLowerCase() };
29113
29401
  }
29114
29402
  if (prev === "on") {
@@ -29420,7 +29708,7 @@ function prFacts(title, body) {
29420
29708
  if (last >= 55296 && last <= 56319) end--;
29421
29709
  return { title, body: body.slice(0, end), truncated: true };
29422
29710
  }
29423
- var PR_BODY_CAP, OWNER_RE, NAME_RE, WELL_KNOWN_REFS, CODE_SPAN, threadSignalsCache, COMPARE_FILES_CAP;
29711
+ var PR_BODY_CAP, OWNER_RE, NAME_RE, WELL_KNOWN_REFS, DETERMINERS, CODE_SPAN, threadSignalsCache, COMPARE_FILES_CAP;
29424
29712
  var init_repoContext = __esm({
29425
29713
  "../../src/core/repoContext.ts"() {
29426
29714
  "use strict";
@@ -29432,6 +29720,7 @@ var init_repoContext = __esm({
29432
29720
  OWNER_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
29433
29721
  NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
29434
29722
  WELL_KNOWN_REFS = /* @__PURE__ */ new Set(["main", "master", "develop", "trunk"]);
29723
+ DETERMINERS = /* @__PURE__ */ new Set(["the", "a", "an", "our", "my", "this"]);
29435
29724
  CODE_SPAN = /```[\s\S]*?```|`[^`\n]*`/g;
29436
29725
  threadSignalsCache = /* @__PURE__ */ new WeakMap();
29437
29726
  COMPARE_FILES_CAP = 300;
@@ -29646,7 +29935,7 @@ var init_indexFeed = __esm({
29646
29935
  });
29647
29936
 
29648
29937
  // ../../src/core/runRegistry.ts
29649
- import { randomBytes as randomBytes3, randomUUID, timingSafeEqual } from "node:crypto";
29938
+ import { randomBytes as randomBytes3, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
29650
29939
  function safeEqual(a, b) {
29651
29940
  const ab = Buffer.from(a, "utf8");
29652
29941
  const bb = Buffer.from(b, "utf8");
@@ -29685,7 +29974,7 @@ var init_runRegistry = __esm({
29685
29974
  bytes: opts.backlogBytes ?? DEFAULT_BACKLOG_BYTES
29686
29975
  };
29687
29976
  this.ttlMs = opts.ttlMs ?? 6e4;
29688
- this.genId = opts.genId ?? (() => randomUUID());
29977
+ this.genId = opts.genId ?? (() => randomUUID2());
29689
29978
  this.genToken = opts.genToken ?? (() => randomBytes3(32).toString("hex"));
29690
29979
  this.now = opts.now ?? Date.now;
29691
29980
  }
@@ -32485,6 +32774,162 @@ var init_status2 = __esm({
32485
32774
  }
32486
32775
  });
32487
32776
 
32777
+ // ../../src/core/prTitle.mjs
32778
+ function checkPrTitle(rawTitle, vocabulary) {
32779
+ const { types, scopes } = vocabulary;
32780
+ const title = (rawTitle ?? "").trim();
32781
+ if (title === "") return { ok: false, problems: ["the title is empty"] };
32782
+ const m = TITLE_GRAMMAR.exec(title);
32783
+ if (!m || !m.groups) return { ok: false, problems: [diagnose(title, types)] };
32784
+ const { type, scope, breaking, description } = m.groups;
32785
+ const problems = [];
32786
+ if (!types.includes(type)) problems.push(`unknown type "${type}" \u2014 use one of: ${types.join(", ")}`);
32787
+ if (scope !== void 0 && !scopes.includes(scope)) {
32788
+ problems.push(
32789
+ `unknown scope "${scope}" \u2014 use one of: ${scopes.join(", ")} (the Areas in ${CODE_MAP_PATH}), or no scope for a tree-wide change`
32790
+ );
32791
+ }
32792
+ if (/\.$/.test(description)) problems.push("the description ends with a period; drop it (it is a commit subject)");
32793
+ const exemptFromCap = type === "revert" || scope !== void 0 && BOT_SCOPES.includes(scope);
32794
+ if (!exemptFromCap && title.length > TITLE_MAX_LENGTH) {
32795
+ problems.push(
32796
+ `the title is ${title.length} characters; at most ${TITLE_MAX_LENGTH} \u2014 one change, one clause, present tense; the PR body carries the rest`
32797
+ );
32798
+ }
32799
+ if (problems.length > 0) return { ok: false, problems };
32800
+ return { ok: true, type, scope: scope ?? null, breaking: breaking === "!", description };
32801
+ }
32802
+ function diagnose(title, types) {
32803
+ const list = types.join(", ");
32804
+ if (/^\[?(wip|draft)\b/i.test(title))
32805
+ return `drop the "${title.split(/[\s\]:]/)[0]}" marker; mark the PR as a draft instead`;
32806
+ if (/^Revert "/.test(title))
32807
+ return 'a revert is titled `revert: <the original title>` (GitHub\'s Revert button writes `Revert "\u2026"`)';
32808
+ if (!/^[a-z]/.test(title)) return `the type must be lowercase, one of: ${list}`;
32809
+ if (!/^[a-z]+(\([^)]*\))?!?:/.test(title))
32810
+ return `start with a type and a colon \u2014 \`type: description\` or \`type(scope): description\` \u2014 where type is one of: ${list}`;
32811
+ if (/^[a-z]+\([^)]*[^a-z0-9._/)-][^)]*\)/.test(title) || /^[a-z]+\([^a-z0-9]/.test(title)) {
32812
+ return "the scope is lowercase letters, digits and . _ / - inside the parentheses, like `feat(slack): \u2026`";
32813
+ }
32814
+ if (/^[a-z]+(\([^)]*\))?!?:\s*$/.test(title)) return "add a description after the colon";
32815
+ if (/^[a-z]+(\([^)]*\))?!?:\S/.test(title)) return "put exactly one space after the colon, then the description";
32816
+ return `the title does not match \`type(scope)!: description\` with type one of: ${list}`;
32817
+ }
32818
+ var TITLE_GRAMMAR, CODE_MAP_PATH, RELEASE_CONFIG_PATH, TITLE_MAX_LENGTH, BOT_SCOPES, VOCABULARY_NOTE;
32819
+ var init_prTitle = __esm({
32820
+ "../../src/core/prTitle.mjs"() {
32821
+ "use strict";
32822
+ TITLE_GRAMMAR = /^(?<type>[a-z]+)(?:\((?<scope>[a-z0-9][a-z0-9._/-]*)\))?(?<breaking>!)?: (?<description>\S.*)$/;
32823
+ CODE_MAP_PATH = "docs/reference/code-map.md";
32824
+ RELEASE_CONFIG_PATH = "release-please-config.json";
32825
+ TITLE_MAX_LENGTH = 72;
32826
+ BOT_SCOPES = ["deps", "main"];
32827
+ VOCABULARY_NOTE = `generated by \`npm run pr-title:gen\` from ${RELEASE_CONFIG_PATH} (types) and the Areas table in ${CODE_MAP_PATH} (scopes) \u2014 do not edit by hand`;
32828
+ }
32829
+ });
32830
+
32831
+ // ../../src/core/prTitleVocabulary.json
32832
+ var prTitleVocabulary_default;
32833
+ var init_prTitleVocabulary = __esm({
32834
+ "../../src/core/prTitleVocabulary.json"() {
32835
+ prTitleVocabulary_default = {
32836
+ $generated: "generated by `npm run pr-title:gen` from release-please-config.json (types) and the Areas table in docs/reference/code-map.md (scopes) \u2014 do not edit by hand",
32837
+ types: [
32838
+ "feat",
32839
+ "fix",
32840
+ "perf",
32841
+ "revert",
32842
+ "docs",
32843
+ "style",
32844
+ "refactor",
32845
+ "test",
32846
+ "build",
32847
+ "ci",
32848
+ "chore"
32849
+ ],
32850
+ scopes: [
32851
+ "dispatcher",
32852
+ "core",
32853
+ "config",
32854
+ "commands",
32855
+ "cli",
32856
+ "init",
32857
+ "setup",
32858
+ "authz",
32859
+ "runs",
32860
+ "tracing",
32861
+ "costs",
32862
+ "delivery",
32863
+ "slack",
32864
+ "http",
32865
+ "mcp",
32866
+ "agents",
32867
+ "review",
32868
+ "coding",
32869
+ "ship",
32870
+ "research",
32871
+ "explore",
32872
+ "conductor",
32873
+ "general",
32874
+ "providers",
32875
+ "resident",
32876
+ "sandbox",
32877
+ "harness",
32878
+ "memory",
32879
+ "skills",
32880
+ "tools",
32881
+ "web",
32882
+ "workers",
32883
+ "deploy",
32884
+ "docs",
32885
+ "load",
32886
+ "process",
32887
+ "release",
32888
+ "deps",
32889
+ "main"
32890
+ ]
32891
+ };
32892
+ }
32893
+ });
32894
+
32895
+ // ../../src/core/selfDescription.ts
32896
+ function residentCapNote(cap) {
32897
+ return cap === void 0 ? RESIDENT_CAP_UNKNOWN_NOTE : `capped at ${cap} residents`;
32898
+ }
32899
+ function buildClause(build) {
32900
+ if (!build) return "";
32901
+ const commit = build.commit === "unknown" ? "an unstamped build" : `build ${build.commit.slice(0, 8)}`;
32902
+ return ` (version ${build.version}, ${commit} \u2014 \`status show\` reports it live)`;
32903
+ }
32904
+ function selfDescriptionBlock(agents, organization, caps, residentCap, build) {
32905
+ const agentLines = Object.values(agents).map((a) => `\`${a.name}\` \u2014 ${a.description}`).join("; ");
32906
+ const surfaces = caps.ingress ? "plus a CLI, an HTTP /api and an MCP surface" : "plus a CLI and an HTTP /api surface";
32907
+ const repos = caps.residents ? `Repos: the coding and review agents work in a "resident" \u2014 an always-warm per-repo environment (a mirror clone, installed deps, a built checkout, refreshed on a schedule and snapshotted) on the resident Worker; each run attaches a worktree on its branch in seconds. An admin onboards a repo with \`repo onboard <owner/name>\` (install/build/test commands are detected from the repo root, \`--install/--build/--test\` override), sees the fleet with \`repo list\`, changes it with \`repo reconfigure\` / \`repo rebuild\` / \`repo offboard\`; the fleet is ${residentCapNote(residentCap)} (\`--evict-coldest\` frees a slot). There is no separate "priority repos" setting: onboarded = warm. Repos that are not onboarded run in a cold per-thread workspace.` : `Repos: this installation has no resident (always-warm) repo environments \u2014 the coding and review agents clone the repository a request names (an https://github.com/owner/name URL or a PR link) into a per-thread workspace ${WORKSPACE_WHERE[caps.execution]} for the run. Nothing is kept warm between runs, and there is nothing to onboard.`;
32908
+ const history = caps.runHistory ? ", kept as run history" : " while it runs (this installation keeps no run history: a finished run's page goes away about a minute later)";
32909
+ const memory = caps.memory ? " Cross-session memory is per org/repo/channel/user (`memory list` / `memory forget`)." : " This installation has no cross-session memory: a run knows only its own thread.";
32910
+ return [
32911
+ `${SELF_DESCRIPTION_HEADER} you are Switchboard${buildClause(build)}, the agent gateway of the ${organization} organization. A message arrives on a channel (Slack \u2014 an @-mention, thread follow-ups stay on the same agent \u2014 ${surfaces}), is routed to an agent, runs on a configured provider/model, and executes tools through an executor.`,
32912
+ `Agents: ${agentLines}. Users pick one with an inline \`agent:<name>\` directive (default: general); \`model:\` / \`effort:\` ride the same way (see the runtime-config block).`,
32913
+ repos,
32914
+ `Runs${caps.memory ? " and memory" : ""}: every run (agent or operator command) has a live page under /runs with its events${history}; runs can be stopped from there.${memory} \`help\` lists every chat command.`,
32915
+ `Source of truth: the code lives in the GitHub repo ${SWITCHBOARD_REPO} \u2014 README.md (architecture + agents), AGENTS.md (invariants + map), and one behavioral spec per feature under docs/reference/specs/*.md (e.g. docs/reference/specs/resident-repos.md, docs/reference/specs/routing-and-config.md, docs/reference/specs/github-tools.md). For a question about how Switchboard works beyond this block, read those files with the GitHub tools when you have them (github_tree / github_file on ${SWITCHBOARD_REPO}) \u2014 they are private, so a public-web fetch 404s; if you have no GitHub tools, answer from this block and point at that path. Never describe yourself as stateless, tool-less by design, or unable to know your own workings.`
32916
+ ].join("\n");
32917
+ }
32918
+ var SELF_DESCRIPTION_HEADER, SWITCHBOARD_REPO, RESIDENT_CAP_UNKNOWN_NOTE, WORKSPACE_WHERE;
32919
+ var init_selfDescription = __esm({
32920
+ "../../src/core/selfDescription.ts"() {
32921
+ "use strict";
32922
+ SELF_DESCRIPTION_HEADER = "About Switchboard (yourself):";
32923
+ SWITCHBOARD_REPO = "coreplanelabs/switchboard";
32924
+ RESIDENT_CAP_UNKNOWN_NOTE = "capped (`repo list` shows the cap)";
32925
+ WORKSPACE_WHERE = {
32926
+ local: "on the bot host",
32927
+ e2b: "in an E2B micro-VM",
32928
+ cloudflare: "in a Cloudflare sandbox"
32929
+ };
32930
+ }
32931
+ });
32932
+
32488
32933
  // ../../src/core/prDescription.ts
32489
32934
  import { z as z21 } from "zod";
32490
32935
  function visibleLength(s) {
@@ -32502,6 +32947,9 @@ function cappedRaw(base, max) {
32502
32947
  if (n2 > max) ctx.addIssue({ code: z21.ZodIssueCode.custom, message: `at most ${max} characters (got ${n2})` });
32503
32948
  });
32504
32949
  }
32950
+ function titleGateApplies(repo) {
32951
+ return repo !== void 0 && repo.toLowerCase() === TITLE_GATE_REPOSITORY;
32952
+ }
32505
32953
  function boundedArray(item, min, max, what) {
32506
32954
  return z21.array(item).superRefine((arr, ctx) => {
32507
32955
  if (arr.length < min)
@@ -32510,8 +32958,11 @@ function boundedArray(item, min, max, what) {
32510
32958
  ctx.addIssue({ code: z21.ZodIssueCode.custom, message: `at most ${max} ${what} (got ${arr.length})` });
32511
32959
  });
32512
32960
  }
32513
- function parsePrDescription(input) {
32514
- return PrDescriptionSchema.parse(input);
32961
+ function prDescriptionSchemaFor(repo) {
32962
+ return titleGateApplies(repo) ? PrDescriptionSchema.extend({ title: gatedTitle }) : PrDescriptionSchema;
32963
+ }
32964
+ function parsePrDescription(input, target = {}) {
32965
+ return prDescriptionSchemaFor(target.repo).parse(input);
32515
32966
  }
32516
32967
  function redactPrDescription(d) {
32517
32968
  return mapStringLeaves(d, redactSecrets);
@@ -32902,16 +33353,19 @@ function parseLegacySteps(lines, problems) {
32902
33353
  }
32903
33354
  return out;
32904
33355
  }
32905
- var PR_DESCRIPTION_CAPS, line2, prose, RISK_SEPARATOR, PrAnchorSchema, PointerSchema, PrDescriptionSchema, REPO_RE2, SHA_RE2, GENERATED_FOOTER, MAP_LABELS, FOLD_SUMMARIES, PERMALINK_SRC, PERMALINK_RE, POINTER_ROW_RE, HEADING_RE, LABEL_RE, SUMMARY_RE, DECISION_RE, TABLE_HEADER_RE, TABLE_RULE_RE, STEP_RE, LOOK_FOR_RE, LABEL_KEYS, WHERE_TO_LOOK, LEGACY_SECTIONS;
33356
+ var PR_DESCRIPTION_CAPS, line2, prose, TITLE_GATE_REPOSITORY, gatedTitle, RISK_SEPARATOR, PrAnchorSchema, PointerSchema, PrDescriptionSchema, REPO_RE2, SHA_RE2, GENERATED_FOOTER, MAP_LABELS, FOLD_SUMMARIES, PERMALINK_SRC, PERMALINK_RE, POINTER_ROW_RE, HEADING_RE, LABEL_RE, SUMMARY_RE, DECISION_RE, TABLE_HEADER_RE, TABLE_RULE_RE, STEP_RE, LOOK_FOR_RE, LABEL_KEYS, WHERE_TO_LOOK, LEGACY_SECTIONS;
32906
33357
  var init_prDescription = __esm({
32907
33358
  "../../src/core/prDescription.ts"() {
32908
33359
  "use strict";
33360
+ init_prTitle();
33361
+ init_prTitleVocabulary();
32909
33362
  init_redact();
33363
+ init_selfDescription();
32910
33364
  PR_DESCRIPTION_CAPS = {
32911
33365
  /** The squash subject and the changelog line, the whole line counted: the
32912
- * number the title gate (`scripts/check-pr-title.mjs`, `TITLE_MAX_LENGTH`)
32913
- * holds it to; a test keeps the two equal. */
32914
- title: 72,
33366
+ * title gate's own number (`src/core/prTitle.mjs`), read from it so the
33367
+ * two cannot drift. */
33368
+ title: TITLE_MAX_LENGTH,
32915
33369
  tldr: 300,
32916
33370
  why: 400,
32917
33371
  pointers: 7,
@@ -32930,6 +33384,13 @@ var init_prDescription = __esm({
32930
33384
  };
32931
33385
  line2 = z21.string().trim().min(1).refine((s) => !/[\r\n]/.test(s), "must be a single line (no embedded newlines)");
32932
33386
  prose = z21.string().trim().min(1);
33387
+ TITLE_GATE_REPOSITORY = SWITCHBOARD_REPO;
33388
+ gatedTitle = line2.superRefine((s, ctx) => {
33389
+ if (s === "") return;
33390
+ const verdict = checkPrTitle(s, prTitleVocabulary_default);
33391
+ if (verdict.ok) return;
33392
+ for (const message4 of verdict.problems) ctx.addIssue({ code: z21.ZodIssueCode.custom, message: message4 });
33393
+ });
32933
33394
  RISK_SEPARATOR = " \u26A0 ";
32934
33395
  PrAnchorSchema = z21.object({
32935
33396
  // Repo-relative: no leading `/`, no `..` SEGMENT (a filename like `a..b.ts` is fine).
@@ -32951,7 +33412,9 @@ var init_prDescription = __esm({
32951
33412
  PrDescriptionSchema = z21.object({
32952
33413
  /** The PR title's single source. Metadata for the PR's own title field —
32953
33414
  * never rendered into the body (GitHub shows the title itself) — and the
32954
- * squash subject, so it is capped like every other field. */
33415
+ * squash subject, so it is capped like every other field; on the
33416
+ * repository that carries the title gate, `prDescriptionSchemaFor` judges
33417
+ * it as the gate does. */
32955
33418
  title: cappedRaw(line2, PR_DESCRIPTION_CAPS.title),
32956
33419
  tldr: capped2(prose, PR_DESCRIPTION_CAPS.tldr),
32957
33420
  why: capped2(prose, PR_DESCRIPTION_CAPS.why),
@@ -33120,13 +33583,13 @@ var init_submit = __esm({
33120
33583
  submitPrDescriptionTool = {
33121
33584
  name: "submit_pr_description",
33122
33585
  failsInText: true,
33123
- description: "Submit the PR description as a typed object. REQUIRED after pushing your branch: Switchboard renders the GitHub PR body from this object at the pushed head and opens (or updates) the pull request itself \u2014 never open a PR yourself. The body is a fixed-size MAP for the reader (tldr, why, at most 7 pointers, feedbackWanted, risk, verified) with decisions, validation and agentNotes 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, so cut and resubmit. `title` becomes the PR's title; pointer anchors are (path, from, to) line ranges at your pushed head, rendered as links. Call it after your last push; if you push again afterwards, call it again \u2014 the last valid call wins.",
33586
+ description: "Submit the PR description as a typed object. REQUIRED after pushing your branch: Switchboard renders the GitHub PR body from this object at the pushed head and opens (or updates) the pull request itself \u2014 never open a PR yourself. The body is a fixed-size MAP for the reader (tldr, why, at most 7 pointers, feedbackWanted, risk, verified) with decisions, validation and agentNotes 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, so cut and resubmit. `title` becomes the PR's title (at most 72 characters); on Switchboard's own repository it is also judged as that repository's CI `title` check judges it \u2014 `type(scope): description`, type from its release config, scope one of its code map's Areas or none \u2014 and refused with that check's own sentence, so fix the title and resubmit; pointer anchors are (path, from, to) line ranges at your pushed head, rendered as links. Call it after your last push; if you push again afterwards, call it again \u2014 the last valid call wins.",
33124
33587
  inputSchema: {
33125
33588
  type: "object",
33126
33589
  properties: {
33127
33590
  title: {
33128
33591
  type: "string",
33129
- 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"
33592
+ 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; on Switchboard's own repository a title its CI title check would refuse (type, scope, grammar) is refused here with the same sentence"
33130
33593
  },
33131
33594
  tldr: {
33132
33595
  type: "string",
@@ -33205,7 +33668,7 @@ var init_submit = __esm({
33205
33668
  async run(input, ctx) {
33206
33669
  let desc;
33207
33670
  try {
33208
- desc = parsePrDescription(input);
33671
+ desc = parsePrDescription(input, ctx.repo === void 0 ? {} : { repo: ctx.repo });
33209
33672
  } catch (err2) {
33210
33673
  const detail = err2 instanceof z22.ZodError ? err2.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ") : String(err2);
33211
33674
  return `error: invalid PR description \u2014 ${detail}`;
@@ -33416,187 +33879,6 @@ var init_statusCardFrame = __esm({
33416
33879
  }
33417
33880
  });
33418
33881
 
33419
- // ../../src/core/confirmations.ts
33420
- import { randomUUID as randomUUID2 } from "node:crypto";
33421
- import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "node:fs";
33422
- import { dirname as dirname9, resolve as resolve10 } from "node:path";
33423
- function newConfirmationId() {
33424
- return randomUUID2();
33425
- }
33426
- function confirmationFooter(scope) {
33427
- switch (scope) {
33428
- case "channel":
33429
- return "confirmation required by this channel's boundary";
33430
- case "user":
33431
- return "confirmation required by your boundary";
33432
- case "defaults":
33433
- return "confirmation required by the defaults' boundary";
33434
- case "built-in":
33435
- return "confirmation required by the built-in default";
33436
- default:
33437
- return `confirmation required by the ${scope} boundary`;
33438
- }
33439
- }
33440
- function renderOffer(offer) {
33441
- return [offer.line, ...offer.risk ? [offer.risk] : [], offer.footer].join("\n");
33442
- }
33443
- function confirmationMessageOf(msg) {
33444
- const { images: _images, documents: _documents, staged: _staged, ...kept } = msg;
33445
- return kept;
33446
- }
33447
- function isRecord3(v) {
33448
- return typeof v === "object" && v !== null && !Array.isArray(v);
33449
- }
33450
- function isMessage(v) {
33451
- return isRecord3(v) && typeof v.channelId === "string" && typeof v.userId === "string" && typeof v.threadKey === "string" && typeof v.text === "string";
33452
- }
33453
- function isPendingConfirmation(v) {
33454
- return isRecord3(v) && typeof v.id === "string" && isMessage(v.message) && typeof v.command === "string" && isRecord3(v.input) && typeof v.receipt === "string" && typeof v.risk === "string" && typeof v.footer === "string" && typeof v.model === "string";
33455
- }
33456
- function isConfirmation(v) {
33457
- return isPendingConfirmation(v) && typeof v.expiresAt === "number";
33458
- }
33459
- function isRefusal(v) {
33460
- return typeof v === "string" && CONFIRMATION_REFUSALS.includes(v);
33461
- }
33462
- function judge2(rows, id, actorIds, now, kind) {
33463
- const row2 = rows.get(id);
33464
- if (!row2) return { ok: false, refused: "used" };
33465
- if (kind === "consume" && row2.expiresAt <= now) {
33466
- rows.delete(id);
33467
- return { ok: false, refused: "expired" };
33468
- }
33469
- if (!actorIds.includes(row2.message.userId)) return { ok: false, refused: "foreign" };
33470
- rows.delete(id);
33471
- return kind === "consume" ? { ok: true, row: row2 } : { ok: true };
33472
- }
33473
- function replaceThreadRow(rows, row2) {
33474
- for (const [id, r] of rows) if (r.message.threadKey === row2.message.threadKey) rows.delete(id);
33475
- rows.set(row2.id, row2);
33476
- }
33477
- function buildConfirmationStore(config, secrets, opts) {
33478
- const worker = config.config.runtimeOverrides?.worker;
33479
- if (!worker) return new FileConfirmationStore(opts.path);
33480
- const tokenEnv = worker.tokenEnv ?? "MEMORY_TOKEN";
33481
- const token2 = secrets.named(tokenEnv);
33482
- if (!token2) throw new Error(`runtimeOverrides.worker is configured but ${tokenEnv} is not set`);
33483
- return new WorkerConfirmationStore({
33484
- baseUrl: worker.baseUrl,
33485
- token: token2.reveal(),
33486
- ...opts.fetch ? { fetch: opts.fetch } : {}
33487
- });
33488
- }
33489
- var CONFIRMATION_REFUSALS, UNSHOWABLE_LINE, STORE_UNREACHABLE_NOTE, FileConfirmationStore, CONFIRMATION_WORKER_TIMEOUT_MS, WorkerConfirmationStore;
33490
- var init_confirmations = __esm({
33491
- "../../src/core/confirmations.ts"() {
33492
- "use strict";
33493
- init_clock();
33494
- CONFIRMATION_REFUSALS = ["used", "expired", "foreign"];
33495
- UNSHOWABLE_LINE = "this command carries a value that cannot be shown; type the line yourself";
33496
- STORE_UNREACHABLE_NOTE = "(the confirmation store could not be reached, so there is no button to press)";
33497
- FileConfirmationStore = class {
33498
- path;
33499
- clock;
33500
- constructor(path, opts = {}) {
33501
- this.path = resolve10(path);
33502
- this.clock = opts.clock ?? systemClock;
33503
- }
33504
- read() {
33505
- const rows = /* @__PURE__ */ new Map();
33506
- if (!existsSync12(this.path)) return rows;
33507
- const raw = JSON.parse(readFileSync14(this.path, "utf8"));
33508
- for (const v of Array.isArray(raw.confirmations) ? raw.confirmations : []) if (isConfirmation(v)) rows.set(v.id, v);
33509
- return rows;
33510
- }
33511
- write(rows) {
33512
- mkdirSync9(dirname9(this.path), { recursive: true });
33513
- writeFileSync8(this.path, JSON.stringify({ confirmations: [...rows.values()] }, null, 2));
33514
- }
33515
- async put(row2, ttlMs) {
33516
- const rows = this.read();
33517
- const stored = { ...row2, expiresAt: this.clock() + ttlMs };
33518
- replaceThreadRow(rows, stored);
33519
- this.write(rows);
33520
- return stored;
33521
- }
33522
- async consume(id, actorIds) {
33523
- const rows = this.read();
33524
- const out = judge2(rows, id, actorIds, this.clock(), "consume");
33525
- this.write(rows);
33526
- return out;
33527
- }
33528
- async cancel(id, actorIds) {
33529
- const rows = this.read();
33530
- const out = judge2(rows, id, actorIds, this.clock(), "cancel");
33531
- this.write(rows);
33532
- return out;
33533
- }
33534
- describe() {
33535
- return `file ${this.path}`;
33536
- }
33537
- };
33538
- CONFIRMATION_WORKER_TIMEOUT_MS = 8e3;
33539
- WorkerConfirmationStore = class {
33540
- constructor(opts) {
33541
- this.opts = opts;
33542
- this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
33543
- this.fetchImpl = opts.fetch ?? fetch;
33544
- }
33545
- opts;
33546
- baseUrl;
33547
- fetchImpl;
33548
- async put(row2, ttlMs) {
33549
- const body = await this.post("/config/confirmations/put", {
33550
- id: row2.id,
33551
- threadKey: row2.message.threadKey,
33552
- requester: row2.message.userId,
33553
- body: row2,
33554
- ttlMs
33555
- });
33556
- if (typeof body.expiresAt !== "number")
33557
- throw new Error("confirmation store answered a put without the expiry it stamped");
33558
- return { ...row2, expiresAt: body.expiresAt };
33559
- }
33560
- async consume(id, actorIds) {
33561
- const body = await this.post("/config/confirmations/consume", { id, actorIds });
33562
- if (isRefusal(body.refused)) return { ok: false, refused: body.refused };
33563
- const stored = isRecord3(body.row) ? body.row : void 0;
33564
- const row2 = stored && isRecord3(stored.body) ? { ...stored.body, expiresAt: stored.expiresAt } : void 0;
33565
- if (!isConfirmation(row2)) throw new Error("confirmation store answered a consume outside its contract");
33566
- return { ok: true, row: row2 };
33567
- }
33568
- async cancel(id, actorIds) {
33569
- const body = await this.post("/config/confirmations/cancel", { id, actorIds });
33570
- if (body.ok === true) return { ok: true };
33571
- if (body.refused === "used" || body.refused === "foreign") return { ok: false, refused: body.refused };
33572
- throw new Error("confirmation store answered a cancel outside its contract");
33573
- }
33574
- describe() {
33575
- return `state Worker ${this.baseUrl} (ConfigDO confirmations)`;
33576
- }
33577
- async post(path, payload) {
33578
- let res;
33579
- try {
33580
- res = await this.fetchImpl(`${this.baseUrl}${path}`, {
33581
- method: "POST",
33582
- headers: { authorization: `Bearer ${this.opts.token}`, "content-type": "application/json" },
33583
- body: JSON.stringify(payload),
33584
- signal: AbortSignal.timeout(CONFIRMATION_WORKER_TIMEOUT_MS)
33585
- });
33586
- } catch (err2) {
33587
- throw new Error(`confirmation store unreachable: ${err2 instanceof Error ? err2.message : String(err2)}`, {
33588
- cause: err2
33589
- });
33590
- }
33591
- if (!res.ok) throw new Error(`confirmation store answered HTTP ${res.status} on ${path}`);
33592
- const body = await res.json().catch(() => void 0);
33593
- if (!isRecord3(body)) throw new Error(`confirmation store returned a non-JSON body on ${path}`);
33594
- return body;
33595
- }
33596
- };
33597
- }
33598
- });
33599
-
33600
33882
  // ../../src/core/dispatch/route.ts
33601
33883
  function routedRunsAtOnce(def, confirm) {
33602
33884
  const radius = blastRadius(def);
@@ -34080,9 +34362,8 @@ async function answerHandBack(branch2, msg, def, input, route3, receipt, confirm
34080
34362
  return { kind: "command", command: def.id, outcome };
34081
34363
  };
34082
34364
  const handBack = `${HAND_BACK_PREFIX} ${receipt}`;
34083
- const offer = io.offer?.bind(io);
34084
34365
  const store = deps.confirmations;
34085
- if (!offer || !store) return answer(handBack, "hand_back", () => io.reply(handBack));
34366
+ if (!io.offer || !store) return answer(handBack, "hand_back", () => io.reply(handBack));
34086
34367
  const line3 = chatInvocation(def, input);
34087
34368
  if (redactSecrets(line3) !== line3) return answer(UNSHOWABLE_LINE, "hand_back", () => io.reply(UNSHOWABLE_LINE));
34088
34369
  const risk = def.annotations?.risk?.(input) ?? "";
@@ -34112,7 +34393,7 @@ ${STORE_UNREACHABLE_NOTE}`;
34112
34393
  }
34113
34394
  const shown = { id: row2.id, line: line3, risk, footer, expiresAt: row2.expiresAt };
34114
34395
  console.log(`[route] ${msg.threadKey} offered ${def.id} as confirmation ${row2.id} (${footer})`);
34115
- return answer(renderOffer(shown), "offered", () => offer(shown));
34396
+ return answer(renderOffer(shown), "offered", () => renderConfirmationOffer(io, shown));
34116
34397
  }
34117
34398
  function redactedInput(input) {
34118
34399
  const text = (s) => redactAndCap(redactSecrets(s), ROUTE_COMMAND_VALUE_CAP);
@@ -34172,6 +34453,7 @@ var init_route = __esm({
34172
34453
  init_confirmations();
34173
34454
  init_repoContext();
34174
34455
  init_commandRun();
34456
+ init_reply();
34175
34457
  init_spawn();
34176
34458
  init_statusCardFrame();
34177
34459
  ROUTE_TEXT_CAP = 2e3;
@@ -34282,8 +34564,8 @@ function refusalLine(refused7) {
34282
34564
  function actorIdsOf(actor) {
34283
34565
  return [.../* @__PURE__ */ new Set([actor.id, ...actor.self ?? []])];
34284
34566
  }
34285
- function refused3(r) {
34286
- return { kind: "refused", refusal: `confirmation_${r}`, text: refusalLine(r) };
34567
+ function refused3(r, row2) {
34568
+ return { kind: "refused", refusal: `confirmation_${r}`, text: refusalLine(r), ...row2 ? { row: row2 } : {} };
34287
34569
  }
34288
34570
  async function consumeAndRun(deps, click, io, ending, trace) {
34289
34571
  const store = deps.confirmations;
@@ -34297,7 +34579,7 @@ async function consumeAndRun(deps, click, io, ending, trace) {
34297
34579
  );
34298
34580
  return UNREADABLE;
34299
34581
  }
34300
- if (!consumed.ok) return refused3(consumed.refused);
34582
+ if (!consumed.ok) return refused3(consumed.refused, consumed.row);
34301
34583
  const row2 = consumed.row;
34302
34584
  const result = await runChatCommand(
34303
34585
  deps,
@@ -34337,7 +34619,7 @@ async function cancelPending(deps, click) {
34337
34619
  if (!cancelled.ok) return refused3(cancelled.refused);
34338
34620
  return { kind: "cancelled", text: OFFER_CANCELLED_LINE };
34339
34621
  }
34340
- var OFFER_EXPIRED_LINE, OFFER_FOREIGN_LINE, OFFER_USED_LINE, OFFER_UNREADABLE_LINE, OFFER_CANCELLED_LINE, CONFIRMED_REASON, UNREADABLE;
34622
+ var OFFER_EXPIRED_LINE, OFFER_FOREIGN_LINE, OFFER_USED_LINE, OFFER_UNREADABLE_LINE, OFFER_CANCELLED_LINE, CONFIRMED_REASON, REFUSED_REASON, UNREADABLE;
34341
34623
  var init_confirm = __esm({
34342
34624
  "../../src/core/dispatch/confirm.ts"() {
34343
34625
  "use strict";
@@ -34350,6 +34632,7 @@ var init_confirm = __esm({
34350
34632
  OFFER_UNREADABLE_LINE = "the confirmation could not be read; type the line to run it";
34351
34633
  OFFER_CANCELLED_LINE = "Cancelled; nothing ran";
34352
34634
  CONFIRMED_REASON = "confirmed after offer";
34635
+ REFUSED_REASON = "refused after offer";
34353
34636
  UNREADABLE = { kind: "refused", refusal: "confirmation_unreadable", text: OFFER_UNREADABLE_LINE };
34354
34637
  }
34355
34638
  });
@@ -35288,12 +35571,12 @@ var init_reviewRound = __esm({
35288
35571
 
35289
35572
  // ../../src/core/dispatch/authorize.ts
35290
35573
  async function authorizeAgent(deps, ctx) {
35291
- const { msg, io, refuse: refuse3, agentName } = ctx;
35574
+ const { msg, refuse: refuse3, agentName } = ctx;
35292
35575
  if (!deps.config.canRunAgent(chatActorOf(deps.config, msg), agentName)) {
35293
35576
  await refuse3(
35294
- "agent_allowlist",
35295
- () => io.reply(
35296
- `\u{1F6AB} You're not on the allowlist for the \`${agentName}\` agent. Ask ${deps.config.adminsHint()} for access.`
35577
+ refusalOf(
35578
+ "agent_allowlist",
35579
+ REFUSAL_SENTENCES.agent_allowlist({ agent: agentName, adminsHint: deps.config.adminsHint() })
35297
35580
  )
35298
35581
  );
35299
35582
  return { kind: "refused", reason: "agent_allowlist" };
@@ -35301,11 +35584,11 @@ async function authorizeAgent(deps, ctx) {
35301
35584
  return { kind: "allowed" };
35302
35585
  }
35303
35586
  async function authorizeProfile(deps, ctx) {
35304
- const { msg, io, refuse: refuse3, agent, resolution } = ctx;
35587
+ const { msg, refuse: refuse3, agent, resolution } = ctx;
35305
35588
  if (resolution.kind === "profile") return { kind: "allowed", profile: resolution.profile };
35306
35589
  const { refusal } = resolution;
35307
35590
  console.log(`[dispatch] ${msg.threadKey} not started: profile bounded (${agent.name}: ${refusalSummary(refusal)})`);
35308
- await refuse3("profile_bounded", () => io.reply(profileRefusalReply(agent.name, refusal, deps.config.adminsHint())));
35591
+ await refuse3(refusalOf("profile_bounded", profileRefusalReply(agent.name, refusal, deps.config.adminsHint())));
35309
35592
  return { kind: "refused", reason: "profile_bounded" };
35310
35593
  }
35311
35594
  function refusalSummary(refusal) {
@@ -35384,19 +35667,17 @@ function profileRefusalReply(agentName, refusal, adminsHint) {
35384
35667
  return `\u{1F6AB} \`${agentName}\` runs on a \`${refusal.needs}\` machine; ${who2} only ${allowed2}. ${capitalize(how)}.`;
35385
35668
  }
35386
35669
  async function authorizeRepo(deps, ctx) {
35387
- const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, profile, needsRepo, repoCtx } = ctx;
35670
+ const { msg, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, profile, needsRepo, repoCtx } = ctx;
35388
35671
  if (profile.machine === "repo-cold" && needsRepo && !repoCtx.repo) {
35389
35672
  if (repoCtx.rejectedRepo) {
35390
35673
  const slug = repoCtx.rejectedRepo;
35391
35674
  console.log(`[dispatch] ${msg.threadKey} not started: repo not visible (${slug}: GitHub answered 404)`);
35392
- await refuse3("repo_not_visible", async () => {
35393
- await card.done(
35675
+ await refuse3(
35676
+ refusalOf("repo_not_visible", REFUSAL_SENTENCES.repo_not_visible({ slug, agent: agent.name })),
35677
+ () => card.done(
35394
35678
  shell.close({ kind: "not_started", icon: "\u{1F4E6}", reason: "repo not visible", ...closeLines(clock4(), false) })
35395
- );
35396
- await io.reply(
35397
- `\u{1F4E6} \`${slug}\` is not a repository this installation can see \u2014 GitHub answered 404 \u2014 so I did not start ${aRun(agent.name)} for it. The repository is outside the Switchboard GitHub App installation (\`github_repos\` lists the reachable ones), or the name is wrong.`
35398
- );
35399
- });
35679
+ )
35680
+ );
35400
35681
  return { kind: "refused", reason: "repo_not_visible" };
35401
35682
  }
35402
35683
  if (repoCtx.unverifiedRepo) {
@@ -35404,19 +35685,17 @@ async function authorizeRepo(deps, ctx) {
35404
35685
  console.log(
35405
35686
  `[dispatch] ${msg.threadKey} not started: repo could not be verified (${slug}: GitHub did not answer)`
35406
35687
  );
35407
- await refuse3("repo_unverified", async () => {
35408
- await card.done(
35688
+ await refuse3(
35689
+ refusalOf("repo_unverified", REFUSAL_SENTENCES.repo_unverified({ slug, agent: agent.name, via: "github" })),
35690
+ () => card.done(
35409
35691
  shell.close({
35410
35692
  kind: "not_started",
35411
35693
  icon: "\u{1F4E6}",
35412
35694
  reason: "repo could not be verified",
35413
35695
  ...closeLines(clock4(), false)
35414
35696
  })
35415
- );
35416
- await io.reply(
35417
- `\u26A0\uFE0F I couldn't verify \`${slug}\` against GitHub \u2014 it didn't answer \u2014 so I did not start ${aRun(agent.name)} rather than guess which repository you meant. Try again in a minute.`
35418
- );
35419
- });
35697
+ )
35698
+ );
35420
35699
  return { kind: "refused", reason: "repo_unverified" };
35421
35700
  }
35422
35701
  }
@@ -35424,14 +35703,12 @@ async function authorizeRepo(deps, ctx) {
35424
35703
  const slug = repoCtx.rejectedRepo;
35425
35704
  console.log(`[dispatch] ${msg.threadKey} not started: repo not onboarded (${slug})`);
35426
35705
  const onboardHint = deps.config.canManageRepos(chatActorOf(deps.config, msg)) ? `Onboard it (\`repo onboard ${slug}\`)` : `Ask ${deps.config.adminsHint()} to onboard it (\`repo onboard ${slug}\`)`;
35427
- await refuse3("repo_not_onboarded", async () => {
35428
- await card.done(
35706
+ await refuse3(
35707
+ refusalOf("repo_not_onboarded", REFUSAL_SENTENCES.repo_not_onboarded({ slug, agent: agent.name, onboardHint })),
35708
+ () => card.done(
35429
35709
  shell.close({ kind: "not_started", icon: "\u{1F4E6}", reason: "repo not onboarded", ...closeLines(clock4(), false) })
35430
- );
35431
- await io.reply(
35432
- `\u{1F4E6} \`${slug}\` is not onboarded as a resident, so I did not start a *${agent.name}* run for it. ${onboardHint} for a warm, deps-ready environment, or name the repository by URL (https://github.com/${slug}) to run in a cold per-thread sandbox.`
35433
- );
35434
- });
35710
+ )
35711
+ );
35435
35712
  return { kind: "refused", reason: "repo_not_onboarded" };
35436
35713
  }
35437
35714
  if (needsRepo && !repoCtx.repo && repoCtx.unverifiedRepo) {
@@ -35439,55 +35716,48 @@ async function authorizeRepo(deps, ctx) {
35439
35716
  console.log(
35440
35717
  `[dispatch] ${msg.threadKey} not started: repo could not be verified (${slug}: resident registry unreachable)`
35441
35718
  );
35442
- await refuse3("repo_unverified", async () => {
35443
- await card.done(
35719
+ await refuse3(
35720
+ refusalOf("repo_unverified", REFUSAL_SENTENCES.repo_unverified({ slug, agent: agent.name, via: "registry" })),
35721
+ () => card.done(
35444
35722
  shell.close({
35445
35723
  kind: "not_started",
35446
35724
  icon: "\u{1F4E6}",
35447
35725
  reason: "repo could not be verified",
35448
35726
  ...closeLines(clock4(), false)
35449
35727
  })
35450
- );
35451
- await io.reply(
35452
- `\u26A0\uFE0F I couldn't verify that \`${slug}\` is an onboarded repo \u2014 the resident registry didn't answer \u2014 so I did not start a *${agent.name}* run rather than guess which repo you meant. Try again in a minute, or name the repository by URL (https://github.com/${slug}) to run in a cold per-thread sandbox.`
35453
- );
35454
- });
35728
+ )
35729
+ );
35455
35730
  return { kind: "refused", reason: "repo_unverified" };
35456
35731
  }
35457
35732
  if (needsRepo && repoCtx.repo && !deps.config.canUseRepo(chatActorOf(deps.config, msg), repoCtx.repo)) {
35458
35733
  const repo = repoCtx.repo;
35459
- await refuse3("repo_access", async () => {
35460
- await card.done(
35734
+ await refuse3(
35735
+ refusalOf("repo_access", REFUSAL_SENTENCES.repo_access({ repo, adminsHint: deps.config.adminsHint() })),
35736
+ () => card.done(
35461
35737
  shell.close({ kind: "not_started", icon: "\u{1F6AB}", reason: "repo access", ...closeLines(clock4(), false) })
35462
- );
35463
- await io.reply(
35464
- `\u{1F6AB} You're not on the allowlist for the \`${repo}\` repo environment. Ask ${deps.config.adminsHint()} for access.`
35465
- );
35466
- });
35738
+ )
35739
+ );
35467
35740
  return { kind: "refused", reason: "repo_access" };
35468
35741
  }
35469
35742
  return { kind: "allowed" };
35470
35743
  }
35471
- function aRun(agentName) {
35472
- return `${/^[aeiou]/i.test(agentName) ? "an" : "a"} *${agentName}* run`;
35473
- }
35474
35744
  async function authorizePrHead(ctx) {
35475
- const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, directives, repoCtx } = ctx;
35745
+ const { msg, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, directives, repoCtx } = ctx;
35476
35746
  const preflight = checkPrHeadPreflight({ agent, requestText: directives.text, repoCtx });
35477
35747
  if (!preflight.ok) {
35478
35748
  console.log(`[review] ${msg.threadKey} not started: PR head unknown (${preflight.where})`);
35479
- await refuse3("pr_head_unknown", async () => {
35480
- await card.done(
35749
+ await refuse3(
35750
+ refusalOf("pr_head_unknown", preflight.reply),
35751
+ () => card.done(
35481
35752
  shell.close({ kind: "not_started", icon: "\u{1F500}", reason: "PR head unknown", ...closeLines(clock4(), false) })
35482
- );
35483
- await io.reply(preflight.reply);
35484
- });
35753
+ )
35754
+ );
35485
35755
  return { kind: "refused", reason: "pr_head_unknown" };
35486
35756
  }
35487
35757
  return { kind: "allowed" };
35488
35758
  }
35489
35759
  async function authorizeAttachedHead(deps, ctx) {
35490
- const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, resume, selection, root } = ctx;
35760
+ const { msg, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, resume, selection, root } = ctx;
35491
35761
  const { executor, resident, binding } = selection;
35492
35762
  let repoCtx = ctx.repoCtx;
35493
35763
  let verifiedAtAttach = false;
@@ -35513,14 +35783,12 @@ async function authorizeAttachedHead(deps, ctx) {
35513
35783
  verifiedAtAttach = true;
35514
35784
  headAdopted = true;
35515
35785
  } else if (guard.outcome === "refused") {
35516
- const reply = guard.reply;
35517
- await refuse3("branch_moved", async () => {
35786
+ await refuse3(refusalOf("branch_moved", guard.reply), async () => {
35518
35787
  if (executor.release) await executor.release("always").catch(() => {
35519
35788
  });
35520
35789
  await card.done(
35521
35790
  shell.close({ kind: "not_started", icon: "\u{1F500}", reason: "branch moved", ...closeLines(clock4(), false) })
35522
35791
  );
35523
- await io.reply(reply);
35524
35792
  });
35525
35793
  return { kind: "refused", reason: "branch_moved" };
35526
35794
  }
@@ -35533,6 +35801,8 @@ var init_authorize2 = __esm({
35533
35801
  init_actor();
35534
35802
  init_repoContext();
35535
35803
  init_reviewRound();
35804
+ init_refusal();
35805
+ init_reply();
35536
35806
  }
35537
35807
  });
35538
35808
 
@@ -35887,44 +36157,6 @@ var init_configAwareness = __esm({
35887
36157
  }
35888
36158
  });
35889
36159
 
35890
- // ../../src/core/selfDescription.ts
35891
- function residentCapNote(cap) {
35892
- return cap === void 0 ? RESIDENT_CAP_UNKNOWN_NOTE : `capped at ${cap} residents`;
35893
- }
35894
- function buildClause(build) {
35895
- if (!build) return "";
35896
- const commit = build.commit === "unknown" ? "an unstamped build" : `build ${build.commit.slice(0, 8)}`;
35897
- return ` (version ${build.version}, ${commit} \u2014 \`status show\` reports it live)`;
35898
- }
35899
- function selfDescriptionBlock(agents, organization, caps, residentCap, build) {
35900
- const agentLines = Object.values(agents).map((a) => `\`${a.name}\` \u2014 ${a.description}`).join("; ");
35901
- const surfaces = caps.ingress ? "plus a CLI, an HTTP /api and an MCP surface" : "plus a CLI and an HTTP /api surface";
35902
- const repos = caps.residents ? `Repos: the coding and review agents work in a "resident" \u2014 an always-warm per-repo environment (a mirror clone, installed deps, a built checkout, refreshed on a schedule and snapshotted) on the resident Worker; each run attaches a worktree on its branch in seconds. An admin onboards a repo with \`repo onboard <owner/name>\` (install/build/test commands are detected from the repo root, \`--install/--build/--test\` override), sees the fleet with \`repo list\`, changes it with \`repo reconfigure\` / \`repo rebuild\` / \`repo offboard\`; the fleet is ${residentCapNote(residentCap)} (\`--evict-coldest\` frees a slot). There is no separate "priority repos" setting: onboarded = warm. Repos that are not onboarded run in a cold per-thread workspace.` : `Repos: this installation has no resident (always-warm) repo environments \u2014 the coding and review agents clone the repository a request names (an https://github.com/owner/name URL or a PR link) into a per-thread workspace ${WORKSPACE_WHERE[caps.execution]} for the run. Nothing is kept warm between runs, and there is nothing to onboard.`;
35903
- const history = caps.runHistory ? ", kept as run history" : " while it runs (this installation keeps no run history: a finished run's page goes away about a minute later)";
35904
- const memory = caps.memory ? " Cross-session memory is per org/repo/channel/user (`memory list` / `memory forget`)." : " This installation has no cross-session memory: a run knows only its own thread.";
35905
- return [
35906
- `${SELF_DESCRIPTION_HEADER} you are Switchboard${buildClause(build)}, the agent gateway of the ${organization} organization. A message arrives on a channel (Slack \u2014 an @-mention, thread follow-ups stay on the same agent \u2014 ${surfaces}), is routed to an agent, runs on a configured provider/model, and executes tools through an executor.`,
35907
- `Agents: ${agentLines}. Users pick one with an inline \`agent:<name>\` directive (default: general); \`model:\` / \`effort:\` ride the same way (see the runtime-config block).`,
35908
- repos,
35909
- `Runs${caps.memory ? " and memory" : ""}: every run (agent or operator command) has a live page under /runs with its events${history}; runs can be stopped from there.${memory} \`help\` lists every chat command.`,
35910
- `Source of truth: the code lives in the GitHub repo ${SWITCHBOARD_REPO} \u2014 README.md (architecture + agents), AGENTS.md (invariants + map), and one behavioral spec per feature under docs/reference/specs/*.md (e.g. docs/reference/specs/resident-repos.md, docs/reference/specs/routing-and-config.md, docs/reference/specs/github-tools.md). For a question about how Switchboard works beyond this block, read those files with the GitHub tools when you have them (github_tree / github_file on ${SWITCHBOARD_REPO}) \u2014 they are private, so a public-web fetch 404s; if you have no GitHub tools, answer from this block and point at that path. Never describe yourself as stateless, tool-less by design, or unable to know your own workings.`
35911
- ].join("\n");
35912
- }
35913
- var SELF_DESCRIPTION_HEADER, SWITCHBOARD_REPO, RESIDENT_CAP_UNKNOWN_NOTE, WORKSPACE_WHERE;
35914
- var init_selfDescription = __esm({
35915
- "../../src/core/selfDescription.ts"() {
35916
- "use strict";
35917
- SELF_DESCRIPTION_HEADER = "About Switchboard (yourself):";
35918
- SWITCHBOARD_REPO = "coreplanelabs/switchboard";
35919
- RESIDENT_CAP_UNKNOWN_NOTE = "capped (`repo list` shows the cap)";
35920
- WORKSPACE_WHERE = {
35921
- local: "on the bot host",
35922
- e2b: "in an E2B micro-VM",
35923
- cloudflare: "in a Cloudflare sandbox"
35924
- };
35925
- }
35926
- });
35927
-
35928
36160
  // ../../src/core/customInstructions.ts
35929
36161
  function customInstructionsBlock(scopes) {
35930
36162
  const channel = scopes.channel.instructions?.trim();
@@ -36307,7 +36539,7 @@ async function reattachWorkspace2(deps, ctx) {
36307
36539
  }
36308
36540
  }
36309
36541
  async function attachWorkspace(deps, ctx) {
36310
- const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, profile, repoCtx, root, reattach, stopSignal } = ctx;
36542
+ const { msg, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, profile, repoCtx, root, reattach, stopSignal } = ctx;
36311
36543
  const { remainingMs } = ctx;
36312
36544
  let round2;
36313
36545
  try {
@@ -36325,14 +36557,12 @@ async function attachWorkspace(deps, ctx) {
36325
36557
  } catch (err2) {
36326
36558
  if (err2 instanceof ResidentNeedsRefError) {
36327
36559
  const repo = repoCtx.repo;
36328
- await refuse3("which_branch", async () => {
36329
- await card.done(
36560
+ await refuse3(
36561
+ refusalOf("which_branch", REFUSAL_SENTENCES.which_branch({ repo })),
36562
+ () => card.done(
36330
36563
  shell.close({ kind: "not_started", icon: "\u{1F33F}", reason: "which branch?", ...closeLines(clock4(), false) })
36331
- );
36332
- await io.reply(
36333
- `\u{1F33F} Which branch of \`${repo}\` should this thread work on? No branch is bound yet \u2014 reply naming one (e.g. "on main" or "on branch fix/login") and I'll pick it up from there.`
36334
- );
36335
- });
36564
+ )
36565
+ );
36336
36566
  return { kind: "refused", reason: "which_branch" };
36337
36567
  }
36338
36568
  if (err2 instanceof WorkspaceReattachRefusedError) return { kind: "reattach_refused", why: err2.why };
@@ -36504,6 +36734,7 @@ var init_provision = __esm({
36504
36734
  init_types5();
36505
36735
  init_record();
36506
36736
  init_reply();
36737
+ init_refusal();
36507
36738
  init_messages();
36508
36739
  init_route();
36509
36740
  init_roster();
@@ -36827,12 +37058,12 @@ async function prepareRelaunch(deps, ctx) {
36827
37058
  );
36828
37059
  round2 = reattached.round;
36829
37060
  }
36830
- const counted = { ...facts2, relaunches: relaunches + 1 };
36831
- let written = counted;
37061
+ const counted2 = { ...facts2, relaunches: relaunches + 1 };
37062
+ let written = counted2;
36832
37063
  let bearer;
36833
37064
  if (deps.runBearers) {
36834
37065
  const rotated = deps.runBearers.rotate(ctx.runId, (secretHash) => {
36835
- written = { ...counted, bearerHash: secretHash };
37066
+ written = { ...counted2, bearerHash: secretHash };
36836
37067
  ctx.saveFacts(written);
36837
37068
  });
36838
37069
  if (!rotated.ok)
@@ -37746,12 +37977,56 @@ var init_botHostContainer = __esm({
37746
37977
  });
37747
37978
 
37748
37979
  // ../../src/core/harness/windDown.ts
37980
+ function windDownEndingOf(writeUp, text, writeUpFailed, ended = "failed") {
37981
+ const failed = writeUpFailed !== void 0 ? { writeUpFailed } : {};
37982
+ if (writeUp?.kind === "time") return { kind: "time", text, ...failed };
37983
+ if (writeUp?.kind === "turns") return { kind: "turns", pace: writeUp.pace, text, ...failed };
37984
+ if (writeUp?.kind === "soft") return { kind: "soft", text, ...failed };
37985
+ if (writeUpFailed !== void 0) return { kind: "unlabelled", text, writeUpFailed, ended };
37986
+ return void 0;
37987
+ }
37988
+ function windDownAnswer(ending, maxMinutes, facts2) {
37989
+ switch (ending.kind) {
37990
+ case "time":
37991
+ return timeBudgetAnswer(ending.text, maxMinutes, ending.writeUpFailed, facts2);
37992
+ case "turns":
37993
+ return turnGuardAnswer(ending.text, ending.pace, ending.writeUpFailed, facts2);
37994
+ case "soft":
37995
+ return softStopAnswer(ending.text, ending.writeUpFailed, facts2);
37996
+ case "unlabelled":
37997
+ return unlabelledAnswer(ending.text, ending.writeUpFailed, ending.ended, facts2);
37998
+ }
37999
+ }
38000
+ function established(facts2, advice) {
38001
+ const then = advice ? ` \u2014 ${advice}` : "";
38002
+ const w = facts2?.workspace;
38003
+ let tree = "";
38004
+ if (w?.kind === "unmeasured") tree = `The workspace could not be measured, so work may sit unpushed there${then}.`;
38005
+ else if (w?.kind === "clean")
38006
+ tree = `The tree was clean${w.branch ? ` and \`${w.branch}\` held no unpushed commits` : " with no unpushed commits"}${w.head ? ` \u2014 its head \`${shortSha(w.head)}\` is on the remote` : ""}.`;
38007
+ else if (w?.kind === "salvaged")
38008
+ tree = `What the tree held was pushed to \`${w.branch}\`${w.head ? ` at \`${shortSha(w.head)}\`` : ""} by the budget salvage, unreviewed \u2014 a follow-up starts from it.`;
38009
+ else if (w?.kind === "left")
38010
+ tree = w.fate === "kept" ? `${counted(w)} sit in the workspace, kept for this thread until it idles out \u2014 a follow-up here reuses them.` : w.fate === "discarded" ? `${counted(w)} were left in the tree and discarded at the run's end${then}.` : `${counted(w)} were left in the tree, which is torn down since a command may still be running in it${then}.`;
38011
+ const description = facts2?.description === "submitted" ? "The PR description was submitted." : facts2?.description === "not_submitted" ? "No PR description was submitted." : "";
38012
+ return [tree, description].filter((s) => s !== "").join(" ");
38013
+ }
38014
+ function nothingWritten(facts2, advice) {
38015
+ const known = established(facts2, advice);
38016
+ if (known) return ` ${known}`;
38017
+ if (facts2?.workspace?.kind === "none") return advice ? ` ${advice.charAt(0).toUpperCase()}${advice.slice(1)}.` : "";
38018
+ return ` Partial work may exist in the workspace${advice ? ` \u2014 ${advice}` : ""}.`;
38019
+ }
38020
+ function beforeFindings(facts2) {
38021
+ const known = established(facts2, void 0);
38022
+ return known ? `. ${known} Findings so far:` : " \u2014 findings so far:";
38023
+ }
37749
38024
  function elapsedMinutes(ms) {
37750
38025
  const minutes = Math.round(ms / 6e4);
37751
38026
  if (minutes < 1) return "under a minute";
37752
38027
  return `${minutes} minute${minutes === 1 ? "" : "s"}`;
37753
38028
  }
37754
- 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, wrapUpNeverPostedNote, wrapUpWriteFailedNote, ABORT_DROPPED_NOTE, abortWriteFailedNote, abortReaskedNote, abortFailedAfterEndNote, abortUnheardAtEndNote, timesWord, wrapUpName, finaleAbortReason, noWriteUp, timeBudgetAnswer, turnGuardAnswer, unlabelledAnswer, softStopAnswer;
38029
+ 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, finaleWaitNote, finaleTimedOutNote, wrapUpUndeliveredNote, wrapUpNeverPostedNote, wrapUpWriteFailedNote, ABORT_DROPPED_NOTE, abortWriteFailedNote, abortReaskedNote, abortFailedAfterEndNote, abortUnheardAtEndNote, timesWord, wrapUpName, finaleAbortReason, noWriteUp, TIME_ADVICE, TURN_ADVICE, shortSha, counted, timeBudgetAnswer, turnGuardAnswer, unlabelledAnswer, softStopAnswer;
37755
38030
  var init_windDown = __esm({
37756
38031
  "../../src/core/harness/windDown.ts"() {
37757
38032
  "use strict";
@@ -37771,6 +38046,7 @@ var init_windDown = __esm({
37771
38046
  softStopNote = () => "soft stop \u2014 no further steps, writing up findings so far";
37772
38047
  hardStopNote = () => "hard stop \u2014 run aborted, no summary written";
37773
38048
  windDownFailureNote = (error, closes = "run") => `the model call failed during the wind-down (${error}); the ${closes} closes with its findings so far`;
38049
+ finaleWaitNote = (reason2, doing, interruptUnanswered, closes = "run") => `the finale bound ended the wait${doing ? ` while ${doing}` : ""}${interruptUnanswered ? ", the loop-end interrupt unanswered" : ""} (${reason2}); the ${closes} closes with its findings so far`;
37774
38050
  finaleTimedOutNote = (closes = "run") => `finale timed out \u2014 closing the ${closes} without a write-up`;
37775
38051
  wrapUpUndeliveredNote = (kind, closes = "run") => `the ${wrapUpName(kind)} wrap-up instruction never reached pi \u2014 held behind a prompt in doubt when pi settled \u2014 so the ${closes} closes on pi's own answer, unlabelled`;
37776
38052
  wrapUpNeverPostedNote = (kind, closes = "run", ended = "finished") => `the ${wrapUpName(kind)} wrap-up instruction was never posted \u2014 ${ended === "finale" ? "the loop-end interrupt was never answered, the finale bound ending the wait" : `the loop-end interrupt found the session idle, the execution having ${ended === "failed" ? "failed on the provider" : "finished on its own"}`} \u2014 so the ${closes} closes on OpenCode's own answer, unlabelled`;
@@ -37792,25 +38068,29 @@ var init_windDown = __esm({
37792
38068
  wrapUpName = (kind) => kind === "time" ? "time-budget" : kind === "turns" ? "turn-guard" : "soft-stop";
37793
38069
  finaleAbortReason = (boundMs) => `aborted at the finale bound (${elapsedMinutes(boundMs)})`;
37794
38070
  noWriteUp = (failed) => failed ? `; the model call failed during the wind-down (${failed}), so no write-up came` : "";
37795
- timeBudgetAnswer = (text, maxMinutes, writeUpFailed) => text ? `\u26A0\uFE0F _Hit the ${maxMinutes}-minute budget before finishing \u2014 findings so far:_
38071
+ TIME_ADVICE = "narrow the task and try again";
38072
+ TURN_ADVICE = "look for a retry loop in the run's events before trying again";
38073
+ shortSha = (sha) => sha.slice(0, 7);
38074
+ counted = (w) => `${w.uncommitted} uncommitted change(s) and ${w.unpushed} unpushed commit(s)`;
38075
+ timeBudgetAnswer = (text, maxMinutes, writeUpFailed, facts2) => text ? `\u26A0\uFE0F _Hit the ${maxMinutes}-minute budget before finishing${beforeFindings(facts2)}_
37796
38076
 
37797
- ${text}` : `Stopped at the ${maxMinutes}-minute budget without finishing${noWriteUp(writeUpFailed)}. Partial work may exist in the workspace \u2014 narrow the task and try again.`;
37798
- turnGuardAnswer = (text, pace, writeUpFailed) => text ? `\u26A0\uFE0F _Stopped after ${pace} \u2014 that pace looks like a loop; findings so far:_
38077
+ ${text}` : `Stopped at the ${maxMinutes}-minute budget without finishing${noWriteUp(writeUpFailed)}.${nothingWritten(facts2, TIME_ADVICE)}`;
38078
+ turnGuardAnswer = (text, pace, writeUpFailed, facts2) => text ? `\u26A0\uFE0F _Stopped after ${pace} \u2014 that pace looks like a loop${established(facts2, void 0) ? beforeFindings(facts2) : "; findings so far:"}_
37799
38079
 
37800
- ${text}` : `Stopped after ${pace} \u2014 that pace looks like a loop \u2014 without finishing${noWriteUp(writeUpFailed)}. Partial work may exist in the workspace \u2014 look for a retry loop in the run's events before trying again.`;
37801
- unlabelledAnswer = (text, writeUpFailed, ended = "failed") => {
38080
+ ${text}` : `Stopped after ${pace} \u2014 that pace looks like a loop \u2014 without finishing${noWriteUp(writeUpFailed)}.${nothingWritten(facts2, TURN_ADVICE)}`;
38081
+ unlabelledAnswer = (text, writeUpFailed, ended = "failed", facts2) => {
37802
38082
  if (!writeUpFailed) return text || "_(no response)_";
37803
38083
  if (ended === "finale")
37804
38084
  return text ? `${text}
37805
38085
 
37806
- \u26A0\uFE0F _The write-up never started: the loop-end interrupt went unanswered and the finale bound ended the wait (${writeUpFailed}); this is the last answer before it._` : `\u26A0\uFE0F The write-up never started: the loop-end interrupt went unanswered and the finale bound ended the wait (${writeUpFailed}). Partial work may exist in the workspace.`;
38086
+ \u26A0\uFE0F _The write-up never started: the loop-end interrupt went unanswered and the finale bound ended the wait (${writeUpFailed}); this is the last answer before it._` : `\u26A0\uFE0F The write-up never started: the loop-end interrupt went unanswered and the finale bound ended the wait (${writeUpFailed}).${nothingWritten(facts2, void 0)}`;
37807
38087
  return text ? `${text}
37808
38088
 
37809
- \u26A0\uFE0F _The model call that followed failed (${writeUpFailed}); this is the last answer before it._` : `\u26A0\uFE0F The model call failed (${writeUpFailed}) and no answer came. Partial work may exist in the workspace.`;
38089
+ \u26A0\uFE0F _The model call that followed failed (${writeUpFailed}); this is the last answer before it._` : `\u26A0\uFE0F The model call failed (${writeUpFailed}) and no answer came.${nothingWritten(facts2, void 0)}`;
37810
38090
  };
37811
- softStopAnswer = (text, writeUpFailed) => text ? `\u23F9 _Stopped early by an operator (soft stop) \u2014 findings so far:_
38091
+ softStopAnswer = (text, writeUpFailed, facts2) => text ? `\u23F9 _Stopped early by an operator (soft stop)${beforeFindings(facts2)}_
37812
38092
 
37813
- ${text}` : `\u23F9 Stopped early by an operator (soft stop) before any findings were written${noWriteUp(writeUpFailed)}. Partial work may exist in the workspace.`;
38093
+ ${text}` : `\u23F9 Stopped early by an operator (soft stop) before any findings were written${noWriteUp(writeUpFailed)}.${nothingWritten(facts2, void 0)}`;
37814
38094
  }
37815
38095
  });
37816
38096
 
@@ -40777,6 +41057,7 @@ ${stagedLine}` : prompt;
40777
41057
  throw new Error(`pi exited before the run settled${tail2.trim() ? `: ${redactAndCap(tail2.trim(), 400)}` : ""}`);
40778
41058
  };
40779
41059
  let answer;
41060
+ let ending;
40780
41061
  if (finaleAborted && !settled && (containerSaid ?? transportLost) !== void 0) {
40781
41062
  const met = containerSaid ?? transportLost;
40782
41063
  note(
@@ -40800,7 +41081,8 @@ ${stagedLine}` : prompt;
40800
41081
  );
40801
41082
  }
40802
41083
  const text = bridge.answer() ?? "";
40803
- answer = writeUp?.kind === "time" ? timeBudgetAnswer(text, run2.agent.maxMinutes, writeUpFailed) : writeUp?.kind === "turns" ? turnGuardAnswer(text, writeUp.pace, writeUpFailed) : writeUp?.kind === "soft" || stopMode === "soft" ? softStopAnswer(text, writeUpFailed) : text || "_(no response)_";
41084
+ ending = windDownEndingOf(writeUp ?? (stopMode === "soft" ? { kind: "soft" } : void 0), text, writeUpFailed);
41085
+ answer = ending ? windDownAnswer(ending, run2.agent.maxMinutes) : unlabelledAnswer(text, void 0);
40804
41086
  }
40805
41087
  agentSpan?.end(hardStopped ? "error" : "ok");
40806
41088
  const root = paths;
@@ -40977,10 +41259,8 @@ ${stagedLine}` : prompt;
40977
41259
  throw new Error(`the model call failed: ${turnError}`);
40978
41260
  }
40979
41261
  const text = bridge.answer() ?? "";
40980
- if (writeUp?.kind === "time") return timeBudgetAnswer(text, input.maxMinutes, writeUpFailed);
40981
- if (writeUp?.kind === "turns") return turnGuardAnswer(text, writeUp.pace, writeUpFailed);
40982
- if (writeUp?.kind === "soft") return softStopAnswer(text, writeUpFailed);
40983
- return text || "_(no response)_";
41262
+ const turnEnding = windDownEndingOf(writeUp, text, writeUpFailed);
41263
+ return turnEnding ? windDownAnswer(turnEnding, input.maxMinutes) : unlabelledAnswer(text, void 0);
40984
41264
  } catch (err2) {
40985
41265
  turnFailed = true;
40986
41266
  if (err2 instanceof HarnessControlFileLostError) note("harness_error", err2.message);
@@ -40993,7 +41273,7 @@ ${stagedLine}` : prompt;
40993
41273
  turnSpan?.end(hardStopped || bypass || turnFailed ? "error" : "ok");
40994
41274
  }
40995
41275
  };
40996
- return { answer, followUp, remainingMs: () => deadline - now(), end };
41276
+ return { answer, ...ending ? { ending } : {}, followUp, remainingMs: () => deadline - now(), end };
40997
41277
  } catch (err2) {
40998
41278
  loopEnded = true;
40999
41279
  requeueUnechoed();
@@ -42088,6 +42368,10 @@ async function runLoop(deps, ctx) {
42088
42368
  };
42089
42369
  const restoredDescription = PrDescriptionSchema.safeParse(restored.prDescription);
42090
42370
  let prDescription = restoredDescription.success ? restoredDescription.data : void 0;
42371
+ if (!restoredDescription.success && restored.prDescription !== void 0) {
42372
+ const why = restoredDescription.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
42373
+ console.log(`[resume] ${msg.threadKey} the restored PR description no longer parses and is dropped \u2014 ${why}`);
42374
+ }
42091
42375
  const onPrDescription = (d) => {
42092
42376
  prDescription = d;
42093
42377
  ledgerRun?.setState({ prDescription: d });
@@ -42151,6 +42435,8 @@ async function runLoop(deps, ctx) {
42151
42435
  let runDiagnosis;
42152
42436
  let harnessSession;
42153
42437
  let leaseSpentDuringRelaunch;
42438
+ let windDownEnding;
42439
+ let salvagedTo;
42154
42440
  let relaunchEndedRun = false;
42155
42441
  const tailSkipped = () => run2.control.requested === "hard" || relaunchEndedRun;
42156
42442
  const releaseWorkspace = (span) => {
@@ -42199,6 +42485,7 @@ async function runLoop(deps, ctx) {
42199
42485
  skills: deps.skills,
42200
42486
  github: githubCapabilityFor(deps, chatActorOf(deps.config, msg)),
42201
42487
  agentName: agent.name,
42488
+ ...repoCtx.repo ? { repo: repoCtx.repo } : {},
42202
42489
  ...spawn7 ? { spawn: spawn7 } : {},
42203
42490
  ...runs ? { runs } : {},
42204
42491
  ...steer ? { steer } : {},
@@ -42280,6 +42567,7 @@ async function runLoop(deps, ctx) {
42280
42567
  await judge4.end(facts2, container);
42281
42568
  }
42282
42569
  } else noteUnknownWord("the run finished on the answer it already had");
42570
+ windDownEnding = windDownEndingUnder(finish2.answer, loopEnding);
42283
42571
  answer = answerUnderEnding(finish2.answer, loopEnding, agent.maxMinutes);
42284
42572
  } else {
42285
42573
  if (!deps.harness)
@@ -42410,7 +42698,8 @@ async function runLoop(deps, ctx) {
42410
42698
  harnessDeps.registry.forget(run2.id);
42411
42699
  relaunchEndedRun = true;
42412
42700
  onEvent({ type: "run_note", kind: "time_budget_exhausted", summary: decision.why });
42413
- leaseSpentDuringRelaunch = timeBudgetAnswer("", agent.maxMinutes);
42701
+ windDownEnding = { kind: "time", text: "" };
42702
+ leaseSpentDuringRelaunch = windDownAnswer(windDownEnding, agent.maxMinutes);
42414
42703
  break;
42415
42704
  }
42416
42705
  if (decision.round !== void 0) {
@@ -42427,6 +42716,7 @@ async function runLoop(deps, ctx) {
42427
42716
  }
42428
42717
  }
42429
42718
  answer = harnessSession?.answer ?? leaseSpentDuringRelaunch ?? HARD_STOP_MESSAGE;
42719
+ if (harnessSession?.ending !== void 0) windDownEnding = harnessSession.ending;
42430
42720
  }
42431
42721
  if (isPrReview && repoCtx.repo && repoCtx.pr !== void 0 && !tailSkipped() && postedBefore === void 0) {
42432
42722
  const settled = await settleReviewedHead({
@@ -42531,6 +42821,11 @@ async function runLoop(deps, ctx) {
42531
42821
  onEvent({ type: "run_note", kind: "budget_salvage", summary: salvaged.summary });
42532
42822
  if (salvaged.pushed && "head" in salvaged && salvaged.head !== void 0 && !("skipped" in target))
42533
42823
  onEvent({ type: "pushed_head", ref: target.branch, sha: salvaged.head, by: "salvage" });
42824
+ if (salvaged.pushed && !("skipped" in target))
42825
+ salvagedTo = {
42826
+ branch: target.branch,
42827
+ ..."head" in salvaged && salvaged.head !== void 0 ? { head: salvaged.head } : {}
42828
+ };
42534
42829
  if (salvaged.pushed) await observeWorkspaceNow();
42535
42830
  }
42536
42831
  let descriptionTurnRan = false;
@@ -42570,6 +42865,28 @@ async function runLoop(deps, ctx) {
42570
42865
  }
42571
42866
  await endHarness();
42572
42867
  const leftBehind = isCodingPrRun && !tailSkipped() ? workLeftBehindOf({ uncommittedChanges: observedUncommitted, unpushedCommits: observedUnpushed }) : void 0;
42868
+ const endingFacts = () => {
42869
+ if (profile.machine === "none") return { workspace: { kind: "none" } };
42870
+ if (!isCodingPrRun || tailSkipped()) return { workspace: { kind: "unread" } };
42871
+ const branch2 = observedBranch ?? observedCheckedOut;
42872
+ let workspace;
42873
+ if (salvagedTo !== void 0) workspace = { kind: "salvaged", ...salvagedTo };
42874
+ else if (observedUncommitted === void 0 || observedUnpushed === void 0) workspace = { kind: "unmeasured" };
42875
+ else if (observedUncommitted === 0 && observedUnpushed === 0)
42876
+ workspace = {
42877
+ kind: "clean",
42878
+ ...branch2 !== void 0 ? { branch: branch2 } : {},
42879
+ ...observedHead !== void 0 ? { head: observedHead } : {}
42880
+ };
42881
+ else
42882
+ workspace = {
42883
+ kind: "left",
42884
+ uncommitted: observedUncommitted,
42885
+ unpushed: observedUnpushed,
42886
+ fate: commandInFlight ? "torn_down" : profile.machine === "repo-resident" ? "discarded" : "kept"
42887
+ };
42888
+ return { workspace, description: prDescription !== void 0 ? "submitted" : "not_submitted" };
42889
+ };
42573
42890
  if (leftBehind) {
42574
42891
  registry.publish(run2.id, {
42575
42892
  type: "run_note",
@@ -42613,6 +42930,8 @@ async function runLoop(deps, ctx) {
42613
42930
  if (baseline) await root.span("run.reading_diff_join", () => baseline);
42614
42931
  const description = descriptionArtifact;
42615
42932
  if (description) await root.span("run.pr_description_join", () => description);
42933
+ if (windDownEnding !== void 0 && answer === windDownAnswer(windDownEnding, agent.maxMinutes))
42934
+ answer = windDownAnswer(windDownEnding, agent.maxMinutes, endingFacts());
42616
42935
  const acceptedAnswer = markdownOutput.parse(answer);
42617
42936
  const rawAnswer = acceptedAnswer.ok && acceptedAnswer.changed ? answer : void 0;
42618
42937
  if (acceptedAnswer.ok) answer = acceptedAnswer.value;
@@ -42750,17 +43069,18 @@ function describeEnding(ending) {
42750
43069
  return `a write-up: ${ending.summary}`;
42751
43070
  }
42752
43071
  }
43072
+ function windDownEndingUnder(text, ending) {
43073
+ if (ending.kind === "soft_stop") return { kind: "soft", text };
43074
+ if (ending.kind === "written_up" && ending.note === "time_budget_exhausted") return { kind: "time", text };
43075
+ return void 0;
43076
+ }
42753
43077
  function answerUnderEnding(text, ending, maxMinutes) {
42754
- if (ending.kind === "answered") return text || "_(no response)_";
42755
- if (ending.kind === "soft_stop") return softStopAnswer(text);
42756
- switch (ending.note) {
42757
- case "time_budget_exhausted":
42758
- return timeBudgetAnswer(text, maxMinutes);
42759
- default:
42760
- return text ? `\u26A0\uFE0F _${ending.summary}_
43078
+ const windDown = windDownEndingUnder(text, ending);
43079
+ if (windDown !== void 0) return windDownAnswer(windDown, maxMinutes);
43080
+ if (ending.kind !== "written_up") return text || "_(no response)_";
43081
+ return text ? `\u26A0\uFE0F _${ending.summary}_
42761
43082
 
42762
43083
  ${text}` : `\u26A0\uFE0F ${ending.summary}`;
42763
- }
42764
43084
  }
42765
43085
  function unknownHarnessWordOf(state) {
42766
43086
  if (typeof state !== "object" || state === null) return void 0;
@@ -43034,7 +43354,13 @@ async function plan(deps, input) {
43034
43354
  }
43035
43355
  let text;
43036
43356
  try {
43037
- text = (await deps.readFile(entry.repo, request2.planPath, base)).content;
43357
+ const file = await deps.readFile(entry.repo, request2.planPath, base, { maxChars: PLAN_MAX_CHARS });
43358
+ if (file.truncated === true)
43359
+ return {
43360
+ ok: false,
43361
+ reply: `\u{1F6AB} The plan \`${request2.planPath}\` is longer than ${PLAN_MAX_CHARS.toLocaleString("en-US")} characters at \`${base}\` in ${entry.repo}; its later units would be lost, so nothing was run \u2014 split the plan.`
43362
+ };
43363
+ text = file.content;
43038
43364
  } catch (err2) {
43039
43365
  return {
43040
43366
  ok: false,
@@ -43384,10 +43710,10 @@ async function runShipBranch(deps, msg, io, ctx) {
43384
43710
  );
43385
43711
  if (!pre.ok) {
43386
43712
  console.log(`[ship] ${msg.threadKey} not started: ${pre.where}`);
43387
- await refuse3("ship_preflight", async () => {
43388
- await card.done(shell.close({ kind: "refused", icon: "\u{1F6AB}", reason: pre.card, ...closeLines(clock4(), false) }));
43389
- await io.reply(pre.reply);
43390
- });
43713
+ await refuse3(
43714
+ refusalOf("ship_preflight", pre.reply),
43715
+ () => card.done(shell.close({ kind: "refused", icon: "\u{1F6AB}", reason: pre.card, ...closeLines(clock4(), false) }))
43716
+ );
43391
43717
  return;
43392
43718
  }
43393
43719
  const entry = pre.entry;
@@ -43530,12 +43856,19 @@ async function runShipBranch(deps, msg, io, ctx) {
43530
43856
  if (!held2.ok) {
43531
43857
  const reason2 = `budget ${caps.maxMinutes} min cannot hold the ship loop (${caps.maxRounds} review rounds need ${held2.need} min)`;
43532
43858
  console.log(`[ship] ${msg.threadKey} not started: ${reason2}`);
43533
- await refuse3("ship_budget", async () => {
43534
- await card.done(shell.close({ kind: "refused", icon: "\u{1F6AB}", reason: reason2, ...closeLines(clock4(), false) }));
43535
- await io.reply(
43536
- `\u{1F6AB} Ship cannot start under a ${caps.maxMinutes}-minute budget: the loop it allows (${caps.maxRounds} review rounds) needs ${held2.need} minutes \u2014 ${ALLOWANCES.provision} to provision, the coding child's ${ASKS.coding}, and the reserve for the rounds after it at their floors. Widen the budget or the boundary that clipped it, or run \`agent:coding\` for a single pass without the review loop.`
43537
- );
43538
- });
43859
+ await refuse3(
43860
+ refusalOf(
43861
+ "ship_budget",
43862
+ REFUSAL_SENTENCES.ship_budget({
43863
+ maxMinutes: caps.maxMinutes,
43864
+ maxRounds: caps.maxRounds,
43865
+ need: held2.need,
43866
+ provision: ALLOWANCES.provision,
43867
+ coding: ASKS.coding
43868
+ })
43869
+ ),
43870
+ () => card.done(shell.close({ kind: "refused", icon: "\u{1F6AB}", reason: reason2, ...closeLines(clock4(), false) }))
43871
+ );
43539
43872
  return;
43540
43873
  }
43541
43874
  const scopes = deps.config.scopes(msg.channelId, msg.userId);
@@ -43560,7 +43893,7 @@ async function runShipBranch(deps, msg, io, ctx) {
43560
43893
  "dispatch.ship_hand_off",
43561
43894
  () => handOffToCoordinator(
43562
43895
  {
43563
- readFile: (repo, path, ref) => githubCapabilityFor(deps, chatActorOf(deps.config, msg)).api.readFile(repo, path, ref),
43896
+ readFile: (repo, path, ref, opts) => githubCapabilityFor(deps, chatActorOf(deps.config, msg)).api.readFile(repo, path, ref, opts),
43564
43897
  instances: deps.coordinatorInstances ?? new NullCoordinatorInstanceStore(),
43565
43898
  create: deps.createCoordinatorInstance ?? ((id) => createInstanceViaShim(shim(), id)),
43566
43899
  status: deps.fetchCoordinatorInstanceStatus ?? ((id) => fetchInstanceStatusViaShim(shim(), id))
@@ -43657,6 +43990,8 @@ var init_ship = __esm({
43657
43990
  init_runRegistry();
43658
43991
  init_statusCardFrame();
43659
43992
  init_types5();
43993
+ init_refusal();
43994
+ init_reply();
43660
43995
  }
43661
43996
  });
43662
43997
 
@@ -44312,10 +44647,28 @@ async function dispatch(deps, msg, io, opts = {}) {
44312
44647
  const queuedBeforeMs = msg.originAt !== void 0 ? Math.max(0, receivedAt - msg.originAt) : void 0;
44313
44648
  const queued = queuedCaption("behind", opts.queuedBehindMs) ?? queuedCaption("before", queuedBeforeMs);
44314
44649
  let refused7 = false;
44315
- const refuse3 = (outcome, fn) => {
44650
+ const stampRefusal = (outcome) => {
44316
44651
  refused7 = true;
44652
+ const cause = causeOf(outcome);
44317
44653
  ended.refusal ??= outcome;
44318
- return root.span("dispatch.refuse", fn, { attrs: { outcome } });
44654
+ ended.cause ??= cause;
44655
+ root.setAttrs({ refusal: outcome, cause });
44656
+ return cause;
44657
+ };
44658
+ const refuse3 = async (refusal, side) => {
44659
+ const cause = stampRefusal(refusal.code);
44660
+ await root.span(
44661
+ "dispatch.refuse",
44662
+ async () => {
44663
+ await side?.();
44664
+ await renderRefusal(refusal, io);
44665
+ },
44666
+ { attrs: { outcome: refusal.code, refusal: refusal.code, cause } }
44667
+ );
44668
+ };
44669
+ const refuseSilently = (outcome, side) => {
44670
+ const cause = stampRefusal(outcome);
44671
+ return root.span("dispatch.refuse", side, { attrs: { outcome, refusal: outcome, cause } });
44319
44672
  };
44320
44673
  const closeLines = (end, finished, owner = "agent") => cardLines(trace, { end, finished, owner, queued });
44321
44674
  const doneLines = (diagnosis) => {
@@ -44436,6 +44789,7 @@ async function dispatch(deps, msg, io, opts = {}) {
44436
44789
  clock: clock4,
44437
44790
  root,
44438
44791
  refuse: refuse3,
44792
+ refuseSilently,
44439
44793
  admission,
44440
44794
  hooks: {
44441
44795
  reservation: reservationHooks,
@@ -44701,7 +45055,7 @@ async function dispatch(deps, msg, io, opts = {}) {
44701
45055
  const request2 = await abandonLostWorkspace({
44702
45056
  msg,
44703
45057
  io,
44704
- refuse: refuse3,
45058
+ refuse: refuseSilently,
44705
45059
  card,
44706
45060
  shell,
44707
45061
  closeLines,
@@ -44992,10 +45346,13 @@ async function dispatch(deps, msg, io, opts = {}) {
44992
45346
  return ended;
44993
45347
  } catch (err2) {
44994
45348
  caught = true;
45349
+ ended.refusal ??= "uncaught";
45350
+ ended.cause ??= "system";
45351
+ if (!refused7) root.setAttrs({ refusal: "uncaught", cause: "system" });
44995
45352
  const errMsg2 = err2 instanceof Error ? err2.message : String(err2);
44996
45353
  if (setupCard && setupShell) {
44997
45354
  const [failedCard, failedShell] = [setupCard, setupShell];
44998
- await refuse3(
45355
+ await refuseSilently(
44999
45356
  "setup_failed",
45000
45357
  () => failedCard.done(
45001
45358
  failedShell.close({
@@ -45008,16 +45365,21 @@ async function dispatch(deps, msg, io, opts = {}) {
45008
45365
  });
45009
45366
  }
45010
45367
  const replyName = root.record().attrs.runId !== void 0 ? "post.reply" : "dispatch.refuse";
45368
+ const replyAttrs = replyName === "dispatch.refuse" ? { attrs: { outcome: "uncaught", refusal: "uncaught", cause: "system" } } : void 0;
45011
45369
  await ending.sealAfterReply(
45012
45370
  async () => {
45013
45371
  },
45014
- () => root.span(replyName, () => {
45015
- const line3 = redactSecrets(stripAnsi(errorReply(err2)));
45016
- const link = admitted?.runLink;
45017
- return io.reply(link ? `${line3}
45372
+ () => root.span(
45373
+ replyName,
45374
+ () => {
45375
+ const line3 = redactSecrets(stripAnsi(errorReply(err2)));
45376
+ const link = admitted?.runLink;
45377
+ return io.reply(link ? `${line3}
45018
45378
 
45019
45379
  [Live run](${link})` : line3);
45020
- })
45380
+ },
45381
+ replyAttrs
45382
+ )
45021
45383
  ).catch(() => {
45022
45384
  });
45023
45385
  } finally {
@@ -45089,10 +45451,14 @@ async function dispatchClick(deps, click) {
45089
45451
  registry: deps.runRegistry ?? defaultRunRegistry,
45090
45452
  onFinished: (id) => void deps.runBearers?.revoke(id)
45091
45453
  });
45092
- const refuse3 = (outcome, text) => {
45454
+ const refuse3 = (refusal) => {
45093
45455
  refused7 = true;
45094
- ended.refusal = outcome;
45095
- return root.span("dispatch.refuse", () => io.reply(text), { attrs: { outcome } });
45456
+ ended.refusal = refusal.code;
45457
+ ended.cause = refusal.cause;
45458
+ root.setAttrs({ refusal: refusal.code, cause: refusal.cause });
45459
+ return root.span("dispatch.refuse", () => renderRefusal(refusal, io), {
45460
+ attrs: { outcome: refusal.code, refusal: refusal.code, cause: refusal.cause }
45461
+ });
45096
45462
  };
45097
45463
  try {
45098
45464
  if (click.kind === "cancel") {
@@ -45101,7 +45467,7 @@ async function dispatchClick(deps, click) {
45101
45467
  await ending.sealAfterReply(
45102
45468
  async () => {
45103
45469
  },
45104
- () => refuse3(res2.refusal, res2.text)
45470
+ () => refuse3(refusalOf(res2.refusal, res2.text))
45105
45471
  );
45106
45472
  else
45107
45473
  await ending.sealAfterReply(
@@ -45113,10 +45479,33 @@ async function dispatchClick(deps, click) {
45113
45479
  }
45114
45480
  const res = await consumeAndRun(deps, { id: click.id, actorIds }, io, ending, trace);
45115
45481
  if (res.kind === "refused") {
45482
+ const refusal = refusalOf(res.refusal, res.text);
45483
+ if (res.row) {
45484
+ const row2 = res.row;
45485
+ await recordRoutedDecision(
45486
+ deps,
45487
+ row2.message,
45488
+ io,
45489
+ { id: row2.command },
45490
+ {
45491
+ preset: COMMAND_RUN_AGENT,
45492
+ reason: REFUSED_REASON,
45493
+ model: row2.model,
45494
+ command: row2.command,
45495
+ input: redactedInput(row2.input),
45496
+ receipt: row2.receipt,
45497
+ outcome: "refused",
45498
+ refusalCode: refusal.code
45499
+ },
45500
+ res.text,
45501
+ ending,
45502
+ trace
45503
+ );
45504
+ }
45116
45505
  await ending.sealAfterReply(
45117
45506
  async () => {
45118
45507
  },
45119
- () => refuse3(res.refusal, res.text)
45508
+ () => refuse3(refusal)
45120
45509
  );
45121
45510
  return ended;
45122
45511
  }
@@ -45154,10 +45543,13 @@ var init_dispatcher = __esm({
45154
45543
  init_runEvents();
45155
45544
  init_redact();
45156
45545
  init_reply();
45546
+ init_refusal();
45157
45547
  init_admission();
45158
45548
  init_fastPath();
45159
45549
  init_confirm();
45160
45550
  init_commandRun();
45551
+ init_runOwner();
45552
+ init_route();
45161
45553
  init_references();
45162
45554
  init_actor();
45163
45555
  init_config2();
@@ -47929,7 +48321,12 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
47929
48321
  const reason2 = finaleAbortReason(lease.finaleMs);
47930
48322
  writeUpFailed ??= reason2;
47931
48323
  run2.onProgress?.(finaleTimedOutNote());
47932
- note("harness_error", windDownFailureNote(reason2));
48324
+ const doing = bridge.doingNow();
48325
+ const onTool = doing !== void 0 && doing !== "model";
48326
+ note(
48327
+ "harness_error",
48328
+ cutInFlight || onTool ? finaleWaitNote(reason2, doingWords(doing), cutInFlight) : windDownFailureNote(reason2)
48329
+ );
47933
48330
  void interrupt({ ending: "the write-up's finale" });
47934
48331
  }
47935
48332
  return;
@@ -48212,9 +48609,9 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
48212
48609
  throw replaced;
48213
48610
  }
48214
48611
  if (interrupts.cut || interrupts.ending !== void 0) {
48215
- const { cut, ending } = interrupts;
48612
+ const { cut, ending: ending2 } = interrupts;
48216
48613
  bridge.closeOpenSpans(
48217
- (open2) => cut && ending !== void 0 ? `${open2.tool} was cut at the loop's end; the loop then left on its interrupt (${ending}) before the call's outcome reached the record` : ending !== void 0 ? `${open2.tool} was still running when the loop left on its interrupt (${ending}); its outcome never reached the record` : `${open2.tool} was cut at the loop's end and its outcome never reached the record before the write-up settled`,
48614
+ (open2) => cut && ending2 !== void 0 ? `${open2.tool} was cut at the loop's end; the loop then left on its interrupt (${ending2}) before the call's outcome reached the record` : ending2 !== void 0 ? `${open2.tool} was still running when the loop left on its interrupt (${ending2}); its outcome never reached the record` : `${open2.tool} was cut at the loop's end and its outcome never reached the record before the write-up settled`,
48218
48615
  { cut: true }
48219
48616
  );
48220
48617
  }
@@ -48258,14 +48655,9 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
48258
48655
  note("wrap_up", wrapUpNeverPostedNote(writeUp.kind, "run", neverPostedEnd));
48259
48656
  writeUp = void 0;
48260
48657
  }
48261
- const answer = writeUpAnswer(writeUp, text, run2.agent.maxMinutes, writeUpFailed, neverPostedEnd);
48262
- return { answer, remainingMs: remaining, hardStopped: false, ...handOver() };
48263
- }
48264
- function writeUpAnswer(writeUp, text, maxMinutes, writeUpFailed, neverPostedEnd) {
48265
- if (writeUp?.kind === "time") return timeBudgetAnswer(text, maxMinutes, writeUpFailed);
48266
- if (writeUp?.kind === "turns") return turnGuardAnswer(text, writeUp.pace, writeUpFailed);
48267
- if (writeUp?.kind === "soft") return softStopAnswer(text, writeUpFailed);
48268
- return unlabelledAnswer(text, writeUpFailed, neverPostedEnd === "finale" ? "finale" : "failed");
48658
+ const ending = windDownEndingOf(writeUp, text, writeUpFailed, neverPostedEnd === "finale" ? "finale" : "failed");
48659
+ const answer = ending ? windDownAnswer(ending, run2.agent.maxMinutes) : unlabelledAnswer(text, void 0);
48660
+ return { answer, ...ending ? { ending } : {}, remainingMs: remaining, hardStopped: false, ...handOver() };
48269
48661
  }
48270
48662
  var OpenCodeGateBypassedError, OpenCodeReplyFailedError, OpenCodeRequestRefusedError, OpenCodeWriteUnresolvedError, FIRST_EVENT_BOUND_MS, SILENCE_TAIL_BYTES, OpenCodeSilentError, OpenCodeContainerReplacedError, TOOL_NAME_WORD, RESOURCE_PERMISSIONS, isRecord9, str7, TURN_BUDGET_CODE, TURN_BUDGET_REFUSAL, OpenCodeBridge, StepBoundaries;
48271
48663
  var init_bridge3 = __esm({
@@ -49395,12 +49787,13 @@ async function openOpenCodeRun(deps, run2, settings) {
49395
49787
  (err2) => err2
49396
49788
  );
49397
49789
  let answer;
49790
+ let ending;
49398
49791
  let remainingMs;
49399
49792
  let hardStopped;
49400
49793
  let drained;
49401
49794
  try {
49402
49795
  let storeIds;
49403
- ({ answer, remainingMs, storeIds, hardStopped } = await driveOpenCode(deps, run2, conn));
49796
+ ({ answer, ending, remainingMs, storeIds, hardStopped } = await driveOpenCode(deps, run2, conn));
49404
49797
  for (const id of storeIds) known.add(id);
49405
49798
  } finally {
49406
49799
  draining = false;
@@ -49409,6 +49802,7 @@ async function openOpenCodeRun(deps, run2, settings) {
49409
49802
  if (drained instanceof OpenCodeWriteUnresolvedError && !hardStopped) throw drained;
49410
49803
  return {
49411
49804
  answer,
49805
+ ...ending ? { ending } : {},
49412
49806
  followUp: async (input) => {
49413
49807
  const turnEnd = await feedEndFrom(deps.container, conn.paths.feed, conn.feedOffset);
49414
49808
  const previous = live.toolContext;
@@ -53884,14 +54278,54 @@ function viewAsFromCookie(header4) {
53884
54278
  function isViewAsPath(pathname) {
53885
54279
  return pathname === VIEW_AS_PATH || pathname === VIEW_AS_EXIT_PATH;
53886
54280
  }
53887
- function requestersOf(rows) {
54281
+ function peopleOf(rows, known) {
53888
54282
  const seen = /* @__PURE__ */ new Map();
53889
- for (const r of rows) {
53890
- if (!r.userId || !isViewablePerson(r.userId) || seen.has(r.userId)) continue;
53891
- seen.set(r.userId, { id: r.userId, ...r.userName ? { name: r.userName } : {} });
53892
- }
54283
+ const add = (id, name) => {
54284
+ if (!isViewablePerson(id)) return;
54285
+ const had = seen.get(id);
54286
+ if (!had) seen.set(id, { id, ...name ? { name } : {} });
54287
+ else if (!had.name && name) seen.set(id, { id, name });
54288
+ };
54289
+ for (const r of rows) if (r.userId) add(r.userId, r.userName);
54290
+ for (const p of known) add(p.id, p.name);
53893
54291
  return [...seen.values()].sort((a, b) => (a.name ?? a.id).localeCompare(b.name ?? b.id));
53894
54292
  }
54293
+ async function knownPeople(deps) {
54294
+ const rows = await deps.requesters().catch(() => []);
54295
+ const fromRuns = peopleOf(rows, []);
54296
+ const namedIds = new Set(fromRuns.filter((p) => p.name).map((p) => p.id));
54297
+ const granted = await Promise.all(
54298
+ deps.granted().map(async (id) => {
54299
+ if (namedIds.has(id)) return { id };
54300
+ const name = await deps.name(id).catch(() => void 0);
54301
+ return { id, ...name ? { name } : {} };
54302
+ })
54303
+ );
54304
+ return peopleOf(rows, granted);
54305
+ }
54306
+ function peopleSource(deps, opts = {}) {
54307
+ const ttlMs = opts.ttlMs ?? KNOWN_PEOPLE_TTL_MS;
54308
+ const now = opts.now ?? Date.now;
54309
+ let people = [];
54310
+ let refreshedAt = Number.NEGATIVE_INFINITY;
54311
+ let inFlight;
54312
+ const refresh = () => {
54313
+ inFlight ??= knownPeople(deps).then((next) => {
54314
+ people = next;
54315
+ refreshedAt = now();
54316
+ }).catch(() => void 0).finally(() => {
54317
+ inFlight = void 0;
54318
+ });
54319
+ return inFlight;
54320
+ };
54321
+ return {
54322
+ current() {
54323
+ if (now() - refreshedAt >= ttlMs) void refresh();
54324
+ return people;
54325
+ },
54326
+ refresh
54327
+ };
54328
+ }
53895
54329
  function refuseWhileViewing(res, person) {
53896
54330
  res.writeHead(403, JSON_NO_STORE);
53897
54331
  res.end(JSON.stringify({ error: "unauthorized", message: viewingRefusal(person) }));
@@ -53955,10 +54389,11 @@ function createViewAsHandler(deps) {
53955
54389
  return true;
53956
54390
  };
53957
54391
  }
53958
- var VIEW_AS_COOKIE, VIEW_AS_PATH, VIEW_AS_EXIT_PATH, MAX_BODY_BYTES3, JSON_NO_STORE;
54392
+ var VIEW_AS_COOKIE, VIEW_AS_PATH, VIEW_AS_EXIT_PATH, MAX_BODY_BYTES3, KNOWN_PEOPLE_TTL_MS, JSON_NO_STORE;
53959
54393
  var init_viewAs2 = __esm({
53960
54394
  "../../src/channels/viewAs.ts"() {
53961
54395
  "use strict";
54396
+ init_budgets();
53962
54397
  init_viewAs();
53963
54398
  init_commandHttp();
53964
54399
  init_http();
@@ -53966,20 +54401,11 @@ var init_viewAs2 = __esm({
53966
54401
  VIEW_AS_PATH = "/runs/view-as";
53967
54402
  VIEW_AS_EXIT_PATH = "/runs/view-as/exit";
53968
54403
  MAX_BODY_BYTES3 = 1024;
54404
+ KNOWN_PEOPLE_TTL_MS = MINUTE_MS;
53969
54405
  JSON_NO_STORE = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
53970
54406
  }
53971
54407
  });
53972
54408
 
53973
- // ../../src/channels/liveView/html.ts
53974
- function escapeHtml(s) {
53975
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
53976
- }
53977
- var init_html = __esm({
53978
- "../../src/channels/liveView/html.ts"() {
53979
- "use strict";
53980
- }
53981
- });
53982
-
53983
54409
  // ../../src/channels/favicon.ts
53984
54410
  function faviconSvg(fill) {
53985
54411
  return `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><circle cx='8' cy='8' r='6' fill='${fill}'/></svg>`;
@@ -54005,121 +54431,6 @@ var init_favicon = __esm({
54005
54431
  }
54006
54432
  });
54007
54433
 
54008
- // ../../src/channels/residentsModel.ts
54009
- function residentStateTone(state) {
54010
- switch (state) {
54011
- case "warm":
54012
- return "green";
54013
- case "onboarding":
54014
- case "refreshing":
54015
- case "restoring":
54016
- return "amber";
54017
- case "degraded":
54018
- case "down":
54019
- return "red";
54020
- default:
54021
- return "grey";
54022
- }
54023
- }
54024
- function residentsFleetTone(records) {
54025
- if (records.length === 0) return "grey";
54026
- const tones = new Set(records.map((record2) => residentStateTone(residentLive(rec(record2)).state)));
54027
- if (tones.has("red")) return "red";
54028
- if (tones.has("amber")) return "amber";
54029
- return tones.size === 1 && tones.has("green") ? "green" : "grey";
54030
- }
54031
- function residentSlug(record2) {
54032
- return str8(record2.resource).replace(/^repo:/, "");
54033
- }
54034
- function residentLive(record2) {
54035
- const live = rec(record2.live);
54036
- if (typeof live.error === "string") return { ...live, state: "unreachable", reason: live.error };
54037
- return { ...live, state: str8(live.state) || "unknown", reason: str8(live.reason) };
54038
- }
54039
- var RESIDENT_SLUG_RE, str8, rec;
54040
- var init_residentsModel = __esm({
54041
- "../../src/channels/residentsModel.ts"() {
54042
- "use strict";
54043
- init_residentDiskBudget();
54044
- RESIDENT_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?\/[a-z0-9](?:[a-z0-9._-]{0,98}[a-z0-9])?$/;
54045
- str8 = (v) => typeof v === "string" ? v : typeof v === "number" ? String(v) : "";
54046
- rec = (v) => v && typeof v === "object" ? v : {};
54047
- }
54048
- });
54049
-
54050
- // ../../src/channels/webSeed.ts
54051
- function serializeSeed(seed) {
54052
- return JSON.stringify(seed).replace(
54053
- /[<>&\u2028\u2029]/g,
54054
- (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`
54055
- );
54056
- }
54057
- var SEED_ELEMENT_ID;
54058
- var init_webSeed = __esm({
54059
- "../../src/channels/webSeed.ts"() {
54060
- "use strict";
54061
- SEED_ELEMENT_ID = "sb-seed";
54062
- }
54063
- });
54064
-
54065
- // ../../src/channels/webShell.ts
54066
- function renderShell(title, seed, assets) {
54067
- const css = assets.css.map((href) => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join("\n");
54068
- const favicon = pageFavicon(seed);
54069
- return `<!doctype html>
54070
- <html lang="en" class="dark">
54071
- <head>
54072
- <meta charset="utf-8" />
54073
- <meta name="viewport" content="width=device-width, initial-scale=1" />
54074
- <meta name="robots" content="noindex" />
54075
- <title>${escapeHtml(title)}</title>
54076
- <link rel="icon" id="favicon" href="${favicon}" />
54077
- ${css}
54078
- </head>
54079
- <body>
54080
- <div id="app"></div>
54081
- <script type="application/json" id="${SEED_ELEMENT_ID}">${serializeSeed(seed)}</script>
54082
- <script type="module" src="${escapeHtml(assets.js)}"></script>
54083
- </body>
54084
- </html>`;
54085
- }
54086
- function pageFavicon(seed) {
54087
- switch (seed.page) {
54088
- case "runs":
54089
- case "run":
54090
- return FAVICON_IDLE;
54091
- case "residents":
54092
- return FAVICON_BY_TONE[residentsFleetTone(seed.residents)];
54093
- default:
54094
- return FAVICON_DEFAULT;
54095
- }
54096
- }
54097
- function makeShellRenderer(assets, capabilities) {
54098
- return (viewer, title, seed) => renderShell(
54099
- title,
54100
- { ...seed, capabilities, ...viewer?.viewingAs ? { viewingAs: viewer.viewingAs } : {} },
54101
- assets
54102
- );
54103
- }
54104
- var PAGE_CSP, FORM_PAGE_CSP, WEB_HTML_HEADERS;
54105
- var init_webShell = __esm({
54106
- "../../src/channels/webShell.ts"() {
54107
- "use strict";
54108
- init_html();
54109
- init_favicon();
54110
- init_residentsModel();
54111
- init_webSeed();
54112
- PAGE_CSP = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; media-src 'self'; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
54113
- FORM_PAGE_CSP = PAGE_CSP.replace("form-action 'none'", "form-action 'self'");
54114
- WEB_HTML_HEADERS = {
54115
- "content-type": "text/html; charset=utf-8",
54116
- "content-security-policy": PAGE_CSP,
54117
- "x-frame-options": "DENY",
54118
- "cache-control": "no-store"
54119
- };
54120
- }
54121
- });
54122
-
54123
54434
  // ../../src/channels/liveView/sse.ts
54124
54435
  function sseData(event, seq) {
54125
54436
  return `id: ${seq}
@@ -54233,8 +54544,8 @@ function serveIndexEvents(subscribeIndex, sink, onLive) {
54233
54544
  onLive?.();
54234
54545
  }
54235
54546
  function withOmittedMarkers(events, eventCount) {
54236
- const counted = events.filter((e) => !(isSpanRecord(e) && e.seq === void 0)).length;
54237
- const omitted = eventCount - counted;
54547
+ const counted2 = events.filter((e) => !(isSpanRecord(e) && e.seq === void 0)).length;
54548
+ const omitted = eventCount - counted2;
54238
54549
  if (omitted <= 0) return [...events];
54239
54550
  const marker = (n2) => ({
54240
54551
  type: "replay_note",
@@ -54290,6 +54601,31 @@ var init_sse = __esm({
54290
54601
  }
54291
54602
  });
54292
54603
 
54604
+ // ../../src/channels/liveView/html.ts
54605
+ function escapeHtml(s) {
54606
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
54607
+ }
54608
+ var init_html = __esm({
54609
+ "../../src/channels/liveView/html.ts"() {
54610
+ "use strict";
54611
+ }
54612
+ });
54613
+
54614
+ // ../../src/channels/webSeed.ts
54615
+ function serializeSeed(seed) {
54616
+ return JSON.stringify(seed).replace(
54617
+ /[<>&\u2028\u2029]/g,
54618
+ (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`
54619
+ );
54620
+ }
54621
+ var SEED_ELEMENT_ID;
54622
+ var init_webSeed = __esm({
54623
+ "../../src/channels/webSeed.ts"() {
54624
+ "use strict";
54625
+ SEED_ELEMENT_ID = "sb-seed";
54626
+ }
54627
+ });
54628
+
54293
54629
  // ../../src/channels/liveView.ts
54294
54630
  function parseRunRoute(pathname) {
54295
54631
  if (pathname === "/runs" || pathname === "/runs/") return { kind: "index" };
@@ -54457,14 +54793,11 @@ function createLiveViewHandler(deps) {
54457
54793
  return token2 === void 0 ? v : { ...v, token: token2 };
54458
54794
  };
54459
54795
  const liveTokens = () => new Map(index.listActive().map((s) => [s.id, s.token]));
54460
- const notFoundPage = (res, viewer) => {
54461
- res.writeHead(404, WEB_HTML_HEADERS);
54462
- res.end(
54463
- deps.shell(viewer, "Run not found", {
54464
- page: "runNotFound",
54465
- retentionDays: deps.retention ? deps.retention.retentionDays : null
54466
- })
54467
- );
54796
+ const notFoundPage = (req, res, viewer) => {
54797
+ deps.page(req, res, 404, viewer, "Run not found", {
54798
+ page: "runNotFound",
54799
+ retentionDays: deps.retention ? deps.retention.retentionDays : null
54800
+ });
54468
54801
  };
54469
54802
  const mergedRows = async (live, visibleTo, cursor, view = {}) => {
54470
54803
  const tokens = new Map(live.map((s) => [s.id, s.token]));
@@ -54508,6 +54841,7 @@ function createLiveViewHandler(deps) {
54508
54841
  }
54509
54842
  const live = index.listActive().filter((s) => matchesPredicate(visibleTo, s));
54510
54843
  const render3 = (page2) => {
54844
+ const people = holdsAll(ctx.actor) ? peopleOf(page2.rows, deps.people?.() ?? []) : void 0;
54511
54845
  const liveCount = page2.rows.filter((r) => !r.finished).length;
54512
54846
  const title = `${liveCount > 0 ? `(${liveCount}) ` : ""}${all ? "All runs" : "Live runs"}`;
54513
54847
  const seed = {
@@ -54515,9 +54849,7 @@ function createLiveViewHandler(deps) {
54515
54849
  all,
54516
54850
  mine,
54517
54851
  ...ctx.actor.asUser ? { asUser: ctx.actor.asUser } : {},
54518
- // A session holding `all` may view the dashboard as a person (record 0053): the
54519
- // picker offers the requesters this page names; the seed says nothing to anyone else.
54520
- ...holdsAll(ctx.actor) ? { viewAs: { people: requestersOf(page2.rows) } } : {},
54852
+ ...people ? { viewAs: { people } } : {},
54521
54853
  retentionDays: deps.retention ? deps.retention.retentionDays : null,
54522
54854
  now: now(),
54523
54855
  rows: [...page2.rows],
@@ -54525,8 +54857,7 @@ function createLiveViewHandler(deps) {
54525
54857
  ...page2.olderHref ? { olderHref: page2.olderHref } : {},
54526
54858
  ...page2.olderThan !== void 0 ? { olderThan: page2.olderThan } : {}
54527
54859
  };
54528
- res.writeHead(200, WEB_HTML_HEADERS);
54529
- res.end(deps.shell(ctx.actor, title, seed));
54860
+ deps.page(req, res, 200, ctx.actor, title, seed);
54530
54861
  };
54531
54862
  if (!all) {
54532
54863
  run2(
@@ -54541,8 +54872,7 @@ function createLiveViewHandler(deps) {
54541
54872
  if (route3.kind === "scheduled") {
54542
54873
  const scheduled = deps.scheduled;
54543
54874
  if (!scheduled) {
54544
- res.writeHead(200, WEB_HTML_HEADERS);
54545
- res.end(deps.shell(ctx.actor, "Scheduled runs", { page: "scheduled", now: now(), rows: null }));
54875
+ deps.page(req, res, 200, ctx.actor, "Scheduled runs", { page: "scheduled", now: now(), rows: null });
54546
54876
  return true;
54547
54877
  }
54548
54878
  const visibleTo = readableRuns(ctx.actor);
@@ -54555,8 +54885,7 @@ function createLiveViewHandler(deps) {
54555
54885
  rows: buildScheduledRows(scheduled.schedules, firings, live, t),
54556
54886
  ...firings.ok ? {} : { firingsUnavailable: firings.reason }
54557
54887
  };
54558
- res.writeHead(200, WEB_HTML_HEADERS);
54559
- res.end(deps.shell(ctx.actor, "Scheduled runs", seed));
54888
+ deps.page(req, res, 200, ctx.actor, "Scheduled runs", seed);
54560
54889
  };
54561
54890
  if (!scheduled.store) {
54562
54891
  render3({ ok: false, reason: NO_STORE_REASON });
@@ -54570,7 +54899,7 @@ function createLiveViewHandler(deps) {
54570
54899
  run2(res, async () => {
54571
54900
  const found = await service.listUnitRuns(route3.key, visibleTo);
54572
54901
  if (!found.ok) {
54573
- notFoundPage(res, ctx.actor);
54902
+ notFoundPage(req, res, ctx.actor);
54574
54903
  return;
54575
54904
  }
54576
54905
  const findings = found.value.pr ? await service.listFindings({ repo: found.value.instance.repo, number: found.value.pr.number }, visibleTo) : void 0;
@@ -54585,8 +54914,7 @@ function createLiveViewHandler(deps) {
54585
54914
  now: now(),
54586
54915
  retentionDays: deps.retention ? deps.retention.retentionDays : null
54587
54916
  };
54588
- res.writeHead(200, WEB_HTML_HEADERS);
54589
- res.end(deps.shell(ctx.actor, `Unit ${found.value.id}`, seed));
54917
+ deps.page(req, res, 200, ctx.actor, `Unit ${found.value.id}`, seed);
54590
54918
  });
54591
54919
  return true;
54592
54920
  }
@@ -54599,29 +54927,26 @@ function createLiveViewHandler(deps) {
54599
54927
  const { token: own, ...row2 } = s;
54600
54928
  return s.finished ? row2 : { ...row2, token: own };
54601
54929
  });
54602
- res.writeHead(200, WEB_HTML_HEADERS);
54603
- res.end(
54604
- deps.shell(ctx.actor, "Live run", {
54605
- page: "run",
54606
- mode: "live",
54607
- id: route3.id,
54608
- ...(() => {
54609
- const key = index.listActive().find((s) => s.id === route3.id)?.threadKey;
54610
- return key !== void 0 ? { threadKey: key } : {};
54611
- })(),
54612
- ...children.length > 0 ? { children } : {},
54613
- // Stop control: same token, POST-only; `&mode=` is appended client-side.
54614
- eventsUrl: `/runs/${encodeURIComponent(route3.id)}/events?t=${encodeURIComponent(token2)}`,
54615
- stopUrl: `/runs/${encodeURIComponent(route3.id)}/stop?t=${encodeURIComponent(token2)}`,
54616
- // The files' URL base with the same token (item 26): the page appends each key.
54617
- ...artifactsSeed(route3.id, token2),
54618
- // The stamps the header's one duration reads (docs/reference/specs/tracing.md).
54619
- serverNow: now(),
54620
- startedAt: snap?.startedAt ?? now(),
54621
- ...snap?.receivedAt !== void 0 ? { receivedAt: snap.receivedAt } : {},
54622
- ...snap?.finishedAt !== void 0 ? { finishedAt: snap.finishedAt } : {}
54623
- })
54624
- );
54930
+ deps.page(req, res, 200, ctx.actor, "Live run", {
54931
+ page: "run",
54932
+ mode: "live",
54933
+ id: route3.id,
54934
+ ...(() => {
54935
+ const key = index.listActive().find((s) => s.id === route3.id)?.threadKey;
54936
+ return key !== void 0 ? { threadKey: key } : {};
54937
+ })(),
54938
+ ...children.length > 0 ? { children } : {},
54939
+ // Stop control: same token, POST-only; `&mode=` is appended client-side.
54940
+ eventsUrl: `/runs/${encodeURIComponent(route3.id)}/events?t=${encodeURIComponent(token2)}`,
54941
+ stopUrl: `/runs/${encodeURIComponent(route3.id)}/stop?t=${encodeURIComponent(token2)}`,
54942
+ // The files' URL base with the same token (item 26): the page appends each key.
54943
+ ...artifactsSeed(route3.id, token2),
54944
+ // The stamps the header's one duration reads (docs/reference/specs/tracing.md).
54945
+ serverNow: now(),
54946
+ startedAt: snap?.startedAt ?? now(),
54947
+ ...snap?.receivedAt !== void 0 ? { receivedAt: snap.receivedAt } : {},
54948
+ ...snap?.finishedAt !== void 0 ? { finishedAt: snap.finishedAt } : {}
54949
+ });
54625
54950
  return true;
54626
54951
  }
54627
54952
  if (route3.kind === "artifact") {
@@ -54724,7 +55049,7 @@ function createLiveViewHandler(deps) {
54724
55049
  sink.write(NOT_FOUND2);
54725
55050
  sink.end();
54726
55051
  } else if (route3.kind === "page") {
54727
- notFoundPage(res, ctx.actor);
55052
+ notFoundPage(req, res, ctx.actor);
54728
55053
  } else text(res, 404, NOT_FOUND2);
54729
55054
  return;
54730
55055
  }
@@ -54750,37 +55075,34 @@ function createLiveViewHandler(deps) {
54750
55075
  ]);
54751
55076
  const findingsLedger = ledger?.ok && ledger.value.unit !== void 0 ? { unit: ledger.value.unit, rows: ledger.value.findings.length } : void 0;
54752
55077
  const tokens = liveTokens();
54753
- res.writeHead(200, WEB_HTML_HEADERS);
54754
- res.end(
54755
- deps.shell(ctx.actor, "Run", {
54756
- page: "run",
54757
- mode: "history",
54758
- id: route3.id,
54759
- ...view.threadKey !== void 0 ? { threadKey: view.threadKey } : {},
54760
- ...children.length > 0 ? { children: children.map((c) => withLiveToken(c, tokens)) } : {},
54761
- ...units.length > 0 ? { units } : {},
54762
- ...findingsLedger !== void 0 ? { findingsLedger } : {},
54763
- // The stored stream with the truncation made visible (AE11): the
54764
- // seed IS the stream on a history page — normalized first on a
54765
- // span-schema record, so a pair whose twin the budget dropped gets
54766
- // it back.
54767
- events: withOmittedMarkers(timed ? normalizeSpans(events) : events, view.eventCount),
54768
- ...timed ? {} : { untimed: true },
54769
- ...view.status ? { status: view.status } : {},
54770
- eventCount: view.eventCount,
54771
- startedAt: view.startedAt,
54772
- ...view.receivedAt !== void 0 ? { receivedAt: view.receivedAt } : {},
54773
- ...view.finishedAt !== void 0 ? { finishedAt: view.finishedAt } : {},
54774
- ...view.sealedAt !== void 0 ? { sealedAt: view.sealedAt } : {},
54775
- ...view.replyOk !== void 0 ? { replyOk: view.replyOk } : {},
54776
- ...runDurationMs(view) !== void 0 ? { durationMs: runDurationMs(view) } : {},
54777
- // The run's dollars beside its duration (costs.md item 4c); a record without usage has none.
54778
- ...view.cost !== void 0 ? { cost: view.cost } : {},
54779
- ...view.truncated !== void 0 ? { truncated: view.truncated } : {},
54780
- // Tokenless: the files are read under the same decision as this page (item 26).
54781
- ...artifactsSeed(route3.id)
54782
- })
54783
- );
55078
+ deps.page(req, res, 200, ctx.actor, "Run", {
55079
+ page: "run",
55080
+ mode: "history",
55081
+ id: route3.id,
55082
+ ...view.threadKey !== void 0 ? { threadKey: view.threadKey } : {},
55083
+ ...children.length > 0 ? { children: children.map((c) => withLiveToken(c, tokens)) } : {},
55084
+ ...units.length > 0 ? { units } : {},
55085
+ ...findingsLedger !== void 0 ? { findingsLedger } : {},
55086
+ // The stored stream with the truncation made visible (AE11): the
55087
+ // seed IS the stream on a history page — normalized first on a
55088
+ // span-schema record, so a pair whose twin the budget dropped gets
55089
+ // it back.
55090
+ events: withOmittedMarkers(timed ? normalizeSpans(events) : events, view.eventCount),
55091
+ ...timed ? {} : { untimed: true },
55092
+ ...view.status ? { status: view.status } : {},
55093
+ eventCount: view.eventCount,
55094
+ startedAt: view.startedAt,
55095
+ ...view.receivedAt !== void 0 ? { receivedAt: view.receivedAt } : {},
55096
+ ...view.finishedAt !== void 0 ? { finishedAt: view.finishedAt } : {},
55097
+ ...view.sealedAt !== void 0 ? { sealedAt: view.sealedAt } : {},
55098
+ ...view.replyOk !== void 0 ? { replyOk: view.replyOk } : {},
55099
+ ...runDurationMs(view) !== void 0 ? { durationMs: runDurationMs(view) } : {},
55100
+ // The run's dollars beside its duration (costs.md item 4c); a record without usage has none.
55101
+ ...view.cost !== void 0 ? { cost: view.cost } : {},
55102
+ ...view.truncated !== void 0 ? { truncated: view.truncated } : {},
55103
+ // Tokenless: the files are read under the same decision as this page (item 26).
55104
+ ...artifactsSeed(route3.id)
55105
+ });
54784
55106
  return;
54785
55107
  }
54786
55108
  serveHistoryEvents(view.events ?? [], view.eventCount, nodeSseSink(req, res));
@@ -54806,7 +55128,6 @@ var init_liveView = __esm({
54806
55128
  init_scheduledPanel();
54807
55129
  init_viewAs();
54808
55130
  init_viewAs2();
54809
- init_webShell();
54810
55131
  init_favicon();
54811
55132
  init_sse();
54812
55133
  init_html();
@@ -54898,6 +55219,138 @@ var init_webAssets = __esm({
54898
55219
  }
54899
55220
  });
54900
55221
 
55222
+ // ../../src/channels/residentsModel.ts
55223
+ function residentStateTone(state) {
55224
+ switch (state) {
55225
+ case "warm":
55226
+ return "green";
55227
+ case "onboarding":
55228
+ case "refreshing":
55229
+ case "restoring":
55230
+ return "amber";
55231
+ case "degraded":
55232
+ case "down":
55233
+ return "red";
55234
+ default:
55235
+ return "grey";
55236
+ }
55237
+ }
55238
+ function residentsFleetTone(records) {
55239
+ if (records.length === 0) return "grey";
55240
+ const tones = new Set(records.map((record2) => residentStateTone(residentLive(rec(record2)).state)));
55241
+ if (tones.has("red")) return "red";
55242
+ if (tones.has("amber")) return "amber";
55243
+ return tones.size === 1 && tones.has("green") ? "green" : "grey";
55244
+ }
55245
+ function residentSlug(record2) {
55246
+ return str8(record2.resource).replace(/^repo:/, "");
55247
+ }
55248
+ function residentLive(record2) {
55249
+ const live = rec(record2.live);
55250
+ if (typeof live.error === "string") return { ...live, state: "unreachable", reason: live.error };
55251
+ return { ...live, state: str8(live.state) || "unknown", reason: str8(live.reason) };
55252
+ }
55253
+ var RESIDENT_SLUG_RE, str8, rec;
55254
+ var init_residentsModel = __esm({
55255
+ "../../src/channels/residentsModel.ts"() {
55256
+ "use strict";
55257
+ init_residentDiskBudget();
55258
+ RESIDENT_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?\/[a-z0-9](?:[a-z0-9._-]{0,98}[a-z0-9])?$/;
55259
+ str8 = (v) => typeof v === "string" ? v : typeof v === "number" ? String(v) : "";
55260
+ rec = (v) => v && typeof v === "object" ? v : {};
55261
+ }
55262
+ });
55263
+
55264
+ // ../../src/channels/pageFavicon.ts
55265
+ function pageFavicon(seed) {
55266
+ switch (seed.page) {
55267
+ case "runs":
55268
+ case "run":
55269
+ return FAVICON_IDLE;
55270
+ case "residents":
55271
+ return FAVICON_BY_TONE[residentsFleetTone(seed.residents)];
55272
+ default:
55273
+ return FAVICON_DEFAULT;
55274
+ }
55275
+ }
55276
+ var init_pageFavicon = __esm({
55277
+ "../../src/channels/pageFavicon.ts"() {
55278
+ "use strict";
55279
+ init_favicon();
55280
+ init_residentsModel();
55281
+ }
55282
+ });
55283
+
55284
+ // ../../src/channels/webShell.ts
55285
+ function renderShell(seed, assets) {
55286
+ const css = assets.css.map((href) => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join("\n");
55287
+ const favicon = pageFavicon(seed);
55288
+ return `<!doctype html>
55289
+ <html lang="en" class="dark">
55290
+ <head>
55291
+ <meta charset="utf-8" />
55292
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
55293
+ <meta name="robots" content="noindex" />
55294
+ <title>${escapeHtml(seed.title)}</title>
55295
+ <link rel="icon" id="favicon" href="${favicon}" />
55296
+ ${css}
55297
+ </head>
55298
+ <body>
55299
+ <div id="app"></div>
55300
+ <script type="application/json" id="${SEED_ELEMENT_ID}">${serializeSeed(seed)}</script>
55301
+ <script type="module" src="${escapeHtml(assets.js)}"></script>
55302
+ </body>
55303
+ </html>`;
55304
+ }
55305
+ function wantsSeed(req) {
55306
+ const accept = req.headers.accept;
55307
+ if (typeof accept !== "string") return false;
55308
+ const types = accept.split(",").map((t) => t.split(";")[0].trim().toLowerCase());
55309
+ return types.includes("application/json") && !types.includes("text/html");
55310
+ }
55311
+ function makePageSender(assets, capabilities) {
55312
+ return (req, res, status3, viewer, title, seed) => {
55313
+ const island = {
55314
+ ...seed,
55315
+ title,
55316
+ capabilities,
55317
+ ...viewer?.viewingAs ? { viewingAs: viewer.viewingAs } : {}
55318
+ };
55319
+ if (wantsSeed(req)) {
55320
+ const body = JSON.stringify(island);
55321
+ res.writeHead(status3, WEB_SEED_HEADERS);
55322
+ res.end(body);
55323
+ return;
55324
+ }
55325
+ const html = renderShell(island, assets);
55326
+ res.writeHead(status3, WEB_HTML_HEADERS);
55327
+ res.end(html);
55328
+ };
55329
+ }
55330
+ var PAGE_CSP, FORM_PAGE_CSP, WEB_HTML_HEADERS, WEB_SEED_HEADERS;
55331
+ var init_webShell = __esm({
55332
+ "../../src/channels/webShell.ts"() {
55333
+ "use strict";
55334
+ init_html();
55335
+ init_pageFavicon();
55336
+ init_webSeed();
55337
+ PAGE_CSP = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; media-src 'self'; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
55338
+ FORM_PAGE_CSP = PAGE_CSP.replace("form-action 'none'", "form-action 'self'");
55339
+ WEB_HTML_HEADERS = {
55340
+ "content-type": "text/html; charset=utf-8",
55341
+ "content-security-policy": PAGE_CSP,
55342
+ "x-frame-options": "DENY",
55343
+ "cache-control": "no-store",
55344
+ vary: "accept"
55345
+ };
55346
+ WEB_SEED_HEADERS = {
55347
+ "content-type": "application/json; charset=utf-8",
55348
+ "cache-control": "no-store",
55349
+ vary: "accept"
55350
+ };
55351
+ }
55352
+ });
55353
+
54901
55354
  // ../../src/channels/residentsFeed.ts
54902
55355
  function frame(f) {
54903
55356
  return `data: ${JSON.stringify(f)}
@@ -55028,7 +55481,7 @@ async function readListing(client) {
55028
55481
  return { listing: { cap: r.data.cap, count: r.data.count, residents } };
55029
55482
  }
55030
55483
  function createResidentsViewHandler(deps) {
55031
- const { client, shell, runs } = deps;
55484
+ const { client, page: page2, runs } = deps;
55032
55485
  const now = deps.now ?? Date.now;
55033
55486
  return (req, res, ctx) => {
55034
55487
  const url = new URL(req.url ?? "/", "http://localhost");
@@ -55089,8 +55542,7 @@ function createResidentsViewHandler(deps) {
55089
55542
  now: now(),
55090
55543
  runs: live
55091
55544
  };
55092
- res.writeHead(200, WEB_HTML_HEADERS);
55093
- res.end(shell(ctx.actor, "Resident repos", seed));
55545
+ page2(req, res, 200, ctx.actor, "Resident repos", seed);
55094
55546
  finish2(200);
55095
55547
  return;
55096
55548
  }
@@ -55099,8 +55551,7 @@ function createResidentsViewHandler(deps) {
55099
55551
  plainEnd(404, `${route3.slug} is not onboarded as a resident`);
55100
55552
  return;
55101
55553
  }
55102
- res.writeHead(200, WEB_HTML_HEADERS);
55103
- res.end(shell(ctx.actor, route3.slug, { page: "resident", slug: route3.slug, record: record2 }));
55554
+ page2(req, res, 200, ctx.actor, route3.slug, { page: "resident", slug: route3.slug, record: record2 });
55104
55555
  finish2(200);
55105
55556
  });
55106
55557
  return true;
@@ -55119,7 +55570,6 @@ var init_residentsView = __esm({
55119
55570
  init_viewer();
55120
55571
  init_residentsFeed();
55121
55572
  init_residentsModel();
55122
- init_webShell();
55123
55573
  init_residentsModel();
55124
55574
  UPSTREAM_REASON_MAX = 500;
55125
55575
  }
@@ -55451,10 +55901,7 @@ function createWebChatHandler(deps) {
55451
55901
  plain5(405, "method not allowed", { allow: "GET" });
55452
55902
  return true;
55453
55903
  }
55454
- const render3 = (title, seed) => {
55455
- res.writeHead(200, WEB_HTML_HEADERS);
55456
- res.end(deps.shell(ctx.actor, title, seed));
55457
- };
55904
+ const render3 = (title, seed) => deps.page(req, res, 200, ctx.actor, title, seed);
55458
55905
  const sub = subOf(ctx.actor);
55459
55906
  if (route3.kind === "new") {
55460
55907
  const conversation = mintId();
@@ -55464,8 +55911,7 @@ function createWebChatHandler(deps) {
55464
55911
  const threadKey = threadKeyFor(sub, route3.id);
55465
55912
  turnsOf(threadKey, ctx.actor).then(async (open2) => {
55466
55913
  if (open2.runs.length === 0 && !ownLane(sub, threadKey)) {
55467
- res.writeHead(404, WEB_HTML_HEADERS);
55468
- res.end(deps.shell(ctx.actor, "Run not found", { page: "runNotFound", retentionDays }));
55914
+ deps.page(req, res, 404, ctx.actor, "Run not found", { page: "runNotFound", retentionDays });
55469
55915
  return;
55470
55916
  }
55471
55917
  const seed = await seedFor(ctx, route3.id, threadKey, open2);
@@ -55489,7 +55935,6 @@ var init_web2 = __esm({
55489
55935
  init_commandHttp();
55490
55936
  init_http();
55491
55937
  init_viewer();
55492
- init_webShell();
55493
55938
  init_viewAs();
55494
55939
  PLATFORM5 = "web";
55495
55940
  CONVERSATION_ID_BYTES = 10;
@@ -55572,7 +56017,7 @@ function json(res, body) {
55572
56017
  res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
55573
56018
  res.end(JSON.stringify(body));
55574
56019
  }
55575
- function createCostsViewHandler(service, shell, opts = {}) {
56020
+ function createCostsViewHandler(service, page2, opts = {}) {
55576
56021
  const names = opts.names ?? NO_NAMES;
55577
56022
  return (req, res, ctx = {}) => {
55578
56023
  const url = new URL(req.url ?? "/", "http://localhost");
@@ -55621,19 +56066,16 @@ function createCostsViewHandler(service, shell, opts = {}) {
55621
56066
  }
55622
56067
  const by = route3.view === "daily" ? Promise.resolve(null) : orNone(byReport(route3.view));
55623
56068
  Promise.all([orNone(service.report(group, days)), by]).then(([report2, byReport2]) => {
55624
- res.writeHead(200, WEB_HTML_HEADERS);
55625
- res.end(
55626
- shell(ctx.actor, `${report2?.label ?? group} spend`, {
55627
- page: "costs",
55628
- group,
55629
- report: report2,
55630
- groups,
55631
- view: route3.view,
55632
- ...byReport2 ? { by: byReport2 } : {},
55633
- snapshot: service.status(),
55634
- canSnapshot: canSnapshot(ctx.actor)
55635
- })
55636
- );
56069
+ page2(req, res, 200, ctx.actor, `${report2?.label ?? group} spend`, {
56070
+ page: "costs",
56071
+ group,
56072
+ report: report2,
56073
+ groups,
56074
+ view: route3.view,
56075
+ ...byReport2 ? { by: byReport2 } : {},
56076
+ snapshot: service.status(),
56077
+ canSnapshot: canSnapshot(ctx.actor)
56078
+ });
55637
56079
  }).catch(failed);
55638
56080
  return true;
55639
56081
  };
@@ -55646,7 +56088,6 @@ var init_costsView = __esm({
55646
56088
  init_names();
55647
56089
  init_costsService();
55648
56090
  init_sse();
55649
- init_webShell();
55650
56091
  DIMENSION_OF_VIEW = Object.freeze({
55651
56092
  users: "user",
55652
56093
  threads: "thread",
@@ -55679,7 +56120,7 @@ function plain3(res, status3, body, extra = {}) {
55679
56120
  res.writeHead(status3, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", ...extra });
55680
56121
  res.end(body);
55681
56122
  }
55682
- function createDeliveryViewHandler(deps, shell) {
56123
+ function createDeliveryViewHandler(deps, page2) {
55683
56124
  return (req, res, ctx) => {
55684
56125
  const url = new URL(req.url ?? "/", "http://localhost");
55685
56126
  const route3 = parseDeliveryRoute(url.pathname);
@@ -55708,13 +56149,13 @@ function createDeliveryViewHandler(deps, shell) {
55708
56149
  const fresh = url.searchParams.get("fresh") === "1";
55709
56150
  const runs = async (range) => {
55710
56151
  if (!deps.runs) return [];
55711
- const page2 = await deps.runs.listRuns({
56152
+ const page3 = await deps.runs.listRuns({
55712
56153
  status: "finished",
55713
56154
  visibleTo: predicateFor(ctx.actor, "runs:read", "run"),
55714
56155
  sinceMs: Date.parse(`${range.since}T00:00:00Z`),
55715
56156
  limit: RUN_LIST_MAX_LIMIT
55716
56157
  });
55717
- return runFactsOf(page2.runs, repo);
56158
+ return runFactsOf(page3.runs, repo);
55718
56159
  };
55719
56160
  deps.service.report(repo, {
55720
56161
  ...weeksParam !== null ? { weeks: Number(weeksParam) } : {},
@@ -55728,9 +56169,7 @@ function createDeliveryViewHandler(deps, shell) {
55728
56169
  res.end(body);
55729
56170
  return;
55730
56171
  }
55731
- const html = shell(ctx.actor, `${report2.repo} delivery`, { page: "delivery", report: report2, repos });
55732
- res.writeHead(200, WEB_HTML_HEADERS);
55733
- res.end(html);
56172
+ page2(req, res, 200, ctx.actor, `${report2.repo} delivery`, { page: "delivery", report: report2, repos });
55734
56173
  }).catch((err2) => {
55735
56174
  const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX4);
55736
56175
  plain3(res, 502, `delivery sources unavailable: ${reason2}`);
@@ -55745,7 +56184,6 @@ var init_deliveryView = __esm({
55745
56184
  init_predicate();
55746
56185
  init_delivery();
55747
56186
  init_runRecord();
55748
- init_webShell();
55749
56187
  SEGMENT = /^(?!\.+$)[\w.-]{1,100}$/;
55750
56188
  DELIVERY_NO_REPOS_MESSAGE = "No repository to report on \u2014 name one or more `owner/name` under delivery.repos in config to enable this view (the `delivery report --repo` command needs no config).";
55751
56189
  UPSTREAM_REASON_MAX4 = 400;
@@ -55810,7 +56248,7 @@ function channelScopeView(description) {
55810
56248
  function homeTab(caps) {
55811
56249
  return caps.mcp ? "mcps" : "channels";
55812
56250
  }
55813
- function createSettingsViewHandler(deps, shell) {
56251
+ function createSettingsViewHandler(deps, page2) {
55814
56252
  const canWrite = (caller, action, kind, id) => authorize(
55815
56253
  caller.actor,
55816
56254
  action,
@@ -55905,10 +56343,7 @@ function createSettingsViewHandler(deps, shell) {
55905
56343
  }
55906
56344
  plain4(res, 502, `settings unavailable: ${reason2}`);
55907
56345
  };
55908
- const render3 = (viewer, seed) => {
55909
- res.writeHead(200, WEB_HTML_HEADERS);
55910
- res.end(shell(viewer, "Settings", seed));
55911
- };
56346
+ const render3 = (viewer, seed) => page2(req, res, 200, viewer, "Settings", seed);
55912
56347
  const channel = "channel" in route3 ? route3.channel : void 0;
55913
56348
  deps.callerFor(ctx.identity).then(async (caller) => {
55914
56349
  const base = {
@@ -55941,7 +56376,6 @@ var init_settingsView = __esm({
55941
56376
  "use strict";
55942
56377
  init_authorize();
55943
56378
  init_names();
55944
- init_webShell();
55945
56379
  CHANNEL_ID_RE = /^[a-z]+:[A-Za-z0-9_.:-]{1,80}$/;
55946
56380
  UPSTREAM_REASON_MAX5 = 400;
55947
56381
  failureText = (r) => r.message;
@@ -57335,18 +57769,22 @@ async function contractFor(instance, unit, readers) {
57335
57769
  if (isGenerated(instance)) {
57336
57770
  source2 = { unit: await generatedUnitOf(instance, unit, readers) };
57337
57771
  } else {
57338
- const planMarkdown = await readers.readRepoFile(instance.plan.path);
57339
- if (planMarkdown === void 0)
57772
+ const plan3 = await readers.readRepoFile(instance.plan.path, { maxChars: PLAN_MAX_CHARS });
57773
+ if (plan3 === void 0)
57340
57774
  throw new Error(`the plan ${instance.plan.path} is not readable at ${rebase.onto} in ${instance.repo}`);
57341
- source2 = { planMarkdown, unitId: unit.unit };
57775
+ if (plan3.truncated)
57776
+ throw new Error(
57777
+ `the plan ${instance.plan.path} is longer than ${PLAN_MAX_CHARS.toLocaleString("en-US")} characters at ${rebase.onto} in ${instance.repo}; a unit read from a cut plan could be briefed short, so none is`
57778
+ );
57779
+ source2 = { planMarkdown: plan3.content, unitId: unit.unit };
57342
57780
  }
57343
57781
  const section = ("unit" in source2 ? source2.unit : parsePlanUnit(source2.planMarkdown, unit.unit))?.section ?? "";
57344
57782
  const specs = /* @__PURE__ */ new Map();
57345
57783
  for (const spec of new Set(specItemRefs(section).map((r) => r.spec)))
57346
- specs.set(spec, await readers.readRepoFile(`${SPECS_DIR2}/${spec}`));
57784
+ specs.set(spec, (await readers.readRepoFile(`${SPECS_DIR2}/${spec}`))?.content);
57347
57785
  let agentRules;
57348
57786
  for (const file of RULES_FILES2) {
57349
- const text = await readers.readRepoFile(file);
57787
+ const text = (await readers.readRepoFile(file))?.content;
57350
57788
  if (text !== void 0) {
57351
57789
  agentRules = { file, text };
57352
57790
  break;
@@ -58474,9 +58912,10 @@ ${planSummary(units)}`).catch(() => {
58474
58912
  function briefReaders(deps, instance) {
58475
58913
  const ref = instance.base ?? "main";
58476
58914
  return {
58477
- readRepoFile: async (path) => {
58915
+ readRepoFile: async (path, opts) => {
58478
58916
  try {
58479
- return (await deps.github.readFile(instance.repo, path, ref)).content;
58917
+ const file = await deps.github.readFile(instance.repo, path, ref, opts);
58918
+ return { content: file.content, truncated: file.truncated };
58480
58919
  } catch {
58481
58920
  return void 0;
58482
58921
  }
@@ -59180,17 +59619,17 @@ async function runBot() {
59180
59619
  const cronArmed = Object.values(auth.tokens).some((id) => id.subject === "cron");
59181
59620
  const schedulesState = `${SCHEDULES.length} schedule(s) on /runs (${capabilities.schedules ? `firings from ${config.config.schedules?.worker?.baseUrl}` : "no firing store"}; cron identity ${cronArmed ? "armed" : "NOT in SWITCHBOARD_INGRESS_TOKENS \u2014 scheduled runs fail closed"})`;
59182
59621
  const webAssets = loadWebAssets(webDistDir(publicEnv(), PACKAGE_ROOT));
59183
- const shell = makeShellRenderer(webAssets.entry, capabilities);
59622
+ const page2 = makePageSender(webAssets.entry, capabilities);
59184
59623
  const residentsView = createResidentsViewHandler({
59185
59624
  client: residentAdminClient,
59186
- shell,
59625
+ page: page2,
59187
59626
  runs: defaultRunRegistry,
59188
59627
  trace: { config, spanLog }
59189
59628
  });
59190
59629
  const residentsState = residentAdminClient instanceof NullResidentAdminClient ? `GET /residents (503 \u2014 ${residentAdminClient.reason})` : `GET /residents (dash \u2192 ${config.config.execution?.resident?.baseUrl})`;
59191
- const costsView = createCostsViewHandler(costsService, shell, { names });
59630
+ const costsView = createCostsViewHandler(costsService, page2, { names });
59192
59631
  const costsState = costs ? `GET /costs (${costsService.groups().join(",")}; LLM ${costs.llmOn ? "on" : "off"}; snapshot every ${costsCfg?.snapshot.everyHours ?? "?"} h)` : costsCfg ? `GET /costs (503 \u2014 ${costsCfg.cloudflareTokenEnv} not set)` : "GET /costs (503 \u2014 no costs config)";
59193
- const deliveryView = createDeliveryViewHandler({ service: deliveryService, runs: runsService }, shell);
59632
+ const deliveryView = createDeliveryViewHandler({ service: deliveryService, runs: runsService }, page2);
59194
59633
  const settingsView = createSettingsViewHandler(
59195
59634
  {
59196
59635
  commands,
@@ -59205,14 +59644,14 @@ async function runBot() {
59205
59644
  },
59206
59645
  capabilities
59207
59646
  },
59208
- shell
59647
+ page2
59209
59648
  );
59210
59649
  const webChat = createWebChatHandler({
59211
59650
  core: deps,
59212
59651
  service: runsService,
59213
59652
  registry: defaultRunRegistry,
59214
59653
  commands,
59215
- shell,
59654
+ page: page2,
59216
59655
  capabilities,
59217
59656
  names,
59218
59657
  retention: capabilities.runHistory && runHistoryCfg ? { retentionDays: retentionPolicyOf(runHistoryCfg).retentionDays } : null,
@@ -59223,10 +59662,18 @@ async function runBot() {
59223
59662
  const liveViewState = process.env.PUBLIC_BASE_URL ? "GET /runs (index) + /runs/:id (live view)" : "GET /runs (index) + live view (no PUBLIC_BASE_URL \u2014 per-run links omitted)";
59224
59663
  const accessConfig = parseAccessConfig(publicEnv());
59225
59664
  const publicBaseUrl = process.env.PUBLIC_BASE_URL;
59665
+ const viewAsPeople = peopleSource({
59666
+ granted: () => config.grantedPeople(),
59667
+ requesters: async () => (await runsService.listRuns({ status: "all", visibleTo: { kind: "all" }, limit: RUN_LIST_MAX_LIMIT })).runs,
59668
+ name: personName
59669
+ });
59670
+ void viewAsPeople.refresh();
59226
59671
  const liveView2 = createLiveViewHandler({
59227
- shell,
59672
+ page: page2,
59228
59673
  service: runsService,
59229
59674
  index: defaultRunRegistry,
59675
+ // The view-as picker's people (record 0053): the cache `viewAsPeople` keeps.
59676
+ people: () => viewAsPeople.current(),
59230
59677
  retention: capabilities.runHistory && runHistoryCfg ? { retentionDays: retentionPolicyOf(runHistoryCfg).retentionDays } : null,
59231
59678
  // The panel's own off-state wording (live-view item 14: "firing history
59232
59679
  // unavailable", never "never fired") stands until the dashboard paints the
@@ -59603,6 +60050,7 @@ var init_index = __esm({
59603
60050
  init_confirmations();
59604
60051
  init_mcpConnectView();
59605
60052
  init_viewAs2();
60053
+ init_runRecord();
59606
60054
  init_lookups();
59607
60055
  init_slackNames();
59608
60056
  init_names();