@coreplane/switchboard 1.240.0 → 1.241.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 (125) hide show
  1. package/dist/assets/config/config.example.yaml +26 -7
  2. package/dist/assets/deploy/cloudflare/worker.ts +77 -18
  3. package/dist/assets/deploy/cloudflare-resident/worker.ts +1 -0
  4. package/dist/assets/deploy/secrets.manifest.json +6 -0
  5. package/dist/assets/package-lock.json +3 -3
  6. package/dist/assets/package.json +1 -1
  7. package/dist/assets/source.json +3 -3
  8. package/dist/assets/src/agents/registry.ts +27 -8
  9. package/dist/assets/src/core/authz/actor.ts +5 -0
  10. package/dist/assets/src/core/authz/grants.ts +20 -9
  11. package/dist/assets/src/core/coordinator/contract.ts +37 -0
  12. package/dist/assets/src/core/coordinator/driver.ts +17 -1
  13. package/dist/assets/src/core/coordinator/instancesRoute.ts +72 -0
  14. package/dist/assets/src/core/costs.ts +10 -5
  15. package/dist/assets/src/core/ship/coordinator.ts +22 -7
  16. package/dist/assets/src/core/trace/attrs.ts +1 -1
  17. package/dist/assets/src/execution/bindingPurge.ts +2 -2
  18. package/dist/assets/web/dist/.vite/manifest.json +422 -420
  19. package/dist/assets/web/dist/assets/AppShell-DEy5jRuS.js +1 -0
  20. package/dist/assets/web/dist/assets/{CostsPage-BD3w5j_S.js → CostsPage-BuHD-Bv7.js} +2 -2
  21. package/dist/assets/web/dist/assets/{DeliveryPage-BtvzKA1m.js → DeliveryPage-BGpPr5xr.js} +1 -1
  22. package/dist/assets/web/dist/assets/HomePage-dKBvWh-E.js +2 -0
  23. package/dist/assets/web/dist/assets/{NotFoundPage-DwOX1HaZ.js → NotFoundPage-DtmQM2gD.js} +1 -1
  24. package/dist/assets/web/dist/assets/PendingTurnRow-CrEAWx5b.js +1 -0
  25. package/dist/assets/web/dist/assets/{ResidentDetailPage-CUAOh5d9.js → ResidentDetailPage-C-48fxat.js} +1 -1
  26. package/dist/assets/web/dist/assets/{ResidentsIndexPage-B3KUlYy2.js → ResidentsIndexPage-CBBIjPOa.js} +1 -1
  27. package/dist/assets/web/dist/assets/RunFoldRow-B6vxjcBI.js +1 -0
  28. package/dist/assets/web/dist/assets/RunRoutePage-CQjd3Cz-.js +6 -0
  29. package/dist/assets/web/dist/assets/RunsIndexPage-Cy9G6D0p.js +1 -0
  30. package/dist/assets/web/dist/assets/{RunsTabs-BnkvCBrM.js → RunsTabs-CtNGNnhN.js} +1 -1
  31. package/dist/assets/web/dist/assets/{ScheduledPage-C1y9jgAk.js → ScheduledPage-Ci1Ygqnc.js} +1 -1
  32. package/dist/assets/web/dist/assets/SettingsPage-DxmJvAmT.js +1 -0
  33. package/dist/assets/web/dist/assets/{StatusDot-CiByEtoR.js → StatusDot-DPWE1JmO.js} +1 -1
  34. package/dist/assets/web/dist/assets/{Tooltip-DyRxk8A8.js → Tooltip-DKhSRH9t.js} +1 -1
  35. package/dist/assets/web/dist/assets/{UnitRoutePage-CnWyOizV.js → UnitRoutePage-DEN5bSQf.js} +1 -1
  36. package/dist/assets/web/dist/assets/{angular-html-DAQ_1T1v.js → angular-html-DgSK1qvr.js} +1 -1
  37. package/dist/assets/web/dist/assets/{angular-ts-D4kxRVp1.js → angular-ts-D3gNdiSG.js} +1 -1
  38. package/dist/assets/web/dist/assets/{apl-Dy07l64y.js → apl-CkHCYM8I.js} +1 -1
  39. package/dist/assets/web/dist/assets/{astro-DZIbTlc7.js → astro-DLm45axt.js} +1 -1
  40. package/dist/assets/web/dist/assets/{blade-CmSXWX3l.js → blade-BKa-VE-c.js} +1 -1
  41. package/dist/assets/web/dist/assets/{c-ggzVdJLK.js → c-C8NCNWai.js} +1 -1
  42. package/dist/assets/web/dist/assets/{chapel-Bo1wktsG.js → chapel-DqDQ7IKH.js} +1 -1
  43. package/dist/assets/web/dist/assets/{cobol-DJUmtKM2.js → cobol-j4vCD6AJ.js} +1 -1
  44. package/dist/assets/web/dist/assets/{coffee-BZWOVz4C.js → coffee-C6oDVH-0.js} +1 -1
  45. package/dist/assets/web/dist/assets/{cpp-Dtk39yMw.js → cpp-Bd3A1Rtc.js} +1 -1
  46. package/dist/assets/web/dist/assets/{crystal-CGf_-Zoj.js → crystal-CUOgOw0d.js} +1 -1
  47. package/dist/assets/web/dist/assets/{css-CX2fcL4u.js → css-RYljyv7G.js} +1 -1
  48. package/dist/assets/web/dist/assets/{dist-D8TaC961.js → dist-BnJXKHCY.js} +2 -2
  49. package/dist/assets/web/dist/assets/{edge-BYS8FtgQ.js → edge-BJhNdQDc.js} +1 -1
  50. package/dist/assets/web/dist/assets/{elixir-fhUpi3nO.js → elixir-g1AWOAfT.js} +1 -1
  51. package/dist/assets/web/dist/assets/{elm-DAQNiwsN.js → elm-CER5e4Dz.js} +1 -1
  52. package/dist/assets/web/dist/assets/{erb-CWzEys6U.js → erb-D5aOmMQf.js} +1 -1
  53. package/dist/assets/web/dist/assets/{git-rebase-9rY3p-Rw.js → git-rebase-D6rfV8jp.js} +1 -1
  54. package/dist/assets/web/dist/assets/{glimmer-js-tY7hy0nE.js → glimmer-js-BvDH3-mC.js} +1 -1
  55. package/dist/assets/web/dist/assets/{glimmer-ts-CTrLv5JF.js → glimmer-ts-DChtNs9V.js} +1 -1
  56. package/dist/assets/web/dist/assets/{glsl-BRJbiDfq.js → glsl-BHlsWrxf.js} +1 -1
  57. package/dist/assets/web/dist/assets/{graphql-B-MtVLh2.js → graphql-BsaBHb53.js} +1 -1
  58. package/dist/assets/web/dist/assets/{hack-BtIkOypQ.js → hack-0lbDosCd.js} +1 -1
  59. package/dist/assets/web/dist/assets/{haml-CoOH6gfa.js → haml-BKtco2vr.js} +1 -1
  60. package/dist/assets/web/dist/assets/{handlebars-DgrRu-GP.js → handlebars-BE_cj04z.js} +1 -1
  61. package/dist/assets/web/dist/assets/{html-DZaaI7ca.js → html-BNcr8EVd.js} +1 -1
  62. package/dist/assets/web/dist/assets/{html-derivative-DgHvT-qZ.js → html-derivative-BsJt2Kei.js} +1 -1
  63. package/dist/assets/web/dist/assets/{http-CvAd51yz.js → http-BR5P8ER_.js} +1 -1
  64. package/dist/assets/web/dist/assets/{hurl-CXGqAfzx.js → hurl-CtHalpWS.js} +1 -1
  65. package/dist/assets/web/dist/assets/{indexRow-Cse3JGb0.js → indexRow-BD1VT8o8.js} +1 -1
  66. package/dist/assets/web/dist/assets/{java-CIQStIXF.js → java-BXVdhN11.js} +1 -1
  67. package/dist/assets/web/dist/assets/{javascript-WbH8s7E3.js → javascript-CyoShgNQ.js} +1 -1
  68. package/dist/assets/web/dist/assets/{jinja-DzyWEVKC.js → jinja-BQYZzHex.js} +1 -1
  69. package/dist/assets/web/dist/assets/{jison-CrT-zCa-.js → jison-DqrUa1Eq.js} +1 -1
  70. package/dist/assets/web/dist/assets/{json-lHjpjXMQ.js → json-CQiTF9Jj.js} +1 -1
  71. package/dist/assets/web/dist/assets/{jsx-WTwYCHW6.js → jsx-Y16MkCYQ.js} +1 -1
  72. package/dist/assets/web/dist/assets/{julia-Bqpk9H7E.js → julia-zn16YONe.js} +1 -1
  73. package/dist/assets/web/dist/assets/{just-LdXk1r3m.js → just-DcVeN_MN.js} +1 -1
  74. package/dist/assets/web/dist/assets/{latex-h_y2A5KD.js → latex-BNNoF-WP.js} +1 -1
  75. package/dist/assets/web/dist/assets/{liquid-CY7bF2_p.js → liquid-DAi6qHzn.js} +1 -1
  76. package/dist/assets/web/dist/assets/{lua-oQ7XVZOY.js → lua-BswS0axw.js} +1 -1
  77. package/dist/assets/web/dist/assets/{main-CGO6fQ8k.js → main-B3is4sk6.js} +2 -2
  78. package/dist/assets/web/dist/assets/main-DP_zSemY.css +1 -0
  79. package/dist/assets/web/dist/assets/{marko-zv_qZ4QV.js → marko-C-4hkmQZ.js} +1 -1
  80. package/dist/assets/web/dist/assets/{mdc-Dl4kL_YK.js → mdc-0ZaHUkS7.js} +1 -1
  81. package/dist/assets/web/dist/assets/{nginx-CAHCZj4F.js → nginx-DBDg5tOR.js} +1 -1
  82. package/dist/assets/web/dist/assets/{nim-BOtmwCJH.js → nim-BVMz49Yw.js} +1 -1
  83. package/dist/assets/web/dist/assets/{org-DspQk3HQ.js → org-1hO_K6ya.js} +1 -1
  84. package/dist/assets/web/dist/assets/{perl-CAXmvVl0.js → perl-CVRQLvVk.js} +1 -1
  85. package/dist/assets/web/dist/assets/{php-CMUx4VyM.js → php-K9nCrB9n.js} +1 -1
  86. package/dist/assets/web/dist/assets/{pug-B55xVJTv.js → pug-CTxOcmO3.js} +1 -1
  87. package/dist/assets/web/dist/assets/{qml-DsNKCCRk.js → qml-CmCkfG5q.js} +1 -1
  88. package/dist/assets/web/dist/assets/{r-DbhP0VvO.js → r-pP47Xn1X.js} +1 -1
  89. package/dist/assets/web/dist/assets/{razor-BCpE6P-p.js → razor-BpW6r3tC.js} +1 -1
  90. package/dist/assets/web/dist/assets/{regexp-BllsjiN4.js → regexp-CINgdY4N.js} +1 -1
  91. package/dist/assets/web/dist/assets/{rst-CO5Rpq9V.js → rst-BZIUEq2Q.js} +1 -1
  92. package/dist/assets/web/dist/assets/{ruby-C2A4OgQq.js → ruby-nzAOxz6r.js} +1 -1
  93. package/dist/assets/web/dist/assets/{sas-Butzoyql.js → sas-QIS1bFth.js} +1 -1
  94. package/dist/assets/web/dist/assets/{scss-PEUwJBpf.js → scss-BuhBBVNt.js} +1 -1
  95. package/dist/assets/web/dist/assets/{shellscript-3Yhq0khE.js → shellscript-CQc1vXbk.js} +1 -1
  96. package/dist/assets/web/dist/assets/{shellsession-04Hkb7m3.js → shellsession-CoubCAUv.js} +1 -1
  97. package/dist/assets/web/dist/assets/{soy-BnId73cP.js → soy-CPRzlder.js} +1 -1
  98. package/dist/assets/web/dist/assets/{sql-BKwdc0ch.js → sql-W9krb8-9.js} +1 -1
  99. package/dist/assets/web/dist/assets/{sseReplay-DutHNghE.js → sseReplay-yji8a1wM.js} +1 -1
  100. package/dist/assets/web/dist/assets/{stata-BPPtNkay.js → stata-CuISJEC0.js} +1 -1
  101. package/dist/assets/web/dist/assets/{surrealql-CDPChcYP.js → surrealql-CSet7584.js} +1 -1
  102. package/dist/assets/web/dist/assets/{svelte-B15DIQ-z.js → svelte-ChXTSYwK.js} +1 -1
  103. package/dist/assets/web/dist/assets/{templ-D-rp_AiB.js → templ-Bm55v62k.js} +1 -1
  104. package/dist/assets/web/dist/assets/{tex-BnVDCD7p.js → tex-CICMX8Gj.js} +1 -1
  105. package/dist/assets/web/dist/assets/{ts-tags-cpujdyAR.js → ts-tags-CA1UzWyB.js} +1 -1
  106. package/dist/assets/web/dist/assets/{tsx-D1vO4L72.js → tsx-Dy04HNbv.js} +1 -1
  107. package/dist/assets/web/dist/assets/{twig-oQGnQGRc.js → twig-BBHsVnVD.js} +1 -1
  108. package/dist/assets/web/dist/assets/{typescript-CKSFpXiH.js → typescript-Dvc-wVBT.js} +1 -1
  109. package/dist/assets/web/dist/assets/{typst-DKHFo8BF.js → typst-Dvlqx3_q.js} +1 -1
  110. package/dist/assets/web/dist/assets/{vue-html-DpCp0_Sz.js → vue-html-D4YxT3An.js} +1 -1
  111. package/dist/assets/web/dist/assets/{vue-vine-y5d5wUb1.js → vue-vine-0YE7uQlH.js} +1 -1
  112. package/dist/assets/web/dist/assets/{vue-BstokpiS.js → vue-zWi1MNU2.js} +1 -1
  113. package/dist/assets/web/dist/assets/{xml-Br2hx5Ax.js → xml-gm-iksZB.js} +1 -1
  114. package/dist/assets/web/dist/assets/{xsl-Bc8Cobap.js → xsl-D_W5BcrY.js} +1 -1
  115. package/dist/assets/web/dist/assets/{yaml-LOITy_I1.js → yaml-CHmZ21wZ.js} +1 -1
  116. package/dist/cli.js +3411 -502
  117. package/package.json +1 -1
  118. package/dist/assets/web/dist/assets/AppShell-CM_fh-WW.js +0 -1
  119. package/dist/assets/web/dist/assets/HomePage-BU6aRCSe.js +0 -2
  120. package/dist/assets/web/dist/assets/RunFoldRow-Dulcbg25.js +0 -1
  121. package/dist/assets/web/dist/assets/RunRoutePage-DGoGrQZq.js +0 -6
  122. package/dist/assets/web/dist/assets/RunsIndexPage-37W7OZbC.js +0 -1
  123. package/dist/assets/web/dist/assets/SettingsPage-ggfB-Rqr.js +0 -1
  124. package/dist/assets/web/dist/assets/Slideover-CX-LEklF.js +0 -1
  125. package/dist/assets/web/dist/assets/main-O8UXRuEv.css +0 -1
package/dist/cli.js CHANGED
@@ -1032,8 +1032,9 @@ function surfaceKeyFor(actorId) {
1032
1032
  function agentRunAction(agent) {
1033
1033
  return `agent:run:${agent}`;
1034
1034
  }
1035
- function browserReadActions(commandGroups) {
1036
- return new Set(commandGroups.map((g) => `${g}:read`));
1035
+ function browserActions(commandGroups) {
1036
+ if (commandGroups.length === 0) return /* @__PURE__ */ new Set();
1037
+ return /* @__PURE__ */ new Set([...commandGroups.map((g) => `${g}:read`), ...BROWSER_WRITE_ACTIONS]);
1037
1038
  }
1038
1039
  function toSet(v) {
1039
1040
  if (v === "all") return "all";
@@ -1111,7 +1112,7 @@ function parseRestrictConfig(raw, agentNames) {
1111
1112
  }
1112
1113
  function namespaceBaseline(actorId, table) {
1113
1114
  if (actorId.startsWith("slack:")) return table.everyone;
1114
- if (actorId.startsWith("access:") && !actorId.startsWith("access:svc:")) return table.browserReads;
1115
+ if (actorId.startsWith("access:") && !actorId.startsWith("access:svc:")) return table.browser;
1115
1116
  return NO_GRANTS;
1116
1117
  }
1117
1118
  function grantsTable(source2) {
@@ -1119,7 +1120,7 @@ function grantsTable(source2) {
1119
1120
  const openAgents = (source2.agentNames ?? []).filter((a) => !restrict.agents.has(a)).map(agentRunAction);
1120
1121
  const baselines = {
1121
1122
  everyone: { ...NO_GRANTS, actions: /* @__PURE__ */ new Set([...CHAT_OPEN_ACTIONS, ...openAgents]) },
1122
- browserReads: { ...NO_GRANTS, actions: browserReadActions(source2.commandGroups ?? []) }
1123
+ browser: { ...NO_GRANTS, actions: browserActions(source2.commandGroups ?? []) }
1123
1124
  };
1124
1125
  const grants = /* @__PURE__ */ new Map();
1125
1126
  const surfaces = /* @__PURE__ */ new Map();
@@ -1164,7 +1165,7 @@ function unionGrants(a, b) {
1164
1165
  function isEmpty(g) {
1165
1166
  return g.actions !== "all" && g.actions.size === 0 && g.channels !== "all" && g.channels.size === 0 && g.repos !== "all" && g.repos.size === 0;
1166
1167
  }
1167
- var ALL_GRANTS, GRANT_ACTOR_PREFIXES, SURFACE_GRANT_PREFIXES, CHAT_OPEN_ACTIONS, grantList, grantsEntrySchema, NO_RESTRICTION, REPO_SLUG_RE, restrictSchema;
1168
+ var ALL_GRANTS, GRANT_ACTOR_PREFIXES, SURFACE_GRANT_PREFIXES, CHAT_OPEN_ACTIONS, BROWSER_WRITE_ACTIONS, grantList, grantsEntrySchema, NO_RESTRICTION, REPO_SLUG_RE, restrictSchema;
1168
1169
  var init_grants = __esm({
1169
1170
  "../../src/core/authz/grants.ts"() {
1170
1171
  "use strict";
@@ -1185,6 +1186,7 @@ var init_grants = __esm({
1185
1186
  "memory:write",
1186
1187
  "mcp:write"
1187
1188
  ];
1189
+ BROWSER_WRITE_ACTIONS = ["memory:write", "mcp:write"];
1188
1190
  grantList = z.union([z.literal("all"), z.array(z.string().min(1))]);
1189
1191
  grantsEntrySchema = z.object({ actions: grantList.optional(), channels: grantList.optional(), repos: grantList.optional() }).strict();
1190
1192
  NO_RESTRICTION = Object.freeze({ agents: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set() });
@@ -1843,6 +1845,9 @@ function runawayTurnCap(maxMinutes) {
1843
1845
  function loopBudget(maxMinutes) {
1844
1846
  return { maxMinutes, maxTurns: runawayTurnCap(maxMinutes) };
1845
1847
  }
1848
+ function statusCardRule(examples = '"Implement the fix", "Run the test suite"') {
1849
+ return `Maintain the user-facing status card with the update_status tool. Post your plan as a checklist (\u25CB pending) as soon as you have it, then keep it truthful at every moment: the markers are facts, not intentions. Mark an item \u2731 when you issue the first command that does it, and \u2713 only after you have read the result that proves it happened \u2014 never in the same turn as the command, never because you intend to run it next, never for a reporting or posting step you have not done. The order is: mark the item \u2731, run its commands, read the result, then mark it \u2713 and the next item \u2731. The card shows the command running right now beside your checklist, so a \u2713 item whose command is still running reads as a lie. Items are short outcomes (${examples}), never commands. This is the only progress the user sees while you work.`;
1850
+ }
1846
1851
  function conductorSystem(siblings) {
1847
1852
  const readers = siblings.filter((a) => a.identity !== "write");
1848
1853
  const writers = siblings.filter((a) => a.identity === "write");
@@ -1946,7 +1951,7 @@ ${SHOW_FILES}
1946
1951
 
1947
1952
  ${NOTEPAD}
1948
1953
 
1949
- Maintain the user-facing status card with the update_status tool: right after you decide your plan, post it as a checklist (\u25CB pending items), then update it whenever an item starts (\u2731) or finishes (\u2713). Items are short outcomes ("Clone repo and read the diff", "Run the test suite"), never commands. Mark an item \u2713 only after it has actually happened \u2014 never pre-mark reporting/posting steps. This is the only progress the user sees while you work.
1954
+ ${statusCardRule('"Clone repo and read the diff", "Run the test suite"')}
1950
1955
 
1951
1956
  If the request doesn't name a repository and you can't infer it, ask for it instead of guessing.
1952
1957
  Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
@@ -1983,7 +1988,7 @@ ${SHOW_FILES}
1983
1988
 
1984
1989
  ${NOTEPAD}
1985
1990
 
1986
- Maintain the user-facing status card with the update_status tool: right after you decide your plan, post it as a checklist (\u25CB pending items), then update it whenever an item starts (\u2731) or finishes (\u2713). Items are short outcomes ("Implement the fix", "Run the test suite"), never commands. Mark an item \u2713 only after it has actually happened \u2014 never pre-mark reporting/posting steps. This is the only progress the user sees while you work.
1991
+ ${statusCardRule()}
1987
1992
 
1988
1993
  Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
1989
1994
  ${FENCED_CONTENT_RULE}
@@ -2015,7 +2020,7 @@ ${SHOW_FILES}
2015
2020
 
2016
2021
  ${NOTEPAD}
2017
2022
 
2018
- Maintain the user-facing status card with the update_status tool: right after you decide your plan, post it as a checklist (\u25CB pending items), then update it whenever an item starts (\u2731) or finishes (\u2713). Items are short outcomes ("Implement the fix", "Run the test suite"), never commands. Mark an item \u2713 only after it has actually happened \u2014 never pre-mark reporting/posting steps. This is the only progress the user sees while you work.
2023
+ ${statusCardRule()}
2019
2024
 
2020
2025
  Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
2021
2026
  ${FENCED_CONTENT_RULE}
@@ -2050,7 +2055,7 @@ ${REVIEW_VERDICT_INSTRUCTION}
2050
2055
 
2051
2056
  ${NOTEPAD}
2052
2057
 
2053
- Maintain the user-facing status card with the update_status tool: post your plan as a checklist (\u25CB pending), update as items start (\u2731) and finish (\u2713 \u2014 only after they actually happened; never pre-mark reporting steps). Items are short outcomes, never commands.
2058
+ ${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
2054
2059
 
2055
2060
  ${FENCED_CONTENT_RULE}
2056
2061
  Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
@@ -2079,7 +2084,7 @@ ${REVIEW_VERDICT_INSTRUCTION}
2079
2084
 
2080
2085
  ${NOTEPAD}
2081
2086
 
2082
- Maintain the user-facing status card with the update_status tool: post your plan as a checklist (\u25CB pending), update as items start (\u2731) and finish (\u2713 \u2014 only after they actually happened; never pre-mark reporting steps). Items are short outcomes, never commands.
2087
+ ${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
2083
2088
 
2084
2089
  ${FENCED_CONTENT_RULE}
2085
2090
  Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
@@ -2108,7 +2113,7 @@ ${REVIEW_VERDICT_INSTRUCTION}
2108
2113
 
2109
2114
  ${NOTEPAD}
2110
2115
 
2111
- Maintain the user-facing status card with the update_status tool: post your plan as a checklist (\u25CB pending), update as items start (\u2731) and finish (\u2713 \u2014 only after they actually happened; never pre-mark reporting steps). Items are short outcomes, never commands.
2116
+ ${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
2112
2117
 
2113
2118
  ${FENCED_CONTENT_RULE}
2114
2119
  Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
@@ -2121,7 +2126,7 @@ How to work:
2121
2126
  2. Prefer primary sources; corroborate a surprising claim with a second source.
2122
2127
  3. Answer concisely and cite the URLs (or repo paths) you used. If sources conflict or you couldn't verify something, say so plainly. If web search is unconfigured, use web_fetch / the GitHub tools on what you have and say search was unavailable.
2123
2128
 
2124
- Maintain the user-facing status card with the update_status tool: post a short checklist (\u25CB pending) after you plan, and update items as they start (\u2731) and finish (\u2713 \u2014 only once they actually happened).
2129
+ ${statusCardRule('"Search the sources", "Write the answer"')}
2125
2130
 
2126
2131
  ${FENCED_CONTENT_RULE}
2127
2132
  Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks). Your final message is posted to Slack \u2014 lead with the answer, then supporting detail and sources.`;
@@ -2131,6 +2136,8 @@ Answer directly and concisely. Use Slack-friendly formatting (no markdown header
2131
2136
 
2132
2137
  Your tools work without a workspace: the GitHub tools \u2014 \`github_repos\` (the org repositories you can reach), \`github_tree\` / \`github_file\` / \`github_search_code\` (browse, read, search their code and docs, private repos included), \`github_issue_list\` / \`github_issue_get\` (read issues), \`github_issue_create\` / \`github_issue_update\` / \`github_issue_comment\` / \`github_issue_delete\` (act on issues) \u2014 and \`web_fetch\` (read a public URL). Use them: when the user names a repo loosely ("the switchboard app"), resolve it with github_repos (or the thread) rather than asking; when asked about one of our repos, read it before answering. Report exactly what a tool did (issue number + URL) \u2014 never claim an action you did not perform, and never fabricate file contents, URLs, or command output.
2133
2138
 
2139
+ ${statusCardRule('"Read the issue and its thread", "Post the comment"')} A one-step answer needs no checklist; post one when the request has steps the person would wait on.
2140
+
2134
2141
  You cannot run commands, clone repositories, edit code, or review pull requests, and you cannot search the web. Other Switchboard agents can: for code changes or PRs tell the user to re-send with \`agent:coding\`; for a PR review, \`agent:review\`; for a web-research question, \`agent:research\` (e.g. "\`agent:coding fix the failing login test in acme/api\`", "\`agent:research compare X and Y\`"). Delete an issue only when the user explicitly asked to delete it (closing is an update).`;
2135
2142
  EXPLORE_SYSTEM = `You are Switchboard's explore agent: a long, read-only investigation of a repository, answering a request from Slack.
2136
2143
 
@@ -2144,7 +2151,7 @@ READ-ONLY: NEVER open a pull request, and never commit or push \u2014 no branch,
2144
2151
 
2145
2152
  You cannot attach or post files: your whole answer is text. Never say a file is attached or below \u2014 name its path in the workspace and describe it (what it shows, its size) instead; a person who needs the file itself asks \`agent:coding\`, which can attach.
2146
2153
 
2147
- Maintain the user-facing status card with the update_status tool: post your plan as a checklist (\u25CB pending) once you have it, and update items as they start (\u2731) and finish (\u2713 \u2014 only after they actually happened). Items are short outcomes ("Clone and install", "Time the full suite"), never commands.
2154
+ ${statusCardRule('"Clone and install", "Time the full suite"')}
2148
2155
 
2149
2156
  ${NOTEPAD}
2150
2157
 
@@ -2843,7 +2850,7 @@ function matchDispositions(findings, dispositions) {
2843
2850
  const dropped = [...new Set(dispositions.filter((d) => !issued.has(d.findingId)).map((d) => d.findingId))];
2844
2851
  return { matched, dropped };
2845
2852
  }
2846
- var MIN, SHIP_ROUND_RESERVE_MS, SHIP_LOOP_RESERVE_MS, SHIP_FIX_RESERVE_MS, SHIP_MIN_MAX_MINUTES, PLAN_ID_PATTERN, PLAN_BRANCH, UNIT_REF, SLUG_MAX, INSTANCE_ID_MAX, ADDRESS_SEVERITIES, DEFAULT_ADDRESS_SEVERITY, isAddressSeverity, WAIT_MARGIN_MS, WAIT_CHUNK_MS, MERGE_POLL_MS, MERGE_WAIT_MAX_MS, BUSY_RETRY_MS;
2853
+ var MIN, SHIP_ROUND_RESERVE_MS, SHIP_LOOP_RESERVE_MS, SHIP_FIX_RESERVE_MS, SHIP_MIN_MAX_MINUTES, PLAN_ID_PATTERN, PLAN_BRANCH, UNIT_REF, SLUG_MAX, INSTANCE_ID_MAX, ADDRESS_SEVERITIES, DEFAULT_ADDRESS_SEVERITY, isAddressSeverity, WAIT_MARGIN_MS, WAIT_CHUNK_MS, MERGE_WAIT_MAX_MS, MERGE_WAIT_CHUNK_MS, BUSY_RETRY_MS;
2847
2854
  var init_coordinator = __esm({
2848
2855
  "../../src/core/ship/coordinator.ts"() {
2849
2856
  "use strict";
@@ -2865,8 +2872,8 @@ var init_coordinator = __esm({
2865
2872
  isAddressSeverity = (v) => ADDRESS_SEVERITIES.includes(v);
2866
2873
  WAIT_MARGIN_MS = 5 * MIN;
2867
2874
  WAIT_CHUNK_MS = 5 * MIN;
2868
- MERGE_POLL_MS = 5 * MIN;
2869
2875
  MERGE_WAIT_MAX_MS = 60 * MIN;
2876
+ MERGE_WAIT_CHUNK_MS = 5 * MIN;
2870
2877
  BUSY_RETRY_MS = 2 * MIN;
2871
2878
  }
2872
2879
  });
@@ -2963,6 +2970,22 @@ var init_dashboardAuthConfig = __esm({
2963
2970
  }
2964
2971
  });
2965
2972
 
2973
+ // ../../src/core/harness/roster.ts
2974
+ function isHarnessName(word) {
2975
+ return typeof word === "string" && HARNESS_NAMES.includes(word);
2976
+ }
2977
+ function harnessForPreset(roster, words, preset) {
2978
+ return roster[words?.[preset] ?? DEFAULT_HARNESS];
2979
+ }
2980
+ var HARNESS_NAMES, DEFAULT_HARNESS;
2981
+ var init_roster = __esm({
2982
+ "../../src/core/harness/roster.ts"() {
2983
+ "use strict";
2984
+ HARNESS_NAMES = ["pi", "opencode"];
2985
+ DEFAULT_HARNESS = "pi";
2986
+ }
2987
+ });
2988
+
2966
2989
  // ../../src/tools/web.ts
2967
2990
  import { lookup as dnsLookup } from "node:dns/promises";
2968
2991
  import { Agent, fetch as undiciFetch } from "undici";
@@ -3578,6 +3601,7 @@ function validateConfig(cfg) {
3578
3601
  if (cfg.artifacts !== void 0) validateArtifacts(cfg.artifacts);
3579
3602
  if (cfg.harness !== void 0) validateHarness(cfg.harness);
3580
3603
  if (cfg.pi !== void 0) validatePi(cfg.pi);
3604
+ if (cfg.opencode !== void 0) validateOpenCode(cfg.opencode);
3581
3605
  validateDashboardConfig(cfg.dashboard);
3582
3606
  }
3583
3607
  function validatePi(pi) {
@@ -3597,15 +3621,32 @@ function validatePi(pi) {
3597
3621
  }
3598
3622
  function validateHarness(harness) {
3599
3623
  if (typeof harness !== "object" || harness === null || Array.isArray(harness))
3600
- throw new Error("config.yaml: harness must be a mapping of preset to pi (a retired key; remove it)");
3624
+ throw new Error(`config.yaml: harness must be a mapping of preset to a harness name (${harnessWords(" or ")})`);
3601
3625
  for (const [preset, value] of Object.entries(harness)) {
3602
3626
  if (!Object.hasOwn(AGENTS, preset)) throw new Error(`config.yaml: harness.${preset} is not a known agent`);
3603
- if (value !== "pi")
3627
+ if (!isHarnessName(value))
3604
3628
  throw new Error(
3605
- `config.yaml: harness.${preset} must be pi \u2014 the native loop is deleted and every preset runs on pi; remove the harness block`
3629
+ `config.yaml: harness.${preset}: ${typeof value === "string" ? value : JSON.stringify(value)} is not a harness; the harnesses are ${harnessWords(" and ")}`
3606
3630
  );
3607
3631
  }
3608
3632
  }
3633
+ function validateOpenCode(opencode) {
3634
+ if (typeof opencode !== "object" || opencode === null || Array.isArray(opencode))
3635
+ throw new Error("config.yaml: opencode must be a mapping");
3636
+ for (const key of unknownKeys2(opencode, OPENCODE_KEYS))
3637
+ throw new Error(`config.yaml: opencode.${key} is not a known key`);
3638
+ const { compaction } = opencode;
3639
+ if (compaction === void 0) return;
3640
+ if (typeof compaction !== "object" || compaction === null || Array.isArray(compaction))
3641
+ throw new Error("config.yaml: opencode.compaction must be a mapping");
3642
+ for (const key of unknownKeys2(compaction, OPENCODE_COMPACTION_KEYS))
3643
+ throw new Error(`config.yaml: opencode.compaction.${key} is not a known key`);
3644
+ for (const key of Object.keys(OPENCODE_COMPACTION_KEYS)) {
3645
+ const value = compaction[key];
3646
+ if (value !== void 0 && (typeof value !== "number" || !Number.isInteger(value) || value <= 0))
3647
+ throw new Error(`config.yaml: opencode.compaction.${key} must be a positive integer (tokens)`);
3648
+ }
3649
+ }
3609
3650
  function validateReferences(references) {
3610
3651
  if (typeof references !== "object" || references === null || Array.isArray(references))
3611
3652
  throw new Error("config.yaml: references must be a mapping");
@@ -3729,7 +3770,7 @@ function validateRuntimeOverrides(ro) {
3729
3770
  }
3730
3771
  }
3731
3772
  }
3732
- var MAX_INSTRUCTIONS_LENGTH, CONFIG_KEYS, SELF_IMPROVEMENT_KEYS, BOUNDARY_KEYS, MIN_BOUNDARY_MINUTES, PI_KEYS, PI_COMPACTION_KEYS, ROUTE_ANSWER_MODES, REFERENCES_KEYS, ROUTING_KEYS, SHIP_KEYS, SPAWN_KEYS;
3773
+ var MAX_INSTRUCTIONS_LENGTH, CONFIG_KEYS, SELF_IMPROVEMENT_KEYS, BOUNDARY_KEYS, MIN_BOUNDARY_MINUTES, PI_KEYS, PI_COMPACTION_KEYS, harnessWords, OPENCODE_KEYS, OPENCODE_COMPACTION_KEYS, ROUTE_ANSWER_MODES, REFERENCES_KEYS, ROUTING_KEYS, SHIP_KEYS, SPAWN_KEYS;
3733
3774
  var init_validate = __esm({
3734
3775
  "../../src/config/validate.ts"() {
3735
3776
  "use strict";
@@ -3740,6 +3781,7 @@ var init_validate = __esm({
3740
3781
  init_config();
3741
3782
  init_grants();
3742
3783
  init_registry();
3784
+ init_roster();
3743
3785
  init_web();
3744
3786
  init_registry2();
3745
3787
  MAX_INSTRUCTIONS_LENGTH = 2e3;
@@ -3767,6 +3809,7 @@ var init_validate = __esm({
3767
3809
  references: true,
3768
3810
  harness: true,
3769
3811
  pi: true,
3812
+ opencode: true,
3770
3813
  slack: true,
3771
3814
  runHistory: true,
3772
3815
  runtimeOverrides: true,
@@ -3783,6 +3826,9 @@ var init_validate = __esm({
3783
3826
  MIN_BOUNDARY_MINUTES = 2;
3784
3827
  PI_KEYS = { compaction: true };
3785
3828
  PI_COMPACTION_KEYS = { reserveTokens: true, keepRecentTokens: true };
3829
+ harnessWords = (joiner) => HARNESS_NAMES.join(joiner);
3830
+ OPENCODE_KEYS = { compaction: true };
3831
+ OPENCODE_COMPACTION_KEYS = { buffer: true, keepTokens: true };
3786
3832
  ROUTE_ANSWER_MODES = ["tool", "text"];
3787
3833
  REFERENCES_KEYS = { enabled: true };
3788
3834
  ROUTING_KEYS = { auto: true, model: true, answer: true };
@@ -4284,15 +4330,15 @@ var init_config2 = __esm({
4284
4330
  const us = this.userScope(opts.userId);
4285
4331
  const [agentName, agentLayer] = opts.request.agent !== void 0 ? [opts.request.agent, "request"] : us.agent !== void 0 ? [us.agent, "user"] : ch.agent !== void 0 ? [ch.agent, "channel"] : [this.config.defaults.agent, "default"];
4286
4332
  const boundary = intersectBoundaries(this.boundaryLayers(ch, us));
4287
- const modelRef2 = opts.request.model ?? us.model ?? ch.model ?? us.models?.[agentName] ?? ch.models?.[agentName] ?? this.config.defaults.models[agentName] ?? this.config.defaults.models["general"];
4288
- if (!modelRef2) {
4333
+ const modelRef3 = opts.request.model ?? us.model ?? ch.model ?? us.models?.[agentName] ?? ch.models?.[agentName] ?? this.config.defaults.models[agentName] ?? this.config.defaults.models["general"];
4334
+ if (!modelRef3) {
4289
4335
  throw new Error(`No model configured for agent "${agentName}" \u2014 set defaults.models.${agentName} in config.yaml`);
4290
4336
  }
4291
4337
  const effort2 = opts.request.effort ?? us.effort ?? ch.effort ?? us.efforts?.[agentName] ?? ch.efforts?.[agentName] ?? this.config.defaults.efforts?.[agentName];
4292
4338
  return {
4293
4339
  agentName,
4294
4340
  agentLayer,
4295
- modelRef: modelRef2,
4341
+ modelRef: modelRef3,
4296
4342
  ...effort2 !== void 0 ? { effort: effort2 } : {},
4297
4343
  ...boundary !== void 0 ? { boundary } : {}
4298
4344
  };
@@ -4339,8 +4385,9 @@ var init_config2 = __esm({
4339
4385
  }
4340
4386
  /** The one grants lookup: what `grants[<actorId>]` declares
4341
4387
  * on top of its namespace's baseline (the chat `open` commands and every
4342
- * unrestricted agent for a Slack user, every group's read for a browser
4343
- * session), else that baseline alone, else nothing — unioned with the
4388
+ * unrestricted agent for a Slack user, every group's read and the two
4389
+ * personal chat writes for a browser session), else that baseline alone,
4390
+ * else nothing — unioned with the
4344
4391
  * surface's `<ns>:*` entry when config has one. Attached to every
4345
4392
  * `Caller.actor`: the ONLY thing `authorize` reads about a caller. */
4346
4393
  grantsFor(actorId) {
@@ -12420,7 +12467,7 @@ var init_costs = __esm({
12420
12467
  };
12421
12468
  DATED_RELEASE_SUFFIX = /^-\d{8}$/;
12422
12469
  DEFAULT_DAYS = 30;
12423
- MAX_DAYS = 90;
12470
+ MAX_DAYS = 31;
12424
12471
  NullLlmCostSource = class {
12425
12472
  fetchDailyCost(_range) {
12426
12473
  return Promise.resolve(null);
@@ -13062,6 +13109,9 @@ function unitOfIdempotencyKey(key) {
13062
13109
  const head = step.split("/")[0];
13063
13110
  return UNIT_PATTERN.test(head) ? head : void 0;
13064
13111
  }
13112
+ function checksSettledEventType(headSha) {
13113
+ return `${CHECKS_SETTLED_EVENT_PREFIX}${headSha}`;
13114
+ }
13065
13115
  function coordinatorFields(tag) {
13066
13116
  return tag ? { parentInstanceId: tag.parentInstanceId, idempotencyKey: tag.idempotencyKey } : {};
13067
13117
  }
@@ -13105,7 +13155,19 @@ function isCoordinatorUnit(v) {
13105
13155
  if (r.startedAt !== void 0 && !isFinite(r.startedAt)) return false;
13106
13156
  return true;
13107
13157
  }
13108
- var COORDINATOR_IDENTITY, COORDINATOR_STEP_ACTION, PLAN_MERGE_ACTION, COORDINATOR_STEP_PATH_PREFIX, INSTANCE_ID_PATTERN, STEP_NAME_PATTERN, IDEMPOTENCY_KEY_PATTERN, UNIT_PATTERN, UNIT_KEY_PATTERN, REPO_SLUG2, MAX_TEXT, MAX_REPORT, MAX_ROUNDS, isText, isOptionalText, isFinite, isObject, isPr, isResume, isThread;
13158
+ async function sendChecksSettled(workflow, instance, headSha, settledAt) {
13159
+ if (!workflow) return { kind: "no-binding", instance };
13160
+ const type = checksSettledEventType(headSha);
13161
+ const payload = { headSha, settledAt };
13162
+ try {
13163
+ const handle2 = await workflow.get(instance);
13164
+ await handle2.sendEvent({ type, payload });
13165
+ return { kind: "sent", instance, type };
13166
+ } catch (err2) {
13167
+ return { kind: "failed", instance, type, reason: err2 instanceof Error ? err2.message : String(err2) };
13168
+ }
13169
+ }
13170
+ var COORDINATOR_IDENTITY, COORDINATOR_STEP_ACTION, PLAN_MERGE_ACTION, COORDINATOR_STEP_PATH_PREFIX, INSTANCE_ID_PATTERN, STEP_NAME_PATTERN, IDEMPOTENCY_KEY_PATTERN, UNIT_PATTERN, UNIT_KEY_PATTERN, CHECKS_SETTLED_EVENT_PREFIX, REPO_SLUG2, MAX_TEXT, MAX_REPORT, MAX_ROUNDS, isText, isOptionalText, isFinite, isObject, isPr, isResume, isThread;
13109
13171
  var init_contract2 = __esm({
13110
13172
  "../../src/core/coordinator/contract.ts"() {
13111
13173
  "use strict";
@@ -13118,6 +13180,7 @@ var init_contract2 = __esm({
13118
13180
  IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,99}:[A-Za-z0-9_][A-Za-z0-9_./-]{0,119}$/;
13119
13181
  UNIT_PATTERN = /^[A-Za-z0-9_-]{1,32}$/;
13120
13182
  UNIT_KEY_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,99}:[A-Za-z0-9_-]{1,32}$/;
13183
+ CHECKS_SETTLED_EVENT_PREFIX = "checks-settled-";
13121
13184
  REPO_SLUG2 = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
13122
13185
  MAX_TEXT = 512;
13123
13186
  MAX_REPORT = 2e4;
@@ -16187,13 +16250,22 @@ var init_config4 = __esm({
16187
16250
  options: z6.object({ channel: channelOption }),
16188
16251
  action: "config:read",
16189
16252
  effect: "read",
16190
- describe: "The effective agent/model/effort for you in this channel, the defaults, both scopes, and what is restricted.",
16253
+ describe: "The effective agent/model/effort for you in this channel, the defaults, both scopes, and what is restricted; without a channel (a browser, a token, the CLI), your settings outside any channel.",
16191
16254
  render: (output) => formatConfigDescription(output),
16192
16255
  handler: async ({ options, caller, deps }) => {
16193
- const channel = targetChannel(caller, options.channel);
16256
+ const me = meIdOf(caller) ?? caller.id;
16257
+ const channel = options.channel ?? caller.origin?.channelId;
16258
+ if (!channel) {
16259
+ const description2 = {
16260
+ ...await deps.config.describeConfig(`none:${me}`, me),
16261
+ channel: {},
16262
+ channelConfigRestricted: true
16263
+ };
16264
+ return description2;
16265
+ }
16194
16266
  await assertMayReadChannel(caller, channel, deps);
16195
16267
  const description = {
16196
- ...await deps.config.describeConfig(channel, meIdOf(caller) ?? caller.id),
16268
+ ...await deps.config.describeConfig(channel, me),
16197
16269
  channelConfigRestricted: !mayEditChannel(caller, channel)
16198
16270
  };
16199
16271
  return description;
@@ -20961,7 +21033,7 @@ var init_stores = __esm({
20961
21033
  // ../../src/core/authz/channelDirectory.ts
20962
21034
  function visibilityOf(channelId) {
20963
21035
  if (channelId.startsWith("http:") || channelId.startsWith("mcp:")) return "machine";
20964
- if (channelId.startsWith("slack:D")) return "dm";
21036
+ if (channelId.startsWith("slack:D") || channelId.startsWith("web:")) return "dm";
20965
21037
  if (channelId.startsWith("slack:G")) return "private";
20966
21038
  return "unknown";
20967
21039
  }
@@ -25607,7 +25679,12 @@ var init_actor = __esm({
25607
25679
  http: "http",
25608
25680
  mcp: "mcp",
25609
25681
  cli: "cli",
25610
- schedule: "schedule"
25682
+ schedule: "schedule",
25683
+ // The web chat (record 0043): a browser session speaks as text, so its
25684
+ // `access:<sub>` is a chat credential too — the browser actor, its baseline
25685
+ // and its entry. A service token never reaches the chat (the dashboard gate
25686
+ // serves it `/api/*` only), so `access:svc:` is no chat surface.
25687
+ access: "access-browser"
25611
25688
  };
25612
25689
  }
25613
25690
  });
@@ -26048,19 +26125,19 @@ function stagedFromInbox(v) {
26048
26125
  }
26049
26126
  function messageFromInbox(stored, fallbackAt) {
26050
26127
  const m = stored;
26051
- const str7 = (k) => typeof m[k] === "string" ? m[k] : void 0;
26052
- const text = str7("text");
26053
- const userId = str7("userId");
26054
- const threadKey = str7("threadKey");
26055
- const channelId = str7("channelId");
26128
+ const str8 = (k) => typeof m[k] === "string" ? m[k] : void 0;
26129
+ const text = str8("text");
26130
+ const userId = str8("userId");
26131
+ const threadKey = str8("threadKey");
26132
+ const channelId = str8("channelId");
26056
26133
  if (text === void 0 || userId === void 0 || threadKey === void 0 || channelId === void 0)
26057
26134
  return void 0;
26058
- const userName = str7("userName");
26059
- const authenticatedAs = str7("authenticatedAs");
26060
- const sourceUrl = str7("sourceUrl");
26061
- const channelName = str7("channelName");
26062
- const messageId = str7("messageId");
26063
- const fromRunId = str7("fromRunId");
26135
+ const userName = str8("userName");
26136
+ const authenticatedAs = str8("authenticatedAs");
26137
+ const sourceUrl = str8("sourceUrl");
26138
+ const channelName = str8("channelName");
26139
+ const messageId = str8("messageId");
26140
+ const fromRunId = str8("fromRunId");
26064
26141
  const at = typeof m.at === "number" && Number.isFinite(m.at) ? m.at : fallbackAt;
26065
26142
  const images = attachmentsFromInbox(m.images);
26066
26143
  const documents = attachmentsFromInbox(m.documents);
@@ -29524,18 +29601,18 @@ var init_textTurns = __esm({
29524
29601
  function maxChildrenOf(cfg) {
29525
29602
  return cfg?.maxChildren ?? DEFAULT_MAX_CHILDREN;
29526
29603
  }
29527
- function childRequestText(request) {
29528
- const directives = [`agent:${request.preset}`, ...request.budget !== void 0 ? [`budget:${request.budget}`] : []];
29529
- const branch2 = request.ref !== void 0 ? ` on branch ${request.ref}` : "";
29530
- const repo = request.repo !== void 0 ? ` in ${request.repo}${branch2}:` : "";
29531
- return `${directives.join(" ")}${repo} ${request.prompt}`;
29604
+ function childRequestText(request2) {
29605
+ const directives = [`agent:${request2.preset}`, ...request2.budget !== void 0 ? [`budget:${request2.budget}`] : []];
29606
+ const branch2 = request2.ref !== void 0 ? ` on branch ${request2.ref}` : "";
29607
+ const repo = request2.repo !== void 0 ? ` in ${request2.repo}${branch2}:` : "";
29608
+ return `${directives.join(" ")}${repo} ${request2.prompt}`;
29532
29609
  }
29533
- function childThreadLead(parent, request) {
29610
+ function childThreadLead(parent, request2) {
29534
29611
  const who2 = parent.msg.userName ?? parent.msg.userId;
29535
29612
  const from = parent.msg.sourceUrl !== void 0 ? `[the *${parent.agentName}* run](${parent.msg.sourceUrl})` : `the *${parent.agentName}* run`;
29536
- const line3 = (request.prompt.split("\n")[0] ?? "").trim();
29613
+ const line3 = (request2.prompt.split("\n")[0] ?? "").trim();
29537
29614
  const snippet = line3.length > 140 ? `${line3.slice(0, 137)}\u2026` : line3;
29538
- return `\u21B3 *${request.preset}* run for ${who2}, spawned by ${from}: ${snippet}`;
29615
+ return `\u21B3 *${request2.preset}* run for ${who2}, spawned by ${from}: ${snippet}`;
29539
29616
  }
29540
29617
  function platformOf(channelId) {
29541
29618
  const colon = channelId.indexOf(":");
@@ -29561,7 +29638,7 @@ function watchedChild(io, on) {
29561
29638
  if (io.openThread) watched2.openThread = (lead) => io.openThread(lead);
29562
29639
  return watched2;
29563
29640
  }
29564
- async function spawnChild(deps, parent, request) {
29641
+ async function spawnChild(deps, parent, request2) {
29565
29642
  const refused6 = (reason3, message4) => ({ kind: "refused", reason: reason3, message: message4 });
29566
29643
  if (parent.depth >= MAX_SPAWN_DEPTH) {
29567
29644
  return refused6(
@@ -29569,10 +29646,10 @@ async function spawnChild(deps, parent, request) {
29569
29646
  `a child run cannot spawn: this run is itself a child, and a tree is ${MAX_SPAWN_DEPTH} level deep \u2014 the run that started it is the one to ask`
29570
29647
  );
29571
29648
  }
29572
- if (AGENTS[request.preset]?.identity === "write") {
29649
+ if (AGENTS[request2.preset]?.identity === "write") {
29573
29650
  return refused6(
29574
29651
  "spawn_identity",
29575
- `\`${request.preset}\` runs as a \`write\` identity \u2014 it pushes branches and opens pull requests \u2014 and a spawned child never writes: it reads this conversation and reports; the person who asked starts that work by hand with \`agent:${request.preset}\``
29652
+ `\`${request2.preset}\` runs as a \`write\` identity \u2014 it pushes branches and opens pull requests \u2014 and a spawned child never writes: it reads this conversation and reports; the person who asked starts that work by hand with \`agent:${request2.preset}\``
29576
29653
  );
29577
29654
  }
29578
29655
  const minutesLeft = Math.floor(parent.remainingMs / 6e4);
@@ -29598,7 +29675,7 @@ async function spawnChild(deps, parent, request) {
29598
29675
  }
29599
29676
  let opened;
29600
29677
  try {
29601
- opened = await parent.io.openThread(childThreadLead(parent, request));
29678
+ opened = await parent.io.openThread(childThreadLead(parent, request2));
29602
29679
  } catch (err2) {
29603
29680
  return refused6("spawn_failed", `the channel could not open the child's thread: ${describe3(err2)}`);
29604
29681
  }
@@ -29610,7 +29687,7 @@ async function spawnChild(deps, parent, request) {
29610
29687
  ...parent.msg.channelName !== void 0 ? { channelName: parent.msg.channelName } : {},
29611
29688
  threadKey: opened.thread.threadKey,
29612
29689
  ...opened.thread.sourceUrl !== void 0 ? { sourceUrl: opened.thread.sourceUrl } : {},
29613
- text: childRequestText(request),
29690
+ text: childRequestText(request2),
29614
29691
  receivedAt: deps.clock()
29615
29692
  };
29616
29693
  let startedId;
@@ -29647,7 +29724,7 @@ async function spawnChild(deps, parent, request) {
29647
29724
  if (first.kind === "started" || startedId !== void 0) {
29648
29725
  const runId3 = first.kind === "started" ? first.id : startedId;
29649
29726
  console.log(
29650
- `[spawn] ${parent.msg.threadKey} run ${parent.runId} spawned ${request.preset} run ${runId3} in ${child.threadKey}`
29727
+ `[spawn] ${parent.msg.threadKey} run ${parent.runId} spawned ${request2.preset} run ${runId3} in ${child.threadKey}`
29651
29728
  );
29652
29729
  return {
29653
29730
  kind: "spawned",
@@ -29658,7 +29735,7 @@ async function spawnChild(deps, parent, request) {
29658
29735
  }
29659
29736
  if (first.kind === "threw") return refused6("spawn_failed", describe3(first.err));
29660
29737
  const reason2 = first.outcome.refusal ?? "spawn_failed";
29661
- console.log(`[spawn] ${parent.msg.threadKey} run ${parent.runId}: ${request.preset} child not started (${reason2})`);
29738
+ console.log(`[spawn] ${parent.msg.threadKey} run ${parent.runId}: ${request2.preset} child not started (${reason2})`);
29662
29739
  return refused6(reason2, lastReply ?? `the child ended (${first.outcome.status}) before it started`);
29663
29740
  }
29664
29741
  function spawnCapabilityFor(deps, parent) {
@@ -29672,8 +29749,8 @@ function spawnCapabilityFor(deps, parent) {
29672
29749
  };
29673
29750
  let previous = Promise.resolve();
29674
29751
  return {
29675
- spawn: (request, at) => {
29676
- const turn = previous.then(() => spawnChild(withHook, { ...parent, ...at }, request));
29752
+ spawn: (request2, at) => {
29753
+ const turn = previous.then(() => spawnChild(withHook, { ...parent, ...at }, request2));
29677
29754
  previous = turn.catch(() => {
29678
29755
  });
29679
29756
  return turn;
@@ -29898,6 +29975,14 @@ async function nextTick(wait, ids, ms, signal2) {
29898
29975
  signal2?.removeEventListener("abort", end);
29899
29976
  }
29900
29977
  }
29978
+ function endsAwaited(watch) {
29979
+ return new Set(
29980
+ watch.pending().map((id) => {
29981
+ const state = watch.get(id);
29982
+ return state?.kind === "running" && state.continuedBy !== void 0 ? state.continuedBy : id;
29983
+ })
29984
+ );
29985
+ }
29901
29986
  var UNAVAILABLE2, NOT_FOUND, CHILD_PRESETS, WRITE_PRESETS, REPO_PRESETS, CHILDREN_PAGES_MAX, spawnRunTool, sendToRunTool, AWAIT_IDS_MAX, awaitRunsTool, listRunsTool, getRunStatusTool, RUN_TOOLS;
29902
29987
  var init_runs2 = __esm({
29903
29988
  "../../src/tools/runs.ts"() {
@@ -29943,17 +30028,17 @@ var init_runs2 = __esm({
29943
30028
  return `error: unknown preset "${preset}" \u2014 one of ${CHILD_PRESETS().join(", ")}`;
29944
30029
  const prompt = String(input.prompt ?? "").trim();
29945
30030
  if (!prompt) return "error: prompt is required \u2014 what the child should do, after this conversation's text";
29946
- const request = { preset, prompt };
29947
- if (input.repo !== void 0 && String(input.repo).trim()) request.repo = String(input.repo).trim();
30031
+ const request2 = { preset, prompt };
30032
+ if (input.repo !== void 0 && String(input.repo).trim()) request2.repo = String(input.repo).trim();
29948
30033
  if (input.budget !== void 0) {
29949
30034
  const budget = input.budget;
29950
30035
  if (typeof budget !== "number" || !Number.isInteger(budget) || budget < MIN_BOUNDARY_MINUTES)
29951
30036
  return `error: budget takes a whole number of minutes, at least ${MIN_BOUNDARY_MINUTES}`;
29952
- request.budget = budget;
30037
+ request2.budget = budget;
29953
30038
  }
29954
30039
  const spawn7 = ctx.spawn ?? nullSpawnCapability;
29955
30040
  const conversation = ctx.conversation ? await ctx.conversation() : void 0;
29956
- const out = await spawn7.spawn(request, {
30041
+ const out = await spawn7.spawn(request2, {
29957
30042
  remainingMs: ctx.remainingMs?.() ?? Number.POSITIVE_INFINITY,
29958
30043
  ...conversation ? { conversation } : {}
29959
30044
  });
@@ -30063,7 +30148,7 @@ var init_runs2 = __esm({
30063
30148
  });
30064
30149
  }
30065
30150
  const ms = Math.min(AWAIT_POLL_MS, Math.max(0, decision.until - now));
30066
- await nextTick(wait, new Set(watch.pending()), ms, ctx.signal);
30151
+ await nextTick(wait, endsAwaited(watch), ms, ctx.signal);
30067
30152
  }
30068
30153
  }
30069
30154
  };
@@ -30593,13 +30678,14 @@ ${skill.body}${footer}`;
30593
30678
  });
30594
30679
 
30595
30680
  // ../../src/tools/status.ts
30596
- var updateStatusTool;
30681
+ var STATUS_RESULT, updateStatusTool;
30597
30682
  var init_status2 = __esm({
30598
30683
  "../../src/tools/status.ts"() {
30599
30684
  "use strict";
30685
+ STATUS_RESULT = "status updated \u2014 keep it truthful: \u2713 only for items whose result you have already read, \u2731 on the item whose command runs next.";
30600
30686
  updateStatusTool = {
30601
30687
  name: "update_status",
30602
- description: "Update the short user-facing status checklist shown while you work. Call it right after planning (all items pending) and again whenever an item's state changes. Format: one item per line, prefixed with a state marker: \u2713 done, \u2731 in progress, \u25CB pending. Keep it to 3-6 short outcome-oriented items (what, not how \u2014 never raw commands).",
30688
+ description: "Update the short user-facing status checklist shown while you work. Call it right after planning (all items pending) and again whenever an item's state changes. Format: one item per line, prefixed with a state marker: \u2713 done, \u2731 in progress, \u25CB pending. The markers are facts, not intentions: mark an item \u2731 when you issue the first command that does it, and \u2713 only after you have read its result \u2014 never in the same turn as the command, never because you plan to run it next. The card shows the command running right now beside this list, so the two must agree. Keep it to 3-6 short outcome-oriented items (what, not how \u2014 never raw commands).",
30603
30689
  inputSchema: {
30604
30690
  type: "object",
30605
30691
  properties: {
@@ -30612,7 +30698,7 @@ var init_status2 = __esm({
30612
30698
  },
30613
30699
  async run(input, ctx) {
30614
30700
  ctx.reportProgress?.(String(input.checklist ?? ""));
30615
- return "status updated";
30701
+ return STATUS_RESULT;
30616
30702
  }
30617
30703
  };
30618
30704
  }
@@ -31185,7 +31271,7 @@ var init_submit = __esm({
31185
31271
  return "no run is recording a handoff here \u2014 it was not recorded (it applies to a coding run started for a plan unit)";
31186
31272
  ctx.onHandoff(parsed.handoff);
31187
31273
  const h = parsed.handoff;
31188
- const count = (n2, one, many) => `${n2} ${n2 === 1 ? one : many}`;
31274
+ const count = (n2, one2, many) => `${n2} ${n2 === 1 ? one2 : many}`;
31189
31275
  return `handoff recorded: ${count(h.deviations.length, "deviation", "deviations")}, ${count(h.followUps.length, "follow-up", "follow-ups")}, ${h.unproven.length} unproven; a later call replaces this one`;
31190
31276
  }
31191
31277
  };
@@ -31690,8 +31776,8 @@ async function routeRequest(deps, ctx) {
31690
31776
  return { kind: "routed", resolved: resolveRouted(deps, msg, directives, sticky, carried.preset), route: carried };
31691
31777
  }
31692
31778
  if (!routingOn(cfg) || agentSource !== "default" || threadLive) return { kind: "unrouted" };
31693
- const modelRef2 = cfg.routing?.model ?? cfg.defaults.models["general"];
31694
- if (!modelRef2) {
31779
+ const modelRef3 = cfg.routing?.model ?? cfg.defaults.models["general"];
31780
+ if (!modelRef3) {
31695
31781
  console.log(`[route] ${msg.threadKey} not routed: no routing.model and no defaults.models.general`);
31696
31782
  return { kind: "unrouted" };
31697
31783
  }
@@ -31699,7 +31785,7 @@ async function routeRequest(deps, ctx) {
31699
31785
  try {
31700
31786
  if (deps.routeModel) model = deps.routeModel;
31701
31787
  else {
31702
- const ref = parseModelRef(modelRef2);
31788
+ const ref = parseModelRef(modelRef3);
31703
31789
  model = providerRouteModel(deps.completions.get(ref.provider), ref.model, {
31704
31790
  ...cfg.routing?.answer ? { answer: cfg.routing.answer } : {}
31705
31791
  });
@@ -31741,15 +31827,15 @@ async function routeRequest(deps, ctx) {
31741
31827
  )
31742
31828
  );
31743
31829
  if (decision.preset === void 0 && decision.command && ctx.command && deps.commands)
31744
- return answerCommand(deps.commands, ctx.command, msg, decision.command, modelRef2, root);
31830
+ return answerCommand(deps.commands, ctx.command, msg, decision.command, modelRef3, root);
31745
31831
  if (decision.preset === void 0) {
31746
31832
  console.log(`[route] ${msg.threadKey} not routed (${decision.reason}) \u2014 running ${cfg.defaults.agent}`);
31747
- return decision.compoundRejected ? { kind: "unrouted", rejected: { preset: cfg.defaults.agent, reason: decision.reason, model: modelRef2 } } : { kind: "unrouted" };
31833
+ return decision.compoundRejected ? { kind: "unrouted", rejected: { preset: cfg.defaults.agent, reason: decision.reason, model: modelRef3 } } : { kind: "unrouted" };
31748
31834
  }
31749
31835
  const resolved = resolveRouted(deps, msg, directives, sticky, decision.preset);
31750
31836
  const { parts, collapsed } = decision;
31751
31837
  console.log(
31752
- `[route] ${msg.threadKey} routed to ${decision.preset} on ${modelRef2}${parts ? ` (${parts.length} parts)` : ""}${collapsed ? ` (compound collapsed: ${collapsed.presets.join("+")})` : ""}: ${decision.reason}`
31838
+ `[route] ${msg.threadKey} routed to ${decision.preset} on ${modelRef3}${parts ? ` (${parts.length} parts)` : ""}${collapsed ? ` (compound collapsed: ${collapsed.presets.join("+")})` : ""}: ${decision.reason}`
31753
31839
  );
31754
31840
  return {
31755
31841
  kind: "routed",
@@ -31757,13 +31843,13 @@ async function routeRequest(deps, ctx) {
31757
31843
  route: {
31758
31844
  preset: decision.preset,
31759
31845
  reason: decision.reason,
31760
- model: modelRef2,
31846
+ model: modelRef3,
31761
31847
  ...parts ? { parts } : {},
31762
31848
  ...collapsed ? { collapsed } : {}
31763
31849
  }
31764
31850
  };
31765
31851
  }
31766
- async function answerCommand(commands, branch2, msg, decided, modelRef2, root) {
31852
+ async function answerCommand(commands, branch2, msg, decided, modelRef3, root) {
31767
31853
  const { deps, io, ending, trace } = branch2;
31768
31854
  const def = commands.get(decided.id);
31769
31855
  if (!def) {
@@ -31773,7 +31859,7 @@ async function answerCommand(commands, branch2, msg, decided, modelRef2, root) {
31773
31859
  const receipt = redactAndCap(chatInvocation(def, decided.input), ROUTE_RECEIPT_CAP);
31774
31860
  const receiptLine = `${ROUTED_RECEIPT_PREFIX} ${receipt}`;
31775
31861
  if (def.effect === "write") {
31776
- console.log(`[route] ${msg.threadKey} handed back ${def.id} on ${modelRef2}: ${receipt}`);
31862
+ console.log(`[route] ${msg.threadKey} handed back ${def.id} on ${modelRef3}: ${receipt}`);
31777
31863
  await ending.sealAfterReply(
31778
31864
  async () => {
31779
31865
  },
@@ -31781,11 +31867,11 @@ async function answerCommand(commands, branch2, msg, decided, modelRef2, root) {
31781
31867
  );
31782
31868
  return { kind: "command", command: def.id, outcome: "hand_back" };
31783
31869
  }
31784
- console.log(`[route] ${msg.threadKey} routed to command ${def.id} on ${modelRef2}: ${receipt}`);
31870
+ console.log(`[route] ${msg.threadKey} routed to command ${def.id} on ${modelRef3}: ${receipt}`);
31785
31871
  const route3 = {
31786
31872
  preset: COMMAND_RUN_AGENT,
31787
31873
  reason: `command ${def.id}`,
31788
- model: modelRef2,
31874
+ model: modelRef3,
31789
31875
  command: def.id,
31790
31876
  input: redactedInput(decided.input),
31791
31877
  receipt
@@ -31814,13 +31900,13 @@ function redactedInput(input) {
31814
31900
  return v === void 0 ? null : text(JSON.stringify(v) ?? String(v));
31815
31901
  };
31816
31902
  const leafOrList = (v) => Array.isArray(v) ? v.map(leaf) : leaf(v);
31817
- const isRecord7 = (v) => typeof v === "object" && v !== null && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
31903
+ const isRecord9 = (v) => typeof v === "object" && v !== null && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
31818
31904
  const entries = (v, of) => Object.fromEntries(
31819
31905
  Object.entries(v).filter(([, x]) => x !== void 0).map(([k, x]) => [k, of(x)])
31820
31906
  );
31821
31907
  const object1 = (v) => entries(v, leafOrList);
31822
- const object2 = (v) => entries(v, (x) => isRecord7(x) ? object1(x) : leafOrList(x));
31823
- const object3 = (v) => entries(v, (x) => isRecord7(x) ? object2(x) : leafOrList(x));
31908
+ const object2 = (v) => entries(v, (x) => isRecord9(x) ? object1(x) : leafOrList(x));
31909
+ const object3 = (v) => entries(v, (x) => isRecord9(x) ? object2(x) : leafOrList(x));
31824
31910
  return {
31825
31911
  ...input.args ? { args: input.args.map(leaf) } : {},
31826
31912
  ...input.options ? { options: object3(input.options) } : {}
@@ -33443,10 +33529,12 @@ async function registerRun(deps, ctx) {
33443
33529
  agentSource,
33444
33530
  model: resolved.modelRef,
33445
33531
  traceId: root.traceId,
33446
- // The harness the run is driven by (harness.md item 8): the object's own
33447
- // name, so a record can be told from another harness's; none in a
33448
- // process that starts no run.
33449
- ...deps.harness ? { harness: deps.harness.harness.name } : {},
33532
+ // The harness the run is driven by (harness.md items 8 and 10): the
33533
+ // object the preset's configuration word picks off the roster, by its
33534
+ // own name, so a record can be told from another harness's; none in a
33535
+ // process that starts no run. A resumed run publishes no meta (above),
33536
+ // and its harness is its row's, not the word's.
33537
+ ...deps.harness ? { harness: harnessForPreset(deps.harness.harnesses, deps.config.config.harness, agent.name).name } : {},
33450
33538
  ...resolved.effort !== void 0 ? { effort: resolved.effort } : {},
33451
33539
  ...repoCtx2.repo !== void 0 ? { repo: repoCtx2.repo } : {},
33452
33540
  ...repoCtx2.ref !== void 0 ? { ref: repoCtx2.ref } : {},
@@ -33766,6 +33854,7 @@ var init_provision = __esm({
33766
33854
  init_reply();
33767
33855
  init_messages();
33768
33856
  init_route();
33857
+ init_roster();
33769
33858
  STATUS_UPDATE_MIN_MS = 3e3;
33770
33859
  }
33771
33860
  });
@@ -35788,6 +35877,12 @@ function reachFor(identity2) {
35788
35877
  if (identity2 === "read") return READ_REACH;
35789
35878
  return NONE_REACH;
35790
35879
  }
35880
+ function openCodeToolWord(action) {
35881
+ const mapped = OPENCODE_ACTION_TO_TOOL_WORD[action];
35882
+ if (mapped !== void 0) return mapped;
35883
+ const underscore = action.indexOf("_");
35884
+ return underscore > 0 ? action.slice(underscore + 1) : action;
35885
+ }
35791
35886
  function judgeToolCall(tool, input, ctx) {
35792
35887
  const bundle = PI_TOOL_BUNDLES[tool];
35793
35888
  if (bundle === void 0) return outside(`${tool} is not in any bundle the ${ctx.identity} identity reaches`);
@@ -35851,7 +35946,7 @@ function judgePath(path, ctx) {
35851
35946
  if (rel.startsWith("..") || isAbsolute5(rel)) return refused3(`path \u2014 \`${path}\` resolves outside the checkout`);
35852
35947
  return allowed;
35853
35948
  }
35854
- var PI_TOOL_BUNDLES, CODING_REACH, READ_REACH, NONE_REACH, allowed, refused3, outside, isRecord6, CREDENTIAL_FILE, ENV_DUMP, CREDENTIAL_VAR, MERGE_OR_APPROVE, GIT_THEN_PUSH, GIT_PUSH, ANY_PUSH, GH_WRITE_VERB, WRITE_METHOD, GH_API_WRITE, CURL_GITHUB_WRITE;
35949
+ var PI_TOOL_BUNDLES, CODING_REACH, READ_REACH, NONE_REACH, allowed, refused3, outside, isRecord6, CREDENTIAL_FILE, ENV_DUMP, CREDENTIAL_VAR, MERGE_OR_APPROVE, GIT_THEN_PUSH, GIT_PUSH, ANY_PUSH, GH_WRITE_VERB, WRITE_METHOD, GH_API_WRITE, CURL_GITHUB_WRITE, OPENCODE_ACTION_TO_TOOL_WORD;
35855
35950
  var init_toolRules = __esm({
35856
35951
  "../../src/core/harness/pi/toolRules.ts"() {
35857
35952
  "use strict";
@@ -35896,6 +35991,19 @@ var init_toolRules = __esm({
35896
35991
  CURL_GITHUB_WRITE = new RegExp(
35897
35992
  `\\bcurl\\b(?=[^;&|]*api\\.github\\.com)(?=[^;&|]*(?:\\s(?:-X|--request)[\\s=]*${WRITE_METHOD}\\b|\\s(?:-d|--data(?:-\\w+)?|--json|-T|--upload-file)[\\s=]))`
35898
35993
  );
35994
+ OPENCODE_ACTION_TO_TOOL_WORD = {
35995
+ shell: "bash",
35996
+ edit: "edit",
35997
+ // OpenCode's `write` and `patch` tools both assert the `edit` action (their
35998
+ // `permission: "edit"`), so `write` is never an action here; a call to the
35999
+ // write tool arrives as `edit`.
36000
+ patch: "edit",
36001
+ read: "read",
36002
+ glob: "find",
36003
+ grep: "grep",
36004
+ webfetch: "web_fetch",
36005
+ websearch: "web_search"
36006
+ };
35899
36007
  }
35900
36008
  });
35901
36009
 
@@ -36667,7 +36775,7 @@ async function runPiHarnessOpen(deps, run2) {
36667
36775
  requeueUnechoed = () => {
36668
36776
  run2.inbox?.requeue(unechoed.splice(0).flatMap((u) => u.inputs));
36669
36777
  };
36670
- const drainFollowUps = () => {
36778
+ const drainFollowUps2 = () => {
36671
36779
  const inputs = run2.inbox?.drain() ?? [];
36672
36780
  if (inputs.length === 0) return;
36673
36781
  const images = inputs.flatMap(
@@ -36749,7 +36857,7 @@ ${stagedLine}` : prompt;
36749
36857
  note("wrap_up", wrapUpNote(minutesLeft));
36750
36858
  transport.send({ type: "steer", message: wrapUpInstruction(minutesLeft) });
36751
36859
  }
36752
- drainFollowUps();
36860
+ drainFollowUps2();
36753
36861
  };
36754
36862
  const iterator = transport.lines[Symbol.asyncIterator]();
36755
36863
  let pending2;
@@ -38118,6 +38226,16 @@ async function runLoop(deps, ctx) {
38118
38226
  const postedBefore = resume ? reviewPostedBefore(resume.events) : void 0;
38119
38227
  if (postedBefore) reviewHead = postedBefore.head;
38120
38228
  const facts2 = resume ? harnessFactsOf(resume.row.state.harness) : void 0;
38229
+ const harnessOf = (roster) => facts2 ? roster.harnesses[facts2.harness] : harnessForPreset(roster.harnesses, deps.config.config.harness, agent.name);
38230
+ const unknownWord = facts2 === void 0 ? unknownHarnessWordOf(resume?.row.state.harness) : void 0;
38231
+ const noteUnknownWord = (ending2) => {
38232
+ if (unknownWord === void 0) return;
38233
+ onEvent({
38234
+ type: "run_note",
38235
+ kind: "resumed",
38236
+ summary: `the row names the ${unknownWord.word} harness, which this build does not know; its process (pid ${unknownWord.pid} in container ${unknownWord.container}) was neither judged nor ended here; ${ending2}`
38237
+ });
38238
+ };
38121
38239
  let lastFacts = facts2;
38122
38240
  const saveFacts = (h) => {
38123
38241
  lastFacts = h;
@@ -38134,10 +38252,11 @@ async function runLoop(deps, ctx) {
38134
38252
  if (facts2) {
38135
38253
  if (!deps.harness)
38136
38254
  throw new Error(
38137
- `the ${agent.name} preset's run left ${facts2.harness} harness facts on its row, but this process has no harness deps to end that process with`
38255
+ `the ${agent.name} preset's run left ${facts2.harness} harness facts on its row, but this process has no harness roster to end that process with`
38138
38256
  );
38257
+ const judge3 = harnessOf(deps.harness);
38139
38258
  const container = deps.harness.containerFor?.(executor, profile.machine) ?? harnessContainerFor(executor, profile.machine);
38140
- const found = await deps.harness.harness.find(facts2, container).catch((err2) => {
38259
+ const found = await judge3.find(facts2, container).catch((err2) => {
38141
38260
  if (!isContainerGone(err2)) throw err2;
38142
38261
  return "gone";
38143
38262
  });
@@ -38162,29 +38281,32 @@ async function runLoop(deps, ctx) {
38162
38281
  onEvent({
38163
38282
  type: "run_note",
38164
38283
  kind: "resumed",
38165
- summary: `the run's row carries ${facts2.harness} harness facts (pid ${facts2.pid}), and this run is driven by ${deps.harness.harness.name}: that process was neither judged nor ended here`
38284
+ summary: `the run's row carries ${facts2.harness} harness facts (pid ${facts2.pid}) that the ${judge3.name} harness does not own: that process was neither judged nor ended here`
38166
38285
  });
38167
38286
  break;
38168
38287
  default:
38169
- await deps.harness.harness.end(facts2, container);
38288
+ await judge3.end(facts2, container);
38170
38289
  }
38171
- }
38290
+ } else noteUnknownWord("the run finished on the answer it already had");
38172
38291
  answer = answerUnderEnding(finish2.answer, loopEnding, agent.maxMinutes);
38173
38292
  } else {
38174
38293
  if (!deps.harness)
38175
- throw new Error(`the ${agent.name} preset runs on the pi harness, but this process has no harness deps`);
38294
+ throw new Error(`the ${agent.name} preset runs on a harness, but this process has no harness roster`);
38176
38295
  const harnessDeps = deps.harness;
38296
+ const harness = harnessOf(harnessDeps);
38297
+ noteUnknownWord(`the run was rebuilt on ${harness.name}`);
38177
38298
  const onBotHost = profile.machine === "none";
38178
38299
  const harnessUrl = onBotHost ? deps.harness.loopbackUrl : deps.harness.harnessUrl;
38179
38300
  if (!harnessUrl)
38180
38301
  throw new Error(
38181
- onBotHost ? "the pi harness needs PORT: a run without a workspace runs pi on the bot host, which reaches the model proxy over loopback" : "the pi harness needs PUBLIC_BASE_URL: the run's container reaches the model proxy through it"
38302
+ onBotHost ? `the ${harness.name} harness needs PORT: a run without a workspace runs its process on the bot host, which reaches the model proxy over loopback` : `the ${harness.name} harness needs PUBLIC_BASE_URL: the run's container reaches the model proxy through it`
38182
38303
  );
38183
38304
  if (ctx.bearer === void 0)
38184
- throw new Error("the pi harness needs the run's model-proxy bearer, and this process minted none");
38305
+ throw new Error(`the ${harness.name} harness needs the run's model-proxy bearer, and this process minted none`);
38185
38306
  const { provider: providerName, model: modelId } = parseModelRef(resolved.modelRef);
38186
38307
  const providerCfg = deps.config.config.providers[providerName];
38187
- if (!providerCfg) throw new Error(`the pi harness found no provider named ${providerName} in the config`);
38308
+ if (!providerCfg)
38309
+ throw new Error(`the ${harness.name} harness found no provider named ${providerName} in the config`);
38188
38310
  const prBase = repoCtx.baseRef ?? await coordinatorBase;
38189
38311
  const ownBranch = prBase !== void 0 ? binding?.ref ?? repoCtx.ref : void 0;
38190
38312
  const protectedBranches = [
@@ -38205,7 +38327,7 @@ async function runLoop(deps, ctx) {
38205
38327
  ...facts2 ? { facts: facts2 } : {}
38206
38328
  } : void 0;
38207
38329
  const openRun = () => openThroughSeam(
38208
- harnessDeps.harness,
38330
+ harness,
38209
38331
  {
38210
38332
  container: harnessDeps.containerFor?.(executor, profile.machine) ?? harnessContainerFor(executor, profile.machine),
38211
38333
  bearer,
@@ -38260,7 +38382,7 @@ async function runLoop(deps, ctx) {
38260
38382
  repoCtx,
38261
38383
  root,
38262
38384
  clock: clock4,
38263
- harness: harnessDeps.harness,
38385
+ harness,
38264
38386
  replaced: err2,
38265
38387
  facts: lastFacts,
38266
38388
  binding: workspaceBinding,
@@ -38589,6 +38711,16 @@ function answerUnderEnding(text, ending, maxMinutes) {
38589
38711
  ${text}` : `\u26A0\uFE0F ${ending.summary}`;
38590
38712
  }
38591
38713
  }
38714
+ function unknownHarnessWordOf(state) {
38715
+ if (typeof state !== "object" || state === null) return void 0;
38716
+ const { harness, pid, container } = state;
38717
+ if (typeof harness !== "string") return void 0;
38718
+ return {
38719
+ word: harness,
38720
+ pid: typeof pid === "number" ? String(pid) : "unknown",
38721
+ container: typeof container === "string" ? container : "unknown"
38722
+ };
38723
+ }
38592
38724
  var init_runLoop = __esm({
38593
38725
  "../../src/core/dispatch/runLoop.ts"() {
38594
38726
  "use strict";
@@ -38598,6 +38730,7 @@ var init_runLoop = __esm({
38598
38730
  init_toolsets();
38599
38731
  init_contract4();
38600
38732
  init_relaunch();
38733
+ init_roster();
38601
38734
  init_botHostContainer();
38602
38735
  init_factory();
38603
38736
  init_container();
@@ -38799,8 +38932,8 @@ async function plan(deps, input) {
38799
38932
  label: input.label
38800
38933
  };
38801
38934
  const taskText = shipTaskText(input.requestText, entry.repo);
38802
- const request = parseShipPlanRequest(taskText);
38803
- if (request === void 0) {
38935
+ const request2 = parseShipPlanRequest(taskText);
38936
+ if (request2 === void 0) {
38804
38937
  const text2 = taskText || "Implement the task this thread's ship request describes.";
38805
38938
  const planId2 = generatedPlanId(text2, msg.threadKey);
38806
38939
  const graph2 = {
@@ -38826,38 +38959,38 @@ async function plan(deps, input) {
38826
38959
  if (input.agentSource === "route")
38827
38960
  return {
38828
38961
  ok: false,
38829
- reply: `\u{1F6AB} A routed request never runs a seeded plan \u2014 its units would merge under the runner's grant. Type \`agent:ship plan ${request.planPath}\` to run it.`
38962
+ reply: `\u{1F6AB} A routed request never runs a seeded plan \u2014 its units would merge under the runner's grant. Type \`agent:ship plan ${request2.planPath}\` to run it.`
38830
38963
  };
38831
38964
  let planId;
38832
38965
  try {
38833
- planId = planIdOf(request.planPath);
38966
+ planId = planIdOf(request2.planPath);
38834
38967
  } catch (err2) {
38835
38968
  return { ok: false, reply: `\u{1F6AB} ${describe6(err2)}` };
38836
38969
  }
38837
38970
  let text;
38838
38971
  try {
38839
- text = (await deps.readFile(entry.repo, request.planPath, base)).content;
38972
+ text = (await deps.readFile(entry.repo, request2.planPath, base)).content;
38840
38973
  } catch (err2) {
38841
38974
  return {
38842
38975
  ok: false,
38843
- reply: `\u{1F6AB} The plan \`${request.planPath}\` could not be read at \`${base}\` in ${entry.repo}: ${describe6(err2)}`
38976
+ reply: `\u{1F6AB} The plan \`${request2.planPath}\` could not be read at \`${base}\` in ${entry.repo}: ${describe6(err2)}`
38844
38977
  };
38845
38978
  }
38846
38979
  const graph = parsePlanGraph(text, planId);
38847
38980
  if (graph.units.length === 0)
38848
38981
  return {
38849
38982
  ok: false,
38850
- reply: `\u{1F6AB} The plan \`${request.planPath}\` has no unit headings (\`### U<n>. <title>\`) \u2014 nothing to run.`
38983
+ reply: `\u{1F6AB} The plan \`${request2.planPath}\` has no unit headings (\`### U<n>. <title>\`) \u2014 nothing to run.`
38851
38984
  };
38852
38985
  let selected;
38853
38986
  try {
38854
- selected = openPlanCursor(graph, request.units).order;
38987
+ selected = openPlanCursor(graph, request2.units).order;
38855
38988
  } catch (err2) {
38856
38989
  return { ok: false, reply: `\u{1F6AB} ${describe6(err2)}` };
38857
38990
  }
38858
38991
  return {
38859
38992
  ok: true,
38860
- planned: { planId, path: request.planPath, base, graph, selected, identity: identity2, merge: "runner" }
38993
+ planned: { planId, path: request2.planPath, base, graph, selected, identity: identity2, merge: "runner" }
38861
38994
  };
38862
38995
  }
38863
38996
  function rowsFor(p, selected, instanceId, lastPushOf) {
@@ -39046,6 +39179,19 @@ function readCreateInstanceAnswer(status3, text) {
39046
39179
  const detail = typeof body?.error === "string" ? body.error : text.slice(0, 200);
39047
39180
  return { kind: "unanswered", reason: `HTTP ${status3} \u2014 ${detail}` };
39048
39181
  }
39182
+ function readSendEventAnswer(status3, text) {
39183
+ let parsed;
39184
+ try {
39185
+ parsed = JSON.parse(text);
39186
+ } catch {
39187
+ parsed = void 0;
39188
+ }
39189
+ const body = typeof parsed === "object" && parsed !== null ? parsed : void 0;
39190
+ if (status3 === 200 && body?.ok === true && body.sent === true) return { kind: "sent" };
39191
+ if (status3 === 404 && body?.error === "no_instance") return { kind: "absent" };
39192
+ const detail = typeof body?.error === "string" ? body.error : text.slice(0, 200);
39193
+ return { kind: "unanswered", reason: `HTTP ${status3} \u2014 ${detail}` };
39194
+ }
39049
39195
  function readInstanceStatusAnswer(status3, text) {
39050
39196
  let parsed;
39051
39197
  try {
@@ -39060,13 +39206,14 @@ function readInstanceStatusAnswer(status3, text) {
39060
39206
  const detail = typeof body?.error === "string" ? body.error : text.slice(0, 200);
39061
39207
  return { kind: "unanswered", reason: `HTTP ${status3} \u2014 ${detail}` };
39062
39208
  }
39063
- var COORDINATOR_INSTANCES_PATH, COORDINATOR_INSTANCE_STATUS_PREFIX;
39209
+ var COORDINATOR_INSTANCES_PATH, COORDINATOR_INSTANCE_STATUS_PREFIX, COORDINATOR_INSTANCE_EVENTS_SUFFIX;
39064
39210
  var init_instancesRoute = __esm({
39065
39211
  "../../src/core/coordinator/instancesRoute.ts"() {
39066
39212
  "use strict";
39067
39213
  init_contract2();
39068
39214
  COORDINATOR_INSTANCES_PATH = "/admin/coordinator/instances";
39069
39215
  COORDINATOR_INSTANCE_STATUS_PREFIX = `${COORDINATOR_INSTANCES_PATH}/`;
39216
+ COORDINATOR_INSTANCE_EVENTS_SUFFIX = "/events";
39070
39217
  }
39071
39218
  });
39072
39219
 
@@ -39112,6 +39259,29 @@ async function fetchInstanceStatusViaShim(opts, id) {
39112
39259
  return { kind: "unanswered", reason: unreachable(err2) };
39113
39260
  }
39114
39261
  }
39262
+ function shimWorkflowSender(opts) {
39263
+ return {
39264
+ get: (id) => Promise.resolve({
39265
+ async sendEvent(event) {
39266
+ const at = shimAddress(opts);
39267
+ if ("reason" in at) throw new Error(at.reason);
39268
+ const fetchImpl = opts.fetch ?? fetch;
39269
+ const res = await fetchImpl(
39270
+ `${at.base}${COORDINATOR_INSTANCE_STATUS_PREFIX}${encodeURIComponent(id)}${COORDINATOR_INSTANCE_EVENTS_SUFFIX}`,
39271
+ {
39272
+ method: "POST",
39273
+ headers: { authorization: `Bearer ${at.bearer}`, "content-type": "application/json" },
39274
+ body: JSON.stringify(event),
39275
+ signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
39276
+ }
39277
+ );
39278
+ const answer = readSendEventAnswer(res.status, await res.text().catch(() => ""));
39279
+ if (answer.kind === "sent") return;
39280
+ throw new Error(answer.kind === "absent" ? `no such instance: ${id}` : answer.reason);
39281
+ }
39282
+ })
39283
+ };
39284
+ }
39115
39285
  var DEFAULT_TIMEOUT_MS, unreachable;
39116
39286
  var init_instancesClient = __esm({
39117
39287
  "../../src/core/coordinator/instancesClient.ts"() {
@@ -39201,10 +39371,10 @@ async function runShipBranch(deps, msg, io, ctx) {
39201
39371
  ...msg.channelName ? { channel: msg.channelName } : {},
39202
39372
  ...msg.userName ? { user: msg.userName } : {}
39203
39373
  };
39204
- const request = humanize ? humanizeMessageText(directives.text) : directives.text;
39374
+ const request2 = humanize ? humanizeMessageText(directives.text) : directives.text;
39205
39375
  publishText(
39206
39376
  "input",
39207
- attachments ? `${request} ${attachments}` : request,
39377
+ attachments ? `${request2} ${attachments}` : request2,
39208
39378
  Object.keys(source2).length > 0 ? source2 : void 0
39209
39379
  );
39210
39380
  registry.publish(run2.id, {
@@ -39527,18 +39697,18 @@ async function abandonLostWorkspace(ctx) {
39527
39697
  return restored?.msg;
39528
39698
  }
39529
39699
  function prepareRestartTurn(deps, ctx) {
39530
- const { request, pending: pending2, clock: clock4, restartOf } = ctx;
39700
+ const { request: request2, pending: pending2, clock: clock4, restartOf } = ctx;
39531
39701
  const merged = mergeFollowUps(pending2);
39532
39702
  const receivedAt = clock4();
39533
39703
  const msg = {
39534
- ...request,
39704
+ ...request2,
39535
39705
  ...merged ? {
39536
- text: `${request.text}
39706
+ text: `${request2.text}
39537
39707
 
39538
39708
  ${merged.text}`,
39539
- ...merged.images || request.images ? { images: [...request.images ?? [], ...merged.images ?? []] } : {},
39540
- ...merged.documents || request.documents ? { documents: [...request.documents ?? [], ...merged.documents ?? []] } : {},
39541
- ...merged.staged || request.staged ? { staged: [...request.staged ?? [], ...merged.staged ?? []] } : {}
39709
+ ...merged.images || request2.images ? { images: [...request2.images ?? [], ...merged.images ?? []] } : {},
39710
+ ...merged.documents || request2.documents ? { documents: [...request2.documents ?? [], ...merged.documents ?? []] } : {},
39711
+ ...merged.staged || request2.staged ? { staged: [...request2.staged ?? [], ...merged.staged ?? []] } : {}
39542
39712
  } : {},
39543
39713
  receivedAt,
39544
39714
  originAt: void 0
@@ -39675,7 +39845,7 @@ var init_thread = __esm({
39675
39845
 
39676
39846
  // ../../src/core/dispatch/seed.ts
39677
39847
  function sessionSeed(input) {
39678
- const { tail, previous, history, request, refusedRequests = [] } = input;
39848
+ const { tail, previous, history, request: request2, refusedRequests = [] } = input;
39679
39849
  const { from, transcript } = tail;
39680
39850
  if (from === 0 && transcript.turns === 0) return void 0;
39681
39851
  const notes = [];
@@ -39750,7 +39920,7 @@ function sessionSeed(input) {
39750
39920
  }
39751
39921
  messages.push({
39752
39922
  role: "user",
39753
- content: requestContent(request.text, request.images, request.documents, request.references)
39923
+ content: requestContent(request2.text, request2.images, request2.documents, request2.references)
39754
39924
  });
39755
39925
  return {
39756
39926
  messages,
@@ -40247,7 +40417,7 @@ async function dispatch(deps, msg, io, opts = {}) {
40247
40417
  if (attach.kind === "refused") return ended;
40248
40418
  if (attach.kind === "reattach_refused") {
40249
40419
  if (resume) {
40250
- const request = await abandonLostWorkspace({
40420
+ const request2 = await abandonLostWorkspace({
40251
40421
  msg,
40252
40422
  io,
40253
40423
  refuse: refuse3,
@@ -40261,7 +40431,7 @@ async function dispatch(deps, msg, io, opts = {}) {
40261
40431
  ledgerRun,
40262
40432
  why: attach.why
40263
40433
  });
40264
- if (request) restartRequest = { request, restartOf: resume.row.runId };
40434
+ if (request2) restartRequest = { request: request2, restartOf: resume.row.runId };
40265
40435
  resumeRowClosed = true;
40266
40436
  }
40267
40437
  return ended;
@@ -40292,10 +40462,10 @@ async function dispatch(deps, msg, io, opts = {}) {
40292
40462
  workspaceFiles.record(pulled);
40293
40463
  line3 = attachmentsLine(pulled);
40294
40464
  } else line3 = noWorkspaceLine(staged);
40295
- const request = messages[messages.length - 1];
40296
- if (request && request.role === "user" && line3) {
40297
- request.content = Array.isArray(request.content) ? [...request.content, { type: "text", text: line3 }] : [
40298
- { type: "text", text: request.content },
40465
+ const request2 = messages[messages.length - 1];
40466
+ if (request2 && request2.role === "user" && line3) {
40467
+ request2.content = Array.isArray(request2.content) ? [...request2.content, { type: "text", text: line3 }] : [
40468
+ { type: "text", text: request2.content },
40299
40469
  { type: "text", text: line3 }
40300
40470
  ];
40301
40471
  }
@@ -41757,6 +41927,2214 @@ var init_piHarness = __esm({
41757
41927
  }
41758
41928
  });
41759
41929
 
41930
+ // ../../src/core/harness/opencode/dispositions.ts
41931
+ function openCodeDispositionOf(type) {
41932
+ if (type.startsWith(OPENCODE_RPC_EVENT_PREFIX)) return "folded";
41933
+ return OPENCODE_EVENT_DISPOSITION[type];
41934
+ }
41935
+ var OPENCODE_RPC_EVENT_PREFIX, OPENCODE_EVENT_DISPOSITION;
41936
+ var init_dispositions = __esm({
41937
+ "../../src/core/harness/opencode/dispositions.ts"() {
41938
+ "use strict";
41939
+ init_toolRules();
41940
+ OPENCODE_RPC_EVENT_PREFIX = "rpc.";
41941
+ OPENCODE_EVENT_DISPOSITION = {
41942
+ // The stream's own marker.
41943
+ "server.connected": "structure",
41944
+ // ── Session: the assistant's turn as the record speaks it ────────────────
41945
+ // The tool call and its result, in pi's tool words (the per-call mirror).
41946
+ "session.tool.called": "mapped",
41947
+ "session.tool.success": "mapped",
41948
+ "session.tool.failed": "mapped",
41949
+ // The model's prose as it lands: the narration beside a call (the `assistant`
41950
+ // event) or, for a text-only turn, the answer.
41951
+ "session.text.ended": "mapped",
41952
+ // The context OpenCode compacted, with the summary it wrote (the compaction row).
41953
+ "session.compaction.ended": "mapped",
41954
+ // ── Session: partial output the final record carries (folded) ────────────
41955
+ "session.text.delta": "folded",
41956
+ "session.reasoning.delta": "folded",
41957
+ "session.compaction.delta": "folded",
41958
+ "session.step.streamed": "folded",
41959
+ "session.tool.progress": "folded",
41960
+ "session.tool.input.started": "folded",
41961
+ "session.tool.input.delta": "folded",
41962
+ "session.tool.input.ended": "folded",
41963
+ // ── Session: the notes (a `run_note`) ────────────────────────────────────
41964
+ // The gate's ask and its answer; the bridge acts on these, and on the record
41965
+ // they are the `tool_refused` note a refusal writes (never a second line).
41966
+ "permission.asked": "note",
41967
+ "permission.replied": "note",
41968
+ "session.retry.scheduled": "note",
41969
+ "session.compaction.failed": "note",
41970
+ "session.execution.failed": "note",
41971
+ "session.step.failed": "note",
41972
+ // ── Session: the run's own shape, already recorded (structure) ───────────
41973
+ // A narration's start writes nothing (its end does); the reasoning is
41974
+ // dropped whole, as pi's mirror drops thinking blocks — the record carries
41975
+ // what the model said and was told, never its private replay material.
41976
+ "session.text.started": "structure",
41977
+ "session.reasoning.started": "structure",
41978
+ "session.reasoning.ended": "structure",
41979
+ "session.step.started": "structure",
41980
+ "session.step.ended": "structure",
41981
+ "session.usage.updated": "structure",
41982
+ "session.compaction.started": "structure",
41983
+ "session.created": "structure",
41984
+ "session.execution.started": "structure",
41985
+ "session.execution.succeeded": "structure",
41986
+ "session.execution.interrupted": "structure",
41987
+ "session.idle": "structure",
41988
+ "session.status": "structure",
41989
+ "session.inbox.delivered": "structure",
41990
+ "session.inbox.enqueued": "structure",
41991
+ "session.inbox.cancelled": "structure",
41992
+ "session.inbox.delivery.changed": "structure",
41993
+ "session.instructions.updated": "structure",
41994
+ "session.permissions.updated": "structure",
41995
+ "session.synthetic": "structure",
41996
+ "session.viewed": "structure",
41997
+ // ── Session: the kinds this run never causes (impossible) ────────────────
41998
+ // The custom agent is the one agent and the proxy the one model, chosen at
41999
+ // create; a switch mid-run would be someone else driving the session.
42000
+ "session.agent.selected": "impossible",
42001
+ "session.model.selected": "impossible",
42002
+ // The session is never moved, forked, renamed or deleted under the run.
42003
+ "session.moved": "impossible",
42004
+ "session.forked": "impossible",
42005
+ "session.renamed": "impossible",
42006
+ "session.deleted": "impossible",
42007
+ // The checkout is Switchboard's; OpenCode never reverts it (`snapshots: false`).
42008
+ "session.revert.staged": "impossible",
42009
+ "session.revert.cleared": "impossible",
42010
+ "session.revert.committed": "impossible",
42011
+ // The persistent shell and the skill activation are features this config leaves off.
42012
+ "session.shell.started": "impossible",
42013
+ "session.shell.ended": "impossible",
42014
+ "session.skill.activated": "impossible",
42015
+ // ── The server's global catalogues and status (structure) ────────────────
42016
+ "agent.updated": "structure",
42017
+ "catalog.updated": "structure",
42018
+ "command.updated": "structure",
42019
+ "config.updated": "structure",
42020
+ "filesystem.changed": "structure",
42021
+ "integration.updated": "structure",
42022
+ "mcp.resources.changed": "structure",
42023
+ "mcp.status.changed": "structure",
42024
+ "plugin.updated": "structure",
42025
+ "project.updated": "structure",
42026
+ "reference.updated": "structure",
42027
+ "skill.updated": "structure",
42028
+ "vcs.branch.updated": "structure",
42029
+ "worktree.resolved": "structure",
42030
+ "worktree.updated": "structure",
42031
+ // ── The server's features this run turns off (impossible) ────────────────
42032
+ "credential.switched": "impossible",
42033
+ "credential.updated": "impossible",
42034
+ "form.created": "impossible",
42035
+ "form.cancelled": "impossible",
42036
+ "form.replied": "impossible",
42037
+ "installation.update-available": "impossible",
42038
+ "installation.updated": "impossible",
42039
+ "models-dev.refreshed": "impossible",
42040
+ "persistent-pty.added": "impossible",
42041
+ "persistent-pty.removed": "impossible",
42042
+ "pty.created": "impossible",
42043
+ "pty.deleted": "impossible",
42044
+ "pty.exited": "impossible",
42045
+ "pty.updated": "impossible",
42046
+ "shell.created": "impossible",
42047
+ "shell.deleted": "impossible",
42048
+ "shell.exited": "impossible",
42049
+ "tui.command.execute": "impossible",
42050
+ "tui.prompt.append": "impossible",
42051
+ "tui.session.select": "impossible",
42052
+ "tui.toast.show": "impossible",
42053
+ "websearch.updated": "impossible"
42054
+ };
42055
+ }
42056
+ });
42057
+
42058
+ // ../../src/core/harness/opencode/client.ts
42059
+ function openCodeAuthHeader(password) {
42060
+ return `Basic ${Buffer.from(`${OPENCODE_AUTH_USER}:${password}`).toString("base64")}`;
42061
+ }
42062
+ function parseFeedRecord(line3) {
42063
+ let value;
42064
+ try {
42065
+ value = JSON.parse(line3);
42066
+ } catch {
42067
+ return void 0;
42068
+ }
42069
+ if (typeof value !== "object" || value === null || !("feed" in value)) return void 0;
42070
+ const feed = value.feed;
42071
+ return feed === "event" || feed === "permissions" || feed === "messages" || feed === "tailer" ? value : void 0;
42072
+ }
42073
+ function parseHealth(body) {
42074
+ let value;
42075
+ try {
42076
+ value = JSON.parse(body);
42077
+ } catch {
42078
+ return void 0;
42079
+ }
42080
+ if (typeof value !== "object" || value === null) return void 0;
42081
+ const v = value;
42082
+ if (v.healthy !== true || typeof v.version !== "string") return void 0;
42083
+ return { healthy: true, version: v.version, pid: typeof v.pid === "number" ? v.pid : 0 };
42084
+ }
42085
+ function parseConfigEntries(body) {
42086
+ let value;
42087
+ try {
42088
+ value = JSON.parse(body);
42089
+ } catch {
42090
+ return void 0;
42091
+ }
42092
+ if (!Array.isArray(value)) return void 0;
42093
+ const entries = [];
42094
+ for (const item of value) {
42095
+ if (typeof item !== "object" || item === null) return void 0;
42096
+ const e = item;
42097
+ if (typeof e.path !== "string") return void 0;
42098
+ if (e.type === "directory") entries.push({ type: "directory", path: e.path });
42099
+ else if (e.type === "document" && typeof e.info === "object" && e.info !== null)
42100
+ entries.push({ type: "document", path: e.path, info: e.info });
42101
+ else return void 0;
42102
+ }
42103
+ return entries;
42104
+ }
42105
+ var OPENCODE_VERSION, OPENCODE_VERSION_TEXT, OPENCODE_AUTH_USER, OPENCODE_ROUTES, openCodeSessionRoutes, openCodePermissionReplyRoute;
42106
+ var init_client2 = __esm({
42107
+ "../../src/core/harness/opencode/client.ts"() {
42108
+ "use strict";
42109
+ OPENCODE_VERSION = "2.0.3";
42110
+ OPENCODE_VERSION_TEXT = `opencode v${OPENCODE_VERSION}`;
42111
+ OPENCODE_AUTH_USER = "opencode";
42112
+ OPENCODE_ROUTES = {
42113
+ /** `packages/protocol/src/groups/health.ts:16` — answers `Health`; behind the password like every route (`packages/server/src/process.ts:189-192`). */
42114
+ "health.get": { method: "GET", path: "/api/health" },
42115
+ /** `packages/protocol/src/groups/config.ts:9` — the loaded configuration entries, the readiness proof that the run's file took. */
42116
+ "config.get": { method: "GET", path: "/api/config" },
42117
+ /** `packages/protocol/src/groups/plugin.ts:24` — settles plugin activation (the relay plugin) before the first session; 204. */
42118
+ "plugin.awaitActivation": { method: "POST", path: "/api/plugin/await-activation" },
42119
+ /** `packages/protocol/src/groups/event.ts:43` — the SSE stream the tailer subscribes to; volatile by contract. */
42120
+ "event.subscribe": { method: "GET", path: "/api/event" },
42121
+ /** `packages/protocol/src/groups/session.ts:172`. */
42122
+ "session.create": { method: "POST", path: "/api/session" },
42123
+ /** `packages/protocol/src/groups/session.ts:192` — the authored session (the survival word). */
42124
+ "session.import": { method: "POST", path: "/api/session/import" }
42125
+ };
42126
+ openCodeSessionRoutes = (sessionID) => ({
42127
+ /** `packages/protocol/src/groups/session.ts:339` — admits one input; async: the answer is the inbox item, not the turn. */
42128
+ "session.prompt": { method: "POST", path: `/api/session/${sessionID}/prompt` },
42129
+ /** `packages/protocol/src/groups/session.ts:464` — BLOCKS until the agent loop is idle, then 204 (measured: a 239 s wait held open). */
42130
+ "session.wait": { method: "POST", path: `/api/session/${sessionID}/wait` },
42131
+ /** `packages/protocol/src/groups/session.ts:692` — `?continue=true|false`; answers `{ interrupted }`. */
42132
+ "session.interrupt": { method: "POST", path: `/api/session/${sessionID}/interrupt` },
42133
+ /** `packages/protocol/src/groups/message.ts:43` — `?order=asc|desc&limit=1..200&cursor=&type=`; the store, the record's truth. */
42134
+ "session.messages": { method: "GET", path: `/api/session/${sessionID}/message` },
42135
+ /** `packages/protocol/src/groups/permission.ts:85` — the pending asks the session owns. */
42136
+ "session.permission.list": { method: "GET", path: `/api/session/${sessionID}/permission` }
42137
+ });
42138
+ openCodePermissionReplyRoute = (sessionID, requestID) => ({
42139
+ method: "POST",
42140
+ path: `/api/session/${sessionID}/permission/${requestID}/reply`
42141
+ });
42142
+ }
42143
+ });
42144
+
42145
+ // ../../src/core/harness/opencode/session.ts
42146
+ function openCodeReplacedCallNote(tool) {
42147
+ return `The container running OpenCode was replaced while this ${tool} call was in flight; its result was lost \u2014 re-check its effects before re-running it.`;
42148
+ }
42149
+ function openCodeSettlementNote(s) {
42150
+ return s.action === "synthetic" ? s.text : openCodeReplacedCallNote(s.toolUse.name);
42151
+ }
42152
+ function openCodeSessionId(runId3) {
42153
+ return `ses_${runId3.replace(/[^A-Za-z0-9]/g, "-")}`;
42154
+ }
42155
+ function tickingClock(at) {
42156
+ let n2 = 0;
42157
+ return () => at + n2++;
42158
+ }
42159
+ function textOf2(content) {
42160
+ return content.filter((p) => p.type === "text").map((p) => p.text).join("\n\n");
42161
+ }
42162
+ function openCodeToolContent(toolUse, resultText, at, isError = false) {
42163
+ const input = isRecord7(toolUse.input) ? toolUse.input : {};
42164
+ const content = [{ type: "text", text: resultText || "(no output)" }];
42165
+ return {
42166
+ type: "tool",
42167
+ id: toolUse.id,
42168
+ name: toolUse.name,
42169
+ state: isError ? { status: "error", input, error: { message: resultText || "the call was interrupted" }, content } : { status: "completed", input, content },
42170
+ time: { created: at, completed: at }
42171
+ };
42172
+ }
42173
+ function openCodeCompactionMessage(id, entry, at) {
42174
+ return {
42175
+ id,
42176
+ type: "compaction",
42177
+ status: "completed",
42178
+ reason: "auto",
42179
+ summary: entry.summary,
42180
+ recent: "",
42181
+ time: { created: at }
42182
+ };
42183
+ }
42184
+ function openCodeStoreMessages(messages, opts) {
42185
+ const out = [];
42186
+ const model = opts.model ?? { providerID: "switchboard", id: "model" };
42187
+ const agent = opts.agent ?? "switchboard";
42188
+ const at = opts.clock ?? tickingClock(opts.at);
42189
+ const resultOf = (toolUseId, from) => {
42190
+ for (let i = from; i < messages.length; i++) {
42191
+ const turn = messages[i];
42192
+ if (turn.role !== "user") break;
42193
+ for (const part of turn.content) {
42194
+ if (part.type === "tool_result" && part.toolUseId === toolUseId) {
42195
+ const text = typeof part.content === "string" ? part.content : part.content.map((c) => c.type === "text" ? c.text : `[${c.type}]`).join("\n");
42196
+ return { text, isError: part.isError === true };
42197
+ }
42198
+ }
42199
+ }
42200
+ return { text: opts.settlements?.get(toolUseId) ?? "", isError: true };
42201
+ };
42202
+ messages.forEach((message4, i) => {
42203
+ if (message4.role === "user") {
42204
+ const text = textOf2(message4.content);
42205
+ const onlyResults = message4.content.length > 0 && message4.content.every((p) => p.type === "tool_result");
42206
+ if (onlyResults) return;
42207
+ out.push({ id: `msg_${opts.sessionID}_u${i}`, type: "user", text, time: { created: at() } });
42208
+ return;
42209
+ }
42210
+ const content = [];
42211
+ for (const part of message4.content) {
42212
+ if (part.type === "text") content.push({ type: "text", text: part.text });
42213
+ else if (part.type === "tool_use") {
42214
+ const result = resultOf(part.id, i + 1);
42215
+ content.push(openCodeToolContent(part, result.text, at(), result.isError));
42216
+ }
42217
+ }
42218
+ if (content.length === 0) return;
42219
+ const now = at();
42220
+ out.push({
42221
+ id: `msg_${opts.sessionID}_a${i}`,
42222
+ type: "assistant",
42223
+ agent,
42224
+ model,
42225
+ content,
42226
+ time: { created: now, completed: now }
42227
+ });
42228
+ });
42229
+ return out;
42230
+ }
42231
+ function openCodeImportBody(messages, opts) {
42232
+ const clock4 = tickingClock(opts.at);
42233
+ const storeMessages = openCodeStoreMessages(messages, { ...opts, clock: clock4 });
42234
+ (opts.compactions ?? []).forEach(
42235
+ (entry, i) => storeMessages.push(openCodeCompactionMessage(`msg_${opts.sessionID}_c${i}`, entry, clock4()))
42236
+ );
42237
+ const info = {
42238
+ id: opts.sessionID,
42239
+ projectID: "global",
42240
+ cost: 0,
42241
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
42242
+ time: { created: opts.at, updated: opts.at },
42243
+ location: opts.location,
42244
+ ...opts.agent ? { agent: opts.agent } : {},
42245
+ ...opts.model ? { model: opts.model } : {}
42246
+ };
42247
+ return { info, messages: storeMessages, location: opts.location };
42248
+ }
42249
+ function openCodeSeedAndRequest(messages) {
42250
+ for (let i = messages.length - 1; i >= 0; i--) {
42251
+ if (messages[i].role === "user") {
42252
+ return { seed: messages.slice(0, i), request: textOf2(messages[i].content) };
42253
+ }
42254
+ }
42255
+ return { seed: messages.slice(), request: "" };
42256
+ }
42257
+ var isRecord7;
42258
+ var init_session2 = __esm({
42259
+ "../../src/core/harness/opencode/session.ts"() {
42260
+ "use strict";
42261
+ isRecord7 = (v) => typeof v === "object" && v !== null;
42262
+ }
42263
+ });
42264
+
42265
+ // ../../src/core/harness/opencode/pluginSource.ts
42266
+ var OC_BLOCKED_AT_DOOR_PREFIX, OC_BLOCKED_UNAVAILABLE_PREFIX, OPENCODE_PLUGIN_SOURCE;
42267
+ var init_pluginSource = __esm({
42268
+ "../../src/core/harness/opencode/pluginSource.ts"() {
42269
+ "use strict";
42270
+ OC_BLOCKED_AT_DOOR_PREFIX = "authorization refused at the door: ";
42271
+ OC_BLOCKED_UNAVAILABLE_PREFIX = "authorization unavailable: ";
42272
+ OPENCODE_PLUGIN_SOURCE = `// Switchboard's OpenCode plugin. Written into the run's plugin directory by the
42273
+ // bot before \`opencode serve\` starts; loaded as \`./plugins/switchboard\`'s
42274
+ // \`index.js\`. Imports nothing.
42275
+
42276
+ const BEARER_ENV = "SWITCHBOARD_RUN_BEARER";
42277
+ const URL_ENV = "SWITCHBOARD_HARNESS_URL";
42278
+ /** How long the door ask keeps retrying an unreachable bot before it blocks. */
42279
+ const AUTHORIZE_WAIT_MS = 90000;
42280
+ const AUTHORIZE_RETRY_MS = 2000;
42281
+ /** How long one request for a relayed tool's result may take before it is asked
42282
+ * again: over the bot's window (30 s, after which it answers the call is still
42283
+ * running), under this runtime's own header timeout, so a hung connection
42284
+ * costs a minute of the wait, not five. */
42285
+ const TOOL_REQUEST_TIMEOUT_MS = 60000;
42286
+ /** How long a relayed call keeps asking a bot it cannot reach before it fails. */
42287
+ const TOOL_WAIT_MS = 90000;
42288
+ /** A bot that says a call is still running is asked again no sooner than this,
42289
+ * so a misbehaving bot is never asked in a hot loop. */
42290
+ const TOOL_REASK_FLOOR_MS = 1000;
42291
+
42292
+ function settings() {
42293
+ const url = process.env[URL_ENV];
42294
+ const bearer = process.env[BEARER_ENV];
42295
+ if (!url || !bearer) throw new Error("switchboard plugin: " + URL_ENV + " and " + BEARER_ENV + " must be set");
42296
+ return { base: url.replace(/\\/$/, ""), bearer };
42297
+ }
42298
+
42299
+ /** The bot answered, and said no: the status rides along so a caller can tell a
42300
+ * verdict from a bot it could not reach. */
42301
+ class HarnessAnswerError extends Error {
42302
+ constructor(message, status) {
42303
+ super(message);
42304
+ this.status = status;
42305
+ }
42306
+ }
42307
+
42308
+ async function call(method, path, body, signal) {
42309
+ const { base, bearer } = settings();
42310
+ const res = await fetch(base + path, {
42311
+ method,
42312
+ headers: { authorization: "Bearer " + bearer, "content-type": "application/json", accept: "application/json" },
42313
+ body: body === undefined ? undefined : JSON.stringify(body),
42314
+ signal,
42315
+ });
42316
+ const status = res.status;
42317
+ const text = await res.text();
42318
+ let json;
42319
+ try {
42320
+ json = text ? JSON.parse(text) : {};
42321
+ } catch {
42322
+ throw new HarnessAnswerError(
42323
+ "switchboard plugin: " + method + " " + path + " answered " + status + " with a body that is not JSON",
42324
+ status,
42325
+ );
42326
+ }
42327
+ if (!res.ok && status !== 202) {
42328
+ throw new HarnessAnswerError(
42329
+ "switchboard plugin: " + method + " " + path + " answered " + status + ": " + (json.error || text),
42330
+ status,
42331
+ );
42332
+ }
42333
+ return { status, json };
42334
+ }
42335
+
42336
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
42337
+
42338
+ /** The bot's verdict on one tool call at the door. A verdict answers at once,
42339
+ * and so does a refusal at the door (a 4xx: the bearer wrong, expired or
42340
+ * revoked, the run not on the harness). A bot that cannot be reached or is
42341
+ * failing (a network error, a 5xx, a body that is not JSON) is asked again
42342
+ * every two seconds until the wait is up, then the call is blocked naming why. */
42343
+ async function authorize(toolCallId, tool, input) {
42344
+ const started = Date.now();
42345
+ let lastError = "";
42346
+ for (;;) {
42347
+ try {
42348
+ const { json } = await call("POST", "/harness/authorize", { toolCallId, tool, input });
42349
+ return json;
42350
+ } catch (err) {
42351
+ lastError = err instanceof Error ? err.message : String(err);
42352
+ const status = err instanceof HarnessAnswerError ? err.status : undefined;
42353
+ if (status !== undefined && status >= 400 && status < 500) {
42354
+ return { allow: false, reason: "${OC_BLOCKED_AT_DOOR_PREFIX}" + lastError };
42355
+ }
42356
+ if (Date.now() - started >= AUTHORIZE_WAIT_MS) {
42357
+ return { allow: false, reason: "${OC_BLOCKED_UNAVAILABLE_PREFIX}the bot did not answer for 90 s (" + lastError + ")" };
42358
+ }
42359
+ await sleep(AUTHORIZE_RETRY_MS);
42360
+ }
42361
+ }
42362
+ }
42363
+
42364
+ /** pi's abort signal for the call, with this request's own timeout beside it
42365
+ * where the runtime can combine the two; the timeout alone where it cannot. */
42366
+ function requestSignal(signal) {
42367
+ const timeout = typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TOOL_REQUEST_TIMEOUT_MS) : undefined;
42368
+ if (!timeout) return signal;
42369
+ if (!signal) return timeout;
42370
+ return typeof AbortSignal.any === "function" ? AbortSignal.any([signal, timeout]) : signal;
42371
+ }
42372
+
42373
+ /** A relayed tool's result. The call is posted with its id; a bot that answers
42374
+ * that the call is still running (a \`202 pending\`) is asked again with the
42375
+ * same id until it answers: the bot runs the call once and every later ask
42376
+ * joins it. A bot that cannot be reached or is failing (a network error, a
42377
+ * timed-out request, a 5xx) is asked again every two seconds until the wait is
42378
+ * up, then the call fails naming why; a refusal at the door (a 4xx) fails it
42379
+ * at once; an abort ends the asking. */
42380
+ async function relay(toolCallId, tool, input, signal) {
42381
+ let lastAnswered = Date.now();
42382
+ let lastError = "";
42383
+ for (;;) {
42384
+ if (signal && signal.aborted) throw new Error("switchboard plugin: " + tool + " was aborted before the bot answered");
42385
+ const asked = Date.now();
42386
+ let answer;
42387
+ try {
42388
+ answer = await call("POST", "/harness/tool", { toolCallId, tool, input }, requestSignal(signal));
42389
+ } catch (err) {
42390
+ if (signal && signal.aborted) throw err;
42391
+ const status = err instanceof HarnessAnswerError ? err.status : undefined;
42392
+ if (status !== undefined && status >= 400 && status < 500) throw err;
42393
+ lastError = err instanceof Error ? err.message : String(err);
42394
+ if (Date.now() - lastAnswered >= TOOL_WAIT_MS) {
42395
+ throw new Error("switchboard plugin: the bot did not answer " + tool + " for 90 s (" + lastError + ")");
42396
+ }
42397
+ await sleep(AUTHORIZE_RETRY_MS);
42398
+ continue;
42399
+ }
42400
+ lastAnswered = Date.now();
42401
+ if (answer.status !== 202 && !answer.json.pending) return answer.json;
42402
+ const held = Date.now() - asked;
42403
+ if (held < TOOL_REASK_FLOOR_MS) await sleep(TOOL_REASK_FLOOR_MS - held);
42404
+ }
42405
+ }
42406
+
42407
+ /** One relayed tool's content in OpenCode's shape: pi's text parts joined into
42408
+ * one string, an image part named (OpenCode's tool content carries text or a
42409
+ * file URI, never inline image bytes); never empty. */
42410
+ function contentText(content) {
42411
+ if (!Array.isArray(content)) return typeof content === "string" ? content : "";
42412
+ const text = content
42413
+ .map((c) => (c && c.type === "text" ? String(c.text) : c && c.type === "image" ? "[image " + (c.mimeType || "") + "]" : ""))
42414
+ .filter((t) => t.length > 0)
42415
+ .join("\\n");
42416
+ return text;
42417
+ }
42418
+
42419
+ /** One relayed tool as OpenCode's editor registers it: a DIRECT tool
42420
+ * (\`codemode: false\`) under its own name and JSON Schema, whose execute runs
42421
+ * the door and the relay with the model's call id. A refusal at the door or an
42422
+ * error from the tool becomes the call's error text \u2014 the model reads it. */
42423
+ function relayTool(def) {
42424
+ return {
42425
+ name: def.name,
42426
+ description: def.description,
42427
+ input: def.inputSchema,
42428
+ options: { codemode: false },
42429
+ async execute(input, context) {
42430
+ const callId = context && context.id;
42431
+ const verdict = await authorize(callId, def.name, input);
42432
+ if (verdict && verdict.allow === false) throw new Error(verdict.reason || "the bot refused this tool call");
42433
+ const answer = await relay(callId, def.name, input, context && context.signal);
42434
+ const text = contentText(answer.content);
42435
+ if (answer.isError) throw new Error(text || "the tool failed");
42436
+ return { content: text || "(no output)" };
42437
+ },
42438
+ };
42439
+ }
42440
+
42441
+ export default {
42442
+ id: "switchboard",
42443
+ async setup(ctx) {
42444
+ const { json } = await call("GET", "/harness/tools");
42445
+ const tools = (json && json.tools) || [];
42446
+ await ctx.tool.transform((editor) => {
42447
+ for (const def of tools) editor.add(relayTool(def));
42448
+ });
42449
+ // The seam the harness watches for a call OpenCode is about to run; the gate
42450
+ // itself rides \`permission.asked\` in the bot, never a decision here.
42451
+ ctx.tool.hook("execute.before", () => {});
42452
+ },
42453
+ };
42454
+ `;
42455
+ }
42456
+ });
42457
+
42458
+ // ../../src/core/harness/opencode/tailerSource.ts
42459
+ var OPENCODE_SERVE_PID_ENV, OPENCODE_TAILER_SOURCE;
42460
+ var init_tailerSource = __esm({
42461
+ "../../src/core/harness/opencode/tailerSource.ts"() {
42462
+ "use strict";
42463
+ OPENCODE_SERVE_PID_ENV = "SWITCHBOARD_SERVE_PID";
42464
+ OPENCODE_TAILER_SOURCE = `// Switchboard's OpenCode tailer. Written into the run's directory by the bot
42465
+ // and started beside \`opencode serve\`; its stdout is the run's feed. Requires
42466
+ // only Node's own modules.
42467
+ "use strict";
42468
+ const http = require("node:http");
42469
+
42470
+ const port = Number(process.env.SWITCHBOARD_HARNESS_PORT);
42471
+ const password = process.env.OPENCODE_PASSWORD || "";
42472
+ const servePid = Number(process.env.SWITCHBOARD_SERVE_PID) || 0;
42473
+ const auth = "Basic " + Buffer.from("opencode:" + password).toString("base64");
42474
+ const REFILL_ON = new Set([
42475
+ "session.step.ended",
42476
+ "session.step.failed",
42477
+ "session.execution.succeeded",
42478
+ "session.execution.failed",
42479
+ "session.execution.interrupted",
42480
+ ]);
42481
+ const MIN_BACKOFF_MS = 250;
42482
+ const MAX_BACKOFF_MS = 5000;
42483
+ const REQUEST_TIMEOUT_MS = 30000;
42484
+ const PAGE_LIMIT = 200;
42485
+
42486
+ /** sessionID -> Map(messageID -> the message as JSON text): the last list each refill saw. */
42487
+ const known = new Map();
42488
+ /** Refills run one at a time, in the order their step ends arrived, so the feed's records keep that order. */
42489
+ let pending = Promise.resolve();
42490
+ let backoffMs = MIN_BACKOFF_MS;
42491
+ let connections = 0;
42492
+
42493
+ function emit(record) {
42494
+ process.stdout.write(JSON.stringify(record) + "\\n");
42495
+ }
42496
+
42497
+ function note(text, extra) {
42498
+ emit(Object.assign({ feed: "tailer", at: Date.now(), note: text }, extra || {}));
42499
+ }
42500
+
42501
+ function detailOf(err) {
42502
+ return String((err && err.message) || err).slice(0, 300);
42503
+ }
42504
+
42505
+ function request(method, path, accept) {
42506
+ return new Promise((resolve, reject) => {
42507
+ const req = http.request(
42508
+ { host: "127.0.0.1", port, method, path, headers: { authorization: auth, accept } },
42509
+ (res) => {
42510
+ let body = "";
42511
+ res.setEncoding("utf8");
42512
+ res.on("data", (chunk) => (body += chunk));
42513
+ res.on("end", () => resolve({ status: res.statusCode || 0, body }));
42514
+ res.on("error", reject);
42515
+ },
42516
+ );
42517
+ req.on("error", reject);
42518
+ req.setTimeout(REQUEST_TIMEOUT_MS, () => req.destroy(new Error("the request timed out")));
42519
+ req.end();
42520
+ });
42521
+ }
42522
+
42523
+ async function getJson(path) {
42524
+ const res = await request("GET", path, "application/json");
42525
+ if (res.status !== 200) throw new Error("GET " + path + " answered " + res.status);
42526
+ return JSON.parse(res.body);
42527
+ }
42528
+
42529
+ /** The session's pending asks, whole; then its messages, the changed ones only. */
42530
+ async function refill(sessionID, reason) {
42531
+ const at = Date.now();
42532
+ try {
42533
+ const answer = await getJson("/api/session/" + sessionID + "/permission");
42534
+ emit({ feed: "permissions", at, sessionID, reason, data: Array.isArray(answer.data) ? answer.data : [] });
42535
+ } catch (err) {
42536
+ note("permission refill failed", { sessionID, reason, detail: detailOf(err) });
42537
+ }
42538
+ try {
42539
+ const all = [];
42540
+ let cursor;
42541
+ for (let page = 0; page < 10000; page++) {
42542
+ const query = (cursor ? "cursor=" + encodeURIComponent(cursor) : "order=asc") + "&limit=" + PAGE_LIMIT;
42543
+ const answer = await getJson("/api/session/" + sessionID + "/message?" + query);
42544
+ for (const message of Array.isArray(answer.data) ? answer.data : []) all.push(message);
42545
+ cursor = answer.cursor && answer.cursor.next;
42546
+ if (!cursor) break;
42547
+ }
42548
+ const seen = known.get(sessionID) || new Map();
42549
+ const changed = [];
42550
+ for (const message of all) {
42551
+ const text = JSON.stringify(message);
42552
+ if (seen.get(message.id) !== text) {
42553
+ seen.set(message.id, text);
42554
+ changed.push(message);
42555
+ }
42556
+ }
42557
+ known.set(sessionID, seen);
42558
+ emit({ feed: "messages", at, sessionID, reason, data: changed });
42559
+ } catch (err) {
42560
+ // The session is known from here on even though its list is empty, so the
42561
+ // reconnect sweep covers it: a session whose only refill failed would
42562
+ // otherwise wait for its next step end.
42563
+ if (!known.has(sessionID)) known.set(sessionID, new Map());
42564
+ note("message refill failed", { sessionID, reason, detail: detailOf(err) });
42565
+ }
42566
+ }
42567
+
42568
+ function queueRefill(sessionID, reason) {
42569
+ pending = pending.then(() => refill(sessionID, reason));
42570
+ }
42571
+
42572
+ function onEvent(event) {
42573
+ emit({ feed: "event", at: Date.now(), event });
42574
+ if (REFILL_ON.has(event.type) && event.data && typeof event.data.sessionID === "string")
42575
+ queueRefill(event.data.sessionID, event.type);
42576
+ }
42577
+
42578
+ function connected() {
42579
+ connections++;
42580
+ backoffMs = MIN_BACKOFF_MS;
42581
+ note(connections === 1 ? "connected" : "reconnected", { connections });
42582
+ // A gap is possible only after a drop: refill what a step end during it would have.
42583
+ if (connections > 1) for (const sessionID of known.keys()) queueRefill(sessionID, "reconnect");
42584
+ }
42585
+
42586
+ /** One subscription; resolves with why it ended. Blocks are \`data:\` lines
42587
+ * ended by a blank line; a comment line (the heartbeat) has no data. */
42588
+ function subscribe() {
42589
+ return new Promise((resolve) => {
42590
+ const req = http.request(
42591
+ { host: "127.0.0.1", port, method: "GET", path: "/api/event", headers: { authorization: auth, accept: "text/event-stream" } },
42592
+ (res) => {
42593
+ if (res.statusCode !== 200) {
42594
+ res.resume();
42595
+ res.on("end", () => resolve("the event stream answered " + res.statusCode));
42596
+ return;
42597
+ }
42598
+ connected();
42599
+ let buffer = "";
42600
+ res.setEncoding("utf8");
42601
+ res.on("data", (chunk) => {
42602
+ buffer += chunk;
42603
+ let cut;
42604
+ while ((cut = buffer.indexOf("\\n\\n")) >= 0) {
42605
+ const block = buffer.slice(0, cut);
42606
+ buffer = buffer.slice(cut + 2);
42607
+ const data = block
42608
+ .split("\\n")
42609
+ .filter((line) => line.startsWith("data:"))
42610
+ .map((line) => line.slice(5).replace(/^ /, ""))
42611
+ .join("\\n");
42612
+ if (!data) continue;
42613
+ try {
42614
+ onEvent(JSON.parse(data));
42615
+ } catch (err) {
42616
+ note("an event was not JSON", { detail: data.slice(0, 200) });
42617
+ }
42618
+ }
42619
+ });
42620
+ res.on("end", () => resolve("the event stream ended"));
42621
+ res.on("error", (err) => resolve("the event stream failed: " + detailOf(err)));
42622
+ },
42623
+ );
42624
+ req.on("error", (err) => resolve("the event stream could not be opened: " + detailOf(err)));
42625
+ req.end();
42626
+ });
42627
+ }
42628
+
42629
+ function serveAlive() {
42630
+ if (!servePid) return true;
42631
+ try {
42632
+ process.kill(servePid, 0);
42633
+ return true;
42634
+ } catch (err) {
42635
+ return false;
42636
+ }
42637
+ }
42638
+
42639
+ async function main() {
42640
+ note("started", { port, servePid });
42641
+ for (;;) {
42642
+ const why = await subscribe();
42643
+ note("stream closed", { detail: why });
42644
+ await pending.catch(() => {});
42645
+ if (!serveAlive()) {
42646
+ note("the server process is gone; exiting");
42647
+ return;
42648
+ }
42649
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
42650
+ backoffMs = Math.min(MAX_BACKOFF_MS, backoffMs * 2);
42651
+ }
42652
+ }
42653
+
42654
+ process.on("SIGTERM", () => process.exit(0));
42655
+ process.on("SIGINT", () => process.exit(0));
42656
+ main().then(
42657
+ () => process.exit(0),
42658
+ (err) => {
42659
+ note("tailer failed", { detail: String((err && err.stack) || err).slice(0, 600) });
42660
+ process.exit(1);
42661
+ },
42662
+ );
42663
+ `;
42664
+ }
42665
+ });
42666
+
42667
+ // ../../src/core/harness/opencode/process.ts
42668
+ import { randomBytes as randomBytes5 } from "node:crypto";
42669
+ function openCodeRunPaths(runId3) {
42670
+ return openCodeRunPathsAt(`/tmp/switchboard-oc-${runId3}`);
42671
+ }
42672
+ function openCodeRunPathsAt(dir) {
42673
+ const xdg = {
42674
+ data: `${dir}/xdg/data`,
42675
+ config: `${dir}/xdg/config`,
42676
+ cache: `${dir}/xdg/cache`,
42677
+ state: `${dir}/xdg/state`
42678
+ };
42679
+ const pluginDir = `${dir}/plugins/switchboard`;
42680
+ const commandDir = `${dir}/cmd`;
42681
+ const feed = `${dir}/feed.jsonl`;
42682
+ return {
42683
+ dir,
42684
+ // The directories the start makes at 700, the root first.
42685
+ dirs: [dir, xdg.data, xdg.config, xdg.cache, xdg.state, pluginDir, commandDir],
42686
+ fifo: `${dir}/serve.in`,
42687
+ log: `${dir}/serve.log`,
42688
+ errLog: `${dir}/serve.err`,
42689
+ pidFile: `${dir}/pid`,
42690
+ commandDir,
42691
+ xdg,
42692
+ config: `${dir}/opencode.json`,
42693
+ pluginDir,
42694
+ plugin: `${pluginDir}/index.js`,
42695
+ feed,
42696
+ tailerScript: `${dir}/tailer.js`,
42697
+ tailer: {
42698
+ dir,
42699
+ dirs: [dir],
42700
+ fifo: `${dir}/tailer.in`,
42701
+ log: feed,
42702
+ errLog: `${dir}/tailer.err`,
42703
+ pidFile: `${dir}/tailer.pid`,
42704
+ commandDir
42705
+ }
42706
+ };
42707
+ }
42708
+ function openCodeDeniedActions(identity2) {
42709
+ if (identity2 === "write") return OPENCODE_ALWAYS_DENIED;
42710
+ if (identity2 === "read") return [...OPENCODE_ALWAYS_DENIED, ...OPENCODE_READ_DENIED];
42711
+ return [...OPENCODE_ALWAYS_DENIED, ...OPENCODE_NONE_DENIED];
42712
+ }
42713
+ function openCodePermissionRules(identity2) {
42714
+ return [
42715
+ ASK_ALL_RULE,
42716
+ ...openCodeDeniedActions(identity2).map((action) => ({ action, resource: "*", effect: "deny" }))
42717
+ ];
42718
+ }
42719
+ function openCodeBuiltinToolsFor(identity2) {
42720
+ if (identity2 === "write") return OPENCODE_BUILTIN_TOOLS;
42721
+ if (identity2 === "read") return OPENCODE_READ_TOOLS;
42722
+ return OPENCODE_NO_TOOLS;
42723
+ }
42724
+ function openCodeProviderPackage(providerType) {
42725
+ return providerType === "anthropic" ? "aisdk:@ai-sdk/anthropic" : "aisdk:@ai-sdk/openai-compatible";
42726
+ }
42727
+ function openCodeProviderBaseUrl(harnessUrl) {
42728
+ return `${harnessUrl.replace(/\/$/, "")}/v1`;
42729
+ }
42730
+ function openCodePromptNote(relayTools, identity2) {
42731
+ const named = relayTools.map((t) => `\`${t}\``).join(", ");
42732
+ if (identity2 === "none") {
42733
+ return [
42734
+ "HARNESS NOTE: this run has no workspace, so none of OpenCode's own tools (`read`, `shell`, `edit`, `write`, `glob`, `grep` and `skill`) is available, and a call to any of them is refused.",
42735
+ relayTools.length > 0 ? `This run's tools are exactly these, each under its own name: ${named}.` : "No tools are available in this run."
42736
+ ].join(" ");
42737
+ }
42738
+ const write = identity2 === "write";
42739
+ return [
42740
+ write ? "HARNESS NOTE: this run's workspace tools are OpenCode's own \u2014 `read`, `shell`, `edit`, `write`, `glob`, `grep` and `skill`, all running in the worktree." : "HARNESS NOTE: this run's workspace tools are OpenCode's own \u2014 `read`, `shell`, `glob`, `grep` and `skill`, all running in the worktree; it has no `edit` and no `write`: the run is read-only, and a call to either is refused.",
42741
+ write ? "Where these instructions say `read_file` use `read`; where they say `write_file` use `write` (a whole file) or `edit` (a targeted change); where they say `bash` use `shell`." : "Where these instructions say `read_file` use `read`; where they say `bash` use `shell`.",
42742
+ relayTools.length > 0 ? `Every other tool named above is available under its own name: ${named}.` : "No other tools are available in this run."
42743
+ ].join(" ");
42744
+ }
42745
+ function openCodeSystemPrompt(spec) {
42746
+ return `${spec.system.trimEnd()}
42747
+
42748
+ ${openCodePromptNote(spec.relayTools, spec.identity)}
42749
+ `;
42750
+ }
42751
+ function openCodeConfig(spec) {
42752
+ const { id, maxTokens, providerType, contextTokens } = spec.model;
42753
+ const compaction = {
42754
+ ...spec.compaction?.buffer !== void 0 ? { buffer: spec.compaction.buffer } : {},
42755
+ ...spec.compaction?.keepTokens !== void 0 ? { keep: { tokens: spec.compaction.keepTokens } } : {}
42756
+ };
42757
+ return {
42758
+ providers: {
42759
+ [PROXY_PROVIDER]: {
42760
+ name: "Switchboard model proxy",
42761
+ package: openCodeProviderPackage(providerType),
42762
+ settings: { baseURL: openCodeProviderBaseUrl(spec.harnessUrl), apiKey: `{env:${RUN_BEARER_ENV}}` },
42763
+ models: {
42764
+ [id]: {
42765
+ name: id,
42766
+ limit: { context: contextTokens ?? OPENCODE_DEFAULT_CONTEXT_TOKENS, output: maxTokens },
42767
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }
42768
+ }
42769
+ }
42770
+ }
42771
+ },
42772
+ model: `${PROXY_PROVIDER}/${id}`,
42773
+ default_agent: OPENCODE_AGENT,
42774
+ agents: {
42775
+ [OPENCODE_AGENT]: {
42776
+ mode: "primary",
42777
+ system: openCodeSystemPrompt(spec),
42778
+ permissions: openCodePermissionRules(spec.identity)
42779
+ },
42780
+ title: { disabled: true }
42781
+ },
42782
+ plugins: [OPENCODE_PLUGIN_REF],
42783
+ update: "disable",
42784
+ share: "disabled",
42785
+ snapshots: false,
42786
+ lsp: false,
42787
+ formatter: false,
42788
+ ...Object.keys(compaction).length > 0 ? { compaction } : {}
42789
+ };
42790
+ }
42791
+ function openCodeConfigJson(spec) {
42792
+ return JSON.stringify(openCodeConfig(spec), null, 2) + "\n";
42793
+ }
42794
+ function openCodeLaunchFiles(spec) {
42795
+ return [
42796
+ { path: spec.paths.config, content: openCodeConfigJson(spec) },
42797
+ { path: spec.paths.plugin, content: OPENCODE_PLUGIN_SOURCE },
42798
+ { path: spec.paths.tailerScript, content: OPENCODE_TAILER_SOURCE }
42799
+ ];
42800
+ }
42801
+ function openCodeLaunchArgs() {
42802
+ return ["serve", "--hostname", "127.0.0.1", "--port", PORT_ARG];
42803
+ }
42804
+ function openCodePassword(bearer) {
42805
+ return bearerHashOf(bearer) ?? randomBytes5(24).toString("base64url");
42806
+ }
42807
+ function openCodeLaunchEnv(spec, bearer, password) {
42808
+ const { paths } = spec;
42809
+ return {
42810
+ [RUN_BEARER_ENV]: bearer,
42811
+ [HARNESS_URL_ENV]: spec.harnessUrl,
42812
+ SWITCHBOARD_RUN_ID: spec.runId,
42813
+ XDG_DATA_HOME: paths.xdg.data,
42814
+ XDG_CONFIG_HOME: paths.xdg.config,
42815
+ XDG_CACHE_HOME: paths.xdg.cache,
42816
+ XDG_STATE_HOME: paths.xdg.state,
42817
+ OPENCODE_CONFIG: paths.config,
42818
+ OPENCODE_CONFIG_PROJECT_DISABLE: "1",
42819
+ OPENCODE_DISABLE_PROJECT_CONFIG: "1",
42820
+ OPENCODE_DISABLE_MODELS_FETCH: "1",
42821
+ OPENCODE_DISABLE_AUTOUPDATE: "1",
42822
+ OPENCODE_DB: ":memory:",
42823
+ [OPENCODE_PASSWORD_ENV]: password
42824
+ };
42825
+ }
42826
+ function openCodeTailerEnv(password, servePid) {
42827
+ return { [OPENCODE_PASSWORD_ENV]: password, [OPENCODE_SERVE_PID_ENV]: String(servePid) };
42828
+ }
42829
+ function configProblem(info, spec) {
42830
+ const providers = info.providers;
42831
+ const agents = info.agents;
42832
+ if (typeof providers?.[PROXY_PROVIDER] !== "object") return `providers.${PROXY_PROVIDER} did not take`;
42833
+ if (info.default_agent !== OPENCODE_AGENT) return `default_agent is not ${OPENCODE_AGENT}`;
42834
+ if (typeof agents?.[OPENCODE_AGENT] !== "object") return `agents.${OPENCODE_AGENT} did not take`;
42835
+ if (agents?.title?.disabled !== true) return "agents.title.disabled did not take";
42836
+ if (!Array.isArray(info.plugins) || !info.plugins.includes(OPENCODE_PLUGIN_REF)) return "plugins did not take";
42837
+ if (info.update !== "disable") return "update is not disable";
42838
+ if (info.share !== "disabled") return "share is not disabled";
42839
+ if (info.snapshots !== false) return "snapshots is not false";
42840
+ if (info.lsp !== false) return "lsp is not false";
42841
+ if (info.formatter !== false) return "formatter is not false";
42842
+ if (spec.compaction && Object.keys(spec.compaction).length > 0 && typeof info.compaction !== "object")
42843
+ return "compaction did not take";
42844
+ return void 0;
42845
+ }
42846
+ function loadedDocument(entries, configPath) {
42847
+ const document = entries.find((e) => e.type === "document" && e.path === configPath);
42848
+ return document && document.type === "document" ? document.info : void 0;
42849
+ }
42850
+ async function launchOpenCode(deps, spec, bearer) {
42851
+ const { container, clock: clock4, sleep: sleep2 } = deps;
42852
+ const pollMs = deps.pollMs ?? OPENCODE_READY_POLL_MS;
42853
+ const readyMs = deps.readyMs ?? OPENCODE_READY_MS;
42854
+ const root = await container.makeRoot(spec.paths.dir);
42855
+ const paths = root === spec.paths.dir ? spec.paths : openCodeRunPathsAt(root);
42856
+ const placed = { ...spec, paths };
42857
+ for (const file of openCodeLaunchFiles(placed)) await container.writeFile(file.path, file.content);
42858
+ const password = openCodePassword(bearer);
42859
+ const started = await container.start({
42860
+ paths,
42861
+ command: OPENCODE_BIN,
42862
+ args: openCodeLaunchArgs(),
42863
+ env: openCodeLaunchEnv(placed, bearer, password),
42864
+ port: "free"
42865
+ });
42866
+ if (started.port === void 0)
42867
+ throw new HarnessContainerError("start", "the start answered no port for opencode serve");
42868
+ const { pid, port } = started;
42869
+ const auth = { Authorization: openCodeAuthHeader(password) };
42870
+ const request2 = (route3) => container.request(paths, { method: route3.method, port, path: route3.path, secretHeaders: auth });
42871
+ const notReady = async (reason2, errLog = paths.errLog) => new OpenCodeNotReadyError(reason2, await container.tail(errLog, READY_TAIL_BYTES));
42872
+ const startedAt = clock4();
42873
+ const deadline = startedAt + readyMs;
42874
+ let last = "no answer yet";
42875
+ let version2;
42876
+ let probePid = true;
42877
+ let poll = pollMs;
42878
+ while (version2 === void 0) {
42879
+ if (probePid && !await container.alive(pid))
42880
+ throw await notReady("the server exited before it answered its health");
42881
+ let res;
42882
+ try {
42883
+ res = await request2(OPENCODE_ROUTES["health.get"]);
42884
+ probePid = false;
42885
+ } catch (err2) {
42886
+ if (isContainerGone(err2)) throw err2;
42887
+ last = err2 instanceof Error ? err2.message : String(err2);
42888
+ probePid = true;
42889
+ }
42890
+ if (res) {
42891
+ if (res.status === 401) throw await notReady("the server refused the run's password");
42892
+ if (res.status === 500) throw await notReady("the server reported that its start failed (health answered 500)");
42893
+ if (res.status === 200) {
42894
+ const health = parseHealth(res.body);
42895
+ if (!health) throw await notReady("the health answered something that is not the health shape");
42896
+ if (health.version !== OPENCODE_VERSION)
42897
+ throw await notReady(`the opencode on PATH is ${health.version}; this build drives ${OPENCODE_VERSION}`);
42898
+ version2 = health.version;
42899
+ break;
42900
+ }
42901
+ last = `the health answered ${res.status}`;
42902
+ }
42903
+ if (clock4() >= deadline)
42904
+ throw await notReady(`the server did not answer its health within ${readyMs} ms (${last})`);
42905
+ await sleep2(poll);
42906
+ if (clock4() - startedAt >= 1e3) poll = Math.min(OPENCODE_READY_POLL_MAX_MS, poll * 2);
42907
+ }
42908
+ const config = await request2(OPENCODE_ROUTES["config.get"]);
42909
+ if (config.status !== 200) throw await notReady(`the configuration route answered ${config.status}`);
42910
+ const entries = parseConfigEntries(config.body);
42911
+ if (!entries) throw await notReady("the configuration route answered something that is not the entry list");
42912
+ const info = loadedDocument(entries, paths.config);
42913
+ if (!info) throw await notReady("the server did not load the run's configuration file");
42914
+ const problem = configProblem(info, placed);
42915
+ if (problem) throw await notReady(`the run's configuration ${problem}`);
42916
+ const activation = await request2(OPENCODE_ROUTES["plugin.awaitActivation"]);
42917
+ if (activation.status < 200 || activation.status >= 300)
42918
+ throw await notReady(`plugin activation answered ${activation.status}`);
42919
+ const tailer = await container.start({
42920
+ paths: paths.tailer,
42921
+ command: TAILER_BIN,
42922
+ args: [paths.tailerScript],
42923
+ env: openCodeTailerEnv(password, pid),
42924
+ port,
42925
+ // The feed is read by offset and a later generation may restart the
42926
+ // tailer over it: its log is never truncated by a start.
42927
+ keepLog: true
42928
+ });
42929
+ let feedOffset;
42930
+ poll = pollMs;
42931
+ while (feedOffset === void 0) {
42932
+ const text = Buffer.from(await container.readLog(paths.feed, 0, LOG_READ_BYTES)).toString("utf8");
42933
+ let consumed = 0;
42934
+ for (const line3 of text.split("\n").slice(0, -1)) {
42935
+ consumed += Buffer.byteLength(line3, "utf8") + 1;
42936
+ const record2 = parseFeedRecord(line3);
42937
+ if (record2?.feed === "tailer" && record2.note === "connected") {
42938
+ feedOffset = consumed;
42939
+ break;
42940
+ }
42941
+ }
42942
+ if (feedOffset !== void 0) break;
42943
+ if (!await container.alive(tailer.pid))
42944
+ throw await notReady("the tailer exited before it connected to the event stream", paths.tailer.errLog);
42945
+ if (clock4() >= deadline)
42946
+ throw await notReady(`the tailer did not connect to the event stream within ${readyMs} ms`, paths.tailer.errLog);
42947
+ await sleep2(poll);
42948
+ if (clock4() - startedAt >= 1e3) poll = Math.min(OPENCODE_READY_POLL_MAX_MS, poll * 2);
42949
+ }
42950
+ return { pid, port, tailerPid: tailer.pid, password, paths, version: version2, feedOffset };
42951
+ }
42952
+ function openCodeFacts(started, run2) {
42953
+ const bearerHash = run2.bearer === void 0 ? void 0 : bearerHashOf(run2.bearer);
42954
+ return {
42955
+ harness: "opencode",
42956
+ pid: started.pid,
42957
+ port: started.port,
42958
+ tailerPid: started.tailerPid,
42959
+ logOffset: run2.logOffset,
42960
+ sessionID: run2.sessionID,
42961
+ root: started.paths.dir,
42962
+ relaunches: run2.relaunches,
42963
+ ...bearerHash === void 0 ? {} : { bearerHash },
42964
+ ...run2.container === void 0 ? {} : { container: run2.container }
42965
+ };
42966
+ }
42967
+ var OPENCODE_BIN, TAILER_BIN, OPENCODE_AGENT, OPENCODE_PLUGIN_REF, OPENCODE_PASSWORD_ENV, OPENCODE_READY_MS, OPENCODE_READY_POLL_MS, OPENCODE_READY_POLL_MAX_MS, OPENCODE_DEFAULT_CONTEXT_TOKENS, READY_TAIL_BYTES, OPENCODE_BUILTIN_TOOLS, OPENCODE_READ_TOOLS, OPENCODE_NO_TOOLS, ASK_ALL_RULE, OPENCODE_ALWAYS_DENIED, OPENCODE_NONE_DENIED, OPENCODE_READ_DENIED, OpenCodeNotReadyError;
42968
+ var init_process2 = __esm({
42969
+ "../../src/core/harness/opencode/process.ts"() {
42970
+ "use strict";
42971
+ init_runBearers();
42972
+ init_container();
42973
+ init_process();
42974
+ init_client2();
42975
+ init_pluginSource();
42976
+ init_tailerSource();
42977
+ OPENCODE_BIN = "opencode";
42978
+ TAILER_BIN = "node";
42979
+ OPENCODE_AGENT = "switchboard";
42980
+ OPENCODE_PLUGIN_REF = "./plugins/switchboard";
42981
+ OPENCODE_PASSWORD_ENV = "OPENCODE_PASSWORD";
42982
+ OPENCODE_READY_MS = 3e4;
42983
+ OPENCODE_READY_POLL_MS = 250;
42984
+ OPENCODE_READY_POLL_MAX_MS = 2e3;
42985
+ OPENCODE_DEFAULT_CONTEXT_TOKENS = 2e5;
42986
+ READY_TAIL_BYTES = 2e3;
42987
+ OPENCODE_BUILTIN_TOOLS = ["read", "shell", "edit", "write", "glob", "grep", "skill"];
42988
+ OPENCODE_READ_TOOLS = ["read", "shell", "glob", "grep", "skill"];
42989
+ OPENCODE_NO_TOOLS = [];
42990
+ ASK_ALL_RULE = { action: "*", resource: "*", effect: "ask" };
42991
+ OPENCODE_ALWAYS_DENIED = ["question", "subagent", "execute", "webfetch", "websearch"];
42992
+ OPENCODE_NONE_DENIED = [
42993
+ "read",
42994
+ "edit",
42995
+ "shell",
42996
+ "glob",
42997
+ "grep",
42998
+ "skill",
42999
+ "external_directory"
43000
+ ];
43001
+ OPENCODE_READ_DENIED = ["edit"];
43002
+ OpenCodeNotReadyError = class extends Error {
43003
+ constructor(reason2, stderr) {
43004
+ super(`opencode serve is not ready: ${reason2}${stderr.trim() ? ` (stderr: ${stderr.trim().slice(-600)})` : ""}`);
43005
+ this.reason = reason2;
43006
+ this.name = "OpenCodeNotReadyError";
43007
+ }
43008
+ reason;
43009
+ };
43010
+ }
43011
+ });
43012
+
43013
+ // ../../src/core/harness/opencode/bridge.ts
43014
+ function openCodeToolNameWord(name) {
43015
+ return TOOL_NAME_WORD[name] ?? name;
43016
+ }
43017
+ function contentText(content) {
43018
+ if (typeof content === "string") return content;
43019
+ if (!Array.isArray(content)) return "";
43020
+ return content.filter((p) => isRecord8(p) && p.type === "text" && typeof p.text === "string").map((p) => String(p.text)).join("\n");
43021
+ }
43022
+ function isBudgetRefusal(error) {
43023
+ if (error.status !== void 0 && error.status !== 403) return false;
43024
+ return TURN_BUDGET_REFUSAL.test(error.message);
43025
+ }
43026
+ function judgeOpenCodeAsk(action, resources, rules, relayedToolNames) {
43027
+ const word = openCodeToolWord(action);
43028
+ const tool = TOOL_NAME_WORD[action] ?? word;
43029
+ if (relayedToolNames.has(action) || relayedToolNames.has(word)) return { reply: "once", tool: action };
43030
+ const refuse3 = (reason2) => ({ reply: "reject", message: reason2, tool });
43031
+ if (action === "external_directory") {
43032
+ for (const resource of resources) {
43033
+ const verdict2 = judgeToolCall("read", { path: resource }, rules);
43034
+ if (verdict2.verdict !== "allowed") return refuse3(verdict2.reason);
43035
+ }
43036
+ return { reply: "once", tool: "external_directory" };
43037
+ }
43038
+ if (word === "bash") {
43039
+ for (const command2 of resources.length > 0 ? resources : [""]) {
43040
+ const verdict2 = judgeToolCall("bash", { command: command2 }, rules);
43041
+ if (verdict2.verdict !== "allowed") return refuse3(verdict2.reason);
43042
+ }
43043
+ return { reply: "once", tool };
43044
+ }
43045
+ const input = word === "read" || word === "edit" || word === "write" ? { path: resources[0] } : {};
43046
+ const verdict = judgeToolCall(word, input, rules);
43047
+ if (verdict.verdict !== "allowed") return refuse3(verdict.reason);
43048
+ return { reply: "once", tool };
43049
+ }
43050
+ function assistantMessage2(turn) {
43051
+ return {
43052
+ role: "assistant",
43053
+ content: turn.content.map(
43054
+ (p) => p.type === "text" ? { type: "text", text: p.text } : p.type === "tool_use" ? { type: "toolCall", id: p.id, name: p.name, arguments: isRecord8(p.input) ? p.input : {} } : {}
43055
+ )
43056
+ };
43057
+ }
43058
+ function userMessage(turn) {
43059
+ const results = turn.content.filter(
43060
+ (p) => p.type === "tool_result"
43061
+ );
43062
+ if (results.length > 0) {
43063
+ return {
43064
+ role: "toolResult",
43065
+ toolCallId: results[0].toolUseId,
43066
+ content: [{ type: "text", text: typeof results[0].content === "string" ? results[0].content : "" }],
43067
+ isError: results[0].isError === true
43068
+ };
43069
+ }
43070
+ return {
43071
+ role: "user",
43072
+ content: turn.content.map((p) => p.type === "text" ? { type: "text", text: p.text } : {})
43073
+ };
43074
+ }
43075
+ function projectStore(messages, seedLength) {
43076
+ const turns = [];
43077
+ messages.slice(seedLength).forEach((message4) => {
43078
+ if (message4.type === "assistant") {
43079
+ const assistant = message4;
43080
+ const content = [];
43081
+ const results = [];
43082
+ for (const part of Array.isArray(assistant.content) ? assistant.content : []) {
43083
+ if (!isRecord8(part)) continue;
43084
+ if (part.type === "text" && typeof part.text === "string") content.push({ type: "text", text: part.text });
43085
+ else if (part.type === "tool") {
43086
+ const id = str6(part.id);
43087
+ const name = openCodeToolNameWord(str6(part.name));
43088
+ const state = isRecord8(part.state) ? part.state : {};
43089
+ content.push({ type: "tool_use", id, name, input: isRecord8(state.input) ? state.input : {} });
43090
+ if (state.status === "completed" || state.status === "error")
43091
+ results.push({
43092
+ type: "tool_result",
43093
+ toolUseId: id,
43094
+ content: contentText(state.content) || (state.status === "error" ? errorMessage(state.error) : ""),
43095
+ ...state.status === "error" ? { isError: true } : {}
43096
+ });
43097
+ }
43098
+ }
43099
+ if (content.length > 0) turns.push({ role: "assistant", content });
43100
+ for (const result of results) turns.push({ role: "user", content: [result] });
43101
+ } else if (message4.type === "user") {
43102
+ const text = str6(message4.text);
43103
+ if (text) turns.push({ role: "user", content: [{ type: "text", text }] });
43104
+ }
43105
+ });
43106
+ return turns;
43107
+ }
43108
+ function errorMessage(error) {
43109
+ return isRecord8(error) && typeof error.message === "string" ? error.message : "";
43110
+ }
43111
+ function statusOf(error) {
43112
+ return isRecord8(error) && typeof error.status === "number" ? error.status : void 0;
43113
+ }
43114
+ function openCodePromptText(messages) {
43115
+ for (let i = messages.length - 1; i >= 0; i--) {
43116
+ const message4 = messages[i];
43117
+ if (message4.role === "user")
43118
+ return message4.content.filter((p) => p.type === "text").map((p) => p.text).join("\n\n");
43119
+ }
43120
+ return "";
43121
+ }
43122
+ async function driveOpenCode(deps, run2, conn) {
43123
+ const now = () => deps.clock();
43124
+ const agentSpan = run2.span?.start("run.agent");
43125
+ if (agentSpan) deps.bearers?.reparent(run2.runId, agentSpan);
43126
+ const remainingMs = run2.resume?.remainingMs ?? run2.agent.maxMinutes * 6e4;
43127
+ const deadline = now() + remainingMs;
43128
+ const warnAt = deadline - Math.min(3 * 6e4, run2.agent.maxMinutes * 15e3);
43129
+ const emit = (event) => run2.onEvent?.(event.at === void 0 ? { ...event, at: now() } : event);
43130
+ const note = (kind, summary) => {
43131
+ run2.onProgress?.(summary);
43132
+ emit({ type: "run_note", kind, summary });
43133
+ };
43134
+ const bridge = new OpenCodeBridge({
43135
+ emit,
43136
+ ...run2.onProgress ? { onProgress: run2.onProgress } : {},
43137
+ clock: deps.clock,
43138
+ ...agentSpan ? { agentSpan } : {},
43139
+ rules: { ...run2.rules, identity: run2.agent.identity },
43140
+ relayedToolNames: new Set(run2.tools.map((t) => t.name)),
43141
+ ...run2.onStep ? { onStep: run2.onStep } : {},
43142
+ // The mirror skips the seed's rows: a fresh run's is the thread's turns plus
43143
+ // the request (`run.messages`); a rebuild's is the record it imported (its
43144
+ // transcript and every compaction row), which the store holds and the
43145
+ // request-prompt appends past.
43146
+ seedLength: run2.resume ? run2.resume.messages.length + (run2.resume.compactions?.length ?? 0) : run2.messages.length,
43147
+ remainingMs: () => deadline - now(),
43148
+ textFailing: new Set(run2.tools.filter((t) => t.failsInText).map((t) => t.name))
43149
+ });
43150
+ if (run2.resume && run2.resume.settlements.length > 0) {
43151
+ bridge.prime(
43152
+ run2.resume.settlements.map((s) => ({
43153
+ type: "tool_result",
43154
+ toolUseId: s.toolUse.id,
43155
+ content: openCodeSettlementNote(s),
43156
+ isError: true
43157
+ }))
43158
+ );
43159
+ }
43160
+ const auth = { Authorization: openCodeAuthHeader(conn.password) };
43161
+ const sessionRoutes = openCodeSessionRoutes(conn.sessionID);
43162
+ const request2 = (route3, body) => conn.container.request(conn.paths, {
43163
+ method: route3.method,
43164
+ port: conn.port,
43165
+ path: route3.path,
43166
+ secretHeaders: auth,
43167
+ ...body !== void 0 ? { headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}
43168
+ });
43169
+ const transport = new PiRpcTransport({
43170
+ container: conn.container,
43171
+ paths: { ...conn.paths.tailer, log: conn.paths.feed },
43172
+ pid: conn.tailerPid,
43173
+ pollMs: deps.pollMs ?? 250,
43174
+ sleep: deps.sleep,
43175
+ offset: conn.feedOffset
43176
+ });
43177
+ let writeUp;
43178
+ let hardStopped = false;
43179
+ let warned = false;
43180
+ let bypass;
43181
+ let replyFailed;
43182
+ let providerError;
43183
+ let settled = false;
43184
+ let containerSaid;
43185
+ const recordBase = {
43186
+ messages: run2.resume?.messages ?? run2.messages,
43187
+ compactions: run2.resume?.compactions ?? []
43188
+ };
43189
+ const startWriteUp = (w, instruction) => {
43190
+ writeUp = w;
43191
+ if (conn.writeUp)
43192
+ conn.writeUp.blocked = w.kind === "time" ? "the run has reached its time budget: no more tool calls \u2014 the run is writing its final answer" : "the run has hit its turn guard: no more tool calls \u2014 the run is writing its final answer";
43193
+ void request2(sessionRoutes["session.prompt"], { text: instruction, delivery: "steer" }).catch(() => {
43194
+ });
43195
+ };
43196
+ const turnCount = () => deps.bearers?.grantOf(run2.runId)?.turns ?? bridge.turns;
43197
+ const check = () => {
43198
+ if (run2.control?.requested === "hard") {
43199
+ if (!hardStopped) {
43200
+ hardStopped = true;
43201
+ run2.onProgress?.(hardStopNote());
43202
+ emit({ type: "run_note", kind: "stopped", summary: hardStopNote(), mode: "hard" });
43203
+ void request2(sessionRoutes["session.interrupt"]).catch(() => {
43204
+ });
43205
+ }
43206
+ return;
43207
+ }
43208
+ if (writeUp) return;
43209
+ if (now() >= deadline) {
43210
+ note("time_budget_exhausted", timeBudgetNote());
43211
+ startWriteUp({ kind: "time" }, timeBudgetInstruction());
43212
+ return;
43213
+ }
43214
+ if (turnCount() >= run2.agent.maxTurns) {
43215
+ const pace = turnGuardPace(turnCount(), run2.agent.maxMinutes * 6e4 - (deadline - now()));
43216
+ note("turn_budget_exhausted", turnGuardNote(pace));
43217
+ startWriteUp({ kind: "turns", pace }, turnGuardInstruction(pace));
43218
+ return;
43219
+ }
43220
+ if (!warned && now() >= warnAt) {
43221
+ warned = true;
43222
+ const minutesLeft = Math.max(1, Math.round((deadline - now()) / 6e4));
43223
+ note("wrap_up", wrapUpNote(minutesLeft));
43224
+ void request2(sessionRoutes["session.prompt"], { text: wrapUpInstruction(minutesLeft), delivery: "steer" }).catch(
43225
+ () => {
43226
+ }
43227
+ );
43228
+ }
43229
+ };
43230
+ try {
43231
+ await request2(sessionRoutes["session.prompt"], { text: openCodePromptText(run2.messages), delivery: "queue" });
43232
+ check();
43233
+ const iterator = transport.lines[Symbol.asyncIterator]();
43234
+ let pending2;
43235
+ for (; ; ) {
43236
+ pending2 ??= iterator.next();
43237
+ const tick = deps.sleep(deps.tickMs ?? 1e3).then(() => "tick");
43238
+ let next;
43239
+ try {
43240
+ next = await Promise.race([pending2, tick]);
43241
+ } catch (err2) {
43242
+ if (!(err2 instanceof Error && saysContainerReplaced(err2))) throw err2;
43243
+ containerSaid = err2;
43244
+ break;
43245
+ }
43246
+ if (next === "tick") {
43247
+ check();
43248
+ if (hardStopped) break;
43249
+ continue;
43250
+ }
43251
+ pending2 = void 0;
43252
+ if (next.done) break;
43253
+ const record2 = parseFeedRecord(next.value);
43254
+ if (!record2) continue;
43255
+ const obs = bridge.observe(record2);
43256
+ for (const reply of obs.replies) {
43257
+ let failure2;
43258
+ try {
43259
+ const res = await request2(openCodePermissionReplyRoute(conn.sessionID, reply.requestID), {
43260
+ reply: reply.reply,
43261
+ ...reply.message ? { message: reply.message } : {}
43262
+ });
43263
+ if (res.status < 200 || res.status >= 300) failure2 = { status: res.status };
43264
+ } catch (err2) {
43265
+ failure2 = err2 instanceof Error ? err2 : new Error(String(err2));
43266
+ }
43267
+ if (failure2 !== void 0) {
43268
+ replyFailed = new OpenCodeReplyFailedError(reply.requestID, reply.callId, reply.reply, failure2);
43269
+ note("harness_error", `${replyFailed.message} \u2014 the run is stopped`);
43270
+ void request2(sessionRoutes["session.interrupt"]).catch(() => {
43271
+ });
43272
+ break;
43273
+ }
43274
+ }
43275
+ if (replyFailed) break;
43276
+ if (obs.bypass) {
43277
+ bypass = obs.bypass;
43278
+ void request2(sessionRoutes["session.interrupt"]).catch(() => {
43279
+ });
43280
+ break;
43281
+ }
43282
+ if (obs.budgetStop && !writeUp) {
43283
+ const pace = turnGuardPace(turnCount(), run2.agent.maxMinutes * 6e4 - (deadline - now()));
43284
+ note("turn_budget_exhausted", turnGuardNote(pace));
43285
+ startWriteUp({ kind: "turns", pace }, turnGuardInstruction(pace));
43286
+ }
43287
+ if (obs.providerError !== void 0) providerError = obs.providerError;
43288
+ if (obs.settled) {
43289
+ settled = true;
43290
+ break;
43291
+ }
43292
+ check();
43293
+ if (hardStopped) break;
43294
+ }
43295
+ } finally {
43296
+ transport.close();
43297
+ agentSpan?.end(hardStopped || bypass || replyFailed || containerSaid ? "error" : "ok");
43298
+ }
43299
+ if (containerSaid !== void 0) {
43300
+ const record2 = bridge.record(recordBase, deadline);
43301
+ bridge.closeOpenSpans((open2) => openCodeReplacedCallNote(open2.tool));
43302
+ const now2 = await conn.container.identity().catch(() => void 0);
43303
+ const replaced = new OpenCodeContainerReplacedError(containerSaid.message, conn.containerWord, now2, record2);
43304
+ note("sandbox_restarted", replaced.message);
43305
+ throw replaced;
43306
+ }
43307
+ if (bypass) throw bypass;
43308
+ if (replyFailed) throw replyFailed;
43309
+ if (hardStopped) return { answer: HARD_STOP_MESSAGE };
43310
+ if (providerError !== void 0) throw new Error(`the model call failed: ${providerError}`);
43311
+ if (!settled) throw new Error("the OpenCode run ended before its execution settled");
43312
+ await bridge.flush();
43313
+ const text = bridge.answer() ?? "";
43314
+ const answer = writeUpAnswer(writeUp, text, run2.agent.maxMinutes);
43315
+ return { answer };
43316
+ }
43317
+ function writeUpAnswer(writeUp, text, maxMinutes) {
43318
+ if (writeUp?.kind === "time") return timeBudgetAnswer(text, maxMinutes);
43319
+ if (writeUp?.kind === "turns") return turnGuardAnswer(text, writeUp.pace);
43320
+ return text || "_(no response)_";
43321
+ }
43322
+ var OpenCodeGateBypassedError, OpenCodeReplyFailedError, OpenCodeContainerReplacedError, TOOL_NAME_WORD, isRecord8, str6, TURN_BUDGET_CODE, TURN_BUDGET_REFUSAL, OpenCodeBridge;
43323
+ var init_bridge3 = __esm({
43324
+ "../../src/core/harness/opencode/bridge.ts"() {
43325
+ "use strict";
43326
+ init_runEvents();
43327
+ init_contract4();
43328
+ init_harness();
43329
+ init_bridge2();
43330
+ init_mirror();
43331
+ init_transport();
43332
+ init_toolRules();
43333
+ init_windDown();
43334
+ init_client2();
43335
+ init_dispositions();
43336
+ init_session2();
43337
+ init_process2();
43338
+ OpenCodeGateBypassedError = class extends Error {
43339
+ constructor(detail) {
43340
+ super(`the gate was bypassed: ${detail}`);
43341
+ this.detail = detail;
43342
+ this.name = "OpenCodeGateBypassedError";
43343
+ }
43344
+ detail;
43345
+ };
43346
+ OpenCodeReplyFailedError = class extends Error {
43347
+ constructor(requestID, callId, reply, cause) {
43348
+ super(
43349
+ `the gate's reply (${reply}) for request ${requestID}${callId ? ` (call ${callId})` : ""} could not be posted (${cause instanceof Error ? cause.message : `the server answered ${cause.status}`}): the ask stays unanswered, so the run stops rather than hang`
43350
+ );
43351
+ this.requestID = requestID;
43352
+ this.callId = callId;
43353
+ this.reply = reply;
43354
+ this.cause = cause;
43355
+ this.name = "OpenCodeReplyFailedError";
43356
+ }
43357
+ requestID;
43358
+ callId;
43359
+ reply;
43360
+ cause;
43361
+ };
43362
+ OpenCodeContainerReplacedError = class extends HarnessContainerReplacedError {
43363
+ constructor(said2, was, now, record2) {
43364
+ super(
43365
+ `the container running OpenCode was replaced (${was ?? "unknown"} \u2192 ${now ?? "unknown"}; the executor said: ${redactAndCap(said2.replace(/\s+/g, " ").trim(), 240)})`,
43366
+ said2,
43367
+ was,
43368
+ now,
43369
+ record2
43370
+ );
43371
+ this.name = "OpenCodeContainerReplacedError";
43372
+ }
43373
+ };
43374
+ TOOL_NAME_WORD = { shell: "bash", glob: "find" };
43375
+ isRecord8 = (v) => typeof v === "object" && v !== null;
43376
+ str6 = (v) => typeof v === "string" ? v : String(v ?? "");
43377
+ TURN_BUDGET_CODE = "turn_budget_exhausted";
43378
+ TURN_BUDGET_REFUSAL = new RegExp(`\\b${TURN_BUDGET_CODE}\\b`);
43379
+ OpenCodeBridge = class {
43380
+ constructor(deps) {
43381
+ this.deps = deps;
43382
+ this.mirror = new PiMirror({
43383
+ ...deps.onStep ? { onStep: deps.onStep } : {},
43384
+ seedLength: deps.seedLength,
43385
+ remainingMs: deps.remainingMs
43386
+ });
43387
+ }
43388
+ deps;
43389
+ /** Model turns that did work — the guard's count (an assistant turn per step). */
43390
+ turns = 0;
43391
+ toolCalls = 0;
43392
+ answerText;
43393
+ /** Narration text seen since the last turn's start, emitted beside its call. */
43394
+ pendingNarration;
43395
+ /** callId → the tool name the model gave it (from `session.tool.input.started`). */
43396
+ toolNames = /* @__PURE__ */ new Map();
43397
+ /** The open calls' spans, by callId. */
43398
+ openTools = /* @__PURE__ */ new Map();
43399
+ /** callId → the bot's decision for its ask, `once` or `reject`. A settled
43400
+ * call not among these ran with no decision; a success for a call the bot
43401
+ * rejected ran against the reject. Both are bypasses. */
43402
+ answered = /* @__PURE__ */ new Map();
43403
+ /** requestID → the reply the bot decided, and whether the server has echoed
43404
+ * it once. A `permission.replied` is the bot's own echo only when its
43405
+ * requestID is here, its effect equals the decided one, and it is the first;
43406
+ * anything else — an id never decided, another effect, a second reply — is a
43407
+ * reply the bot did not send (a forgery). */
43408
+ decidedReplies = /* @__PURE__ */ new Map();
43409
+ /** The store messages already mirrored, by count of the pi-shaped turns fed. */
43410
+ fedTurns = 0;
43411
+ mirror;
43412
+ turnCounted = 0;
43413
+ /** The mirror writes, serialized: a refill (or a compaction) chains behind
43414
+ * the last, so two refills that arrive in one poll never race the
43415
+ * high-water mark. `flush` awaits the chain, so a caller reads the answer
43416
+ * and the steps only once every refill it saw has landed. */
43417
+ mirrorChain = Promise.resolve();
43418
+ /** The run's answer as it stands: the last text-only assistant turn. */
43419
+ answer() {
43420
+ return this.answerText;
43421
+ }
43422
+ /** Awaits every mirror write chained so far. */
43423
+ flush() {
43424
+ return this.mirrorChain;
43425
+ }
43426
+ /** The settlement turn a rebuilt process's session starts on (the rebuild is
43427
+ * an import): the calls in flight at the death, each a tool result — the
43428
+ * settlement note — primed as the next step's pending user content, so the
43429
+ * ledger's first step after the rebuild is `[user(settlements), assistant]`
43430
+ * from the seed index, and OpenCode's store rows (the settlement is a
43431
+ * completed tool content in the imported record) project to the same turn.
43432
+ * The record then rebuilds a transcript with a result for every call. */
43433
+ prime(parts) {
43434
+ this.mirror.prime(parts);
43435
+ }
43436
+ /** Settle every call still open when the container went under a running call
43437
+ * (the replaced verdict): each open span ends `error` and its call is put on
43438
+ * the record as a failed `tool_result` carrying `reason` — the restart note,
43439
+ * said of the container — so no span outlives the run and the record holds a
43440
+ * result for every call at the death, exactly as pi's `closeOpenSpans` does. */
43441
+ closeOpenSpans(reason2) {
43442
+ for (const [callId, open2] of this.openTools) {
43443
+ open2.span?.end("error", { callId, ok: false });
43444
+ this.emit({
43445
+ type: "tool_result",
43446
+ tool: open2.tool,
43447
+ ok: false,
43448
+ callId,
43449
+ summary: redactAndCap(reason2({ callId, tool: open2.tool }), COMMAND_CAP)
43450
+ });
43451
+ }
43452
+ this.openTools.clear();
43453
+ }
43454
+ /** The record as this generation holds it (`HarnessRecord`; harness.md item
43455
+ * 6): the base — the seed with its request, or the resumed transcript — and
43456
+ * the mirror's rows, every call of the last turn settled with the replaced
43457
+ * note, so the run loop can relaunch OpenCode from it. Mirrors pi's
43458
+ * `recordNow`. */
43459
+ record(base, deadline) {
43460
+ const written = this.mirror.written;
43461
+ const messages = [...base.messages, ...written.messages];
43462
+ const last = messages.at(-1);
43463
+ const calls = last?.role === "assistant" ? last.content.filter((p) => p.type === "tool_use") : [];
43464
+ return {
43465
+ messages,
43466
+ compactions: [
43467
+ ...base.compactions,
43468
+ ...written.compactions.map((c) => ({ ...c, before: base.messages.length + c.before }))
43469
+ ],
43470
+ settlements: calls.map((toolUse) => ({
43471
+ toolUse,
43472
+ action: "synthetic",
43473
+ text: openCodeReplacedCallNote(toolUse.name)
43474
+ })),
43475
+ turn: this.turns,
43476
+ inboxConsumedSeq: this.mirror.inboxConsumedSeq,
43477
+ deadline
43478
+ };
43479
+ }
43480
+ note(kind, summary) {
43481
+ this.deps.onProgress?.(summary);
43482
+ this.emit({ type: "run_note", kind, summary });
43483
+ }
43484
+ emit(event) {
43485
+ this.deps.emit(event.at === void 0 ? { ...event, at: this.deps.clock() } : event);
43486
+ }
43487
+ observe(record2) {
43488
+ switch (record2.feed) {
43489
+ case "event":
43490
+ return this.onEvent(record2.event);
43491
+ case "permissions":
43492
+ return this.onPermissionsRefill(record2.data);
43493
+ case "messages":
43494
+ return this.onMessagesRefill(record2.data);
43495
+ case "tailer":
43496
+ if (record2.note === "stream closed" || record2.note.includes("failed"))
43497
+ this.deps.onProgress?.(`opencode feed: ${redactAndCap(record2.note, 120)}`);
43498
+ return { replies: [], settled: false };
43499
+ }
43500
+ }
43501
+ onEvent(event) {
43502
+ const out = { replies: [], settled: false };
43503
+ const disposition = openCodeDispositionOf(event.type);
43504
+ if (disposition === void 0) {
43505
+ this.note(
43506
+ "harness_error",
43507
+ `OpenCode emitted an event kind this build does not know: ${redactAndCap(event.type, 80)}`
43508
+ );
43509
+ return out;
43510
+ }
43511
+ if (disposition === "impossible") {
43512
+ this.note("harness_error", `OpenCode emitted ${event.type}, which this run's configuration turns off`);
43513
+ return out;
43514
+ }
43515
+ const data = isRecord8(event.data) ? event.data : {};
43516
+ switch (event.type) {
43517
+ case "session.tool.input.started":
43518
+ if (typeof data.id === "string" && typeof data.name === "string") this.toolNames.set(data.id, data.name);
43519
+ break;
43520
+ case "session.tool.called":
43521
+ this.onToolCalled(data);
43522
+ break;
43523
+ case "session.text.ended":
43524
+ if (typeof data.text === "string" && data.text.trim())
43525
+ this.pendingNarration = this.pendingNarration ? `${this.pendingNarration}
43526
+ ${data.text}` : data.text;
43527
+ break;
43528
+ case "session.step.ended":
43529
+ this.pendingNarration = void 0;
43530
+ break;
43531
+ case "session.tool.success":
43532
+ this.onToolSettled(data, true, out);
43533
+ break;
43534
+ case "session.tool.failed":
43535
+ this.onToolSettled(data, false, out);
43536
+ break;
43537
+ case "permission.asked":
43538
+ this.onPermissionAsked(data, out);
43539
+ break;
43540
+ case "permission.replied":
43541
+ this.onPermissionReplied(data, out);
43542
+ break;
43543
+ case "session.compaction.ended":
43544
+ this.onCompactionEnded(data);
43545
+ break;
43546
+ case "session.compaction.failed":
43547
+ this.note("harness_error", `OpenCode's compaction failed: ${redactAndCap(errorMessage(data.error), 200)}`);
43548
+ break;
43549
+ case "session.retry.scheduled":
43550
+ this.note("harness_error", `OpenCode scheduled a model retry (${redactAndCap(errorMessage(data.error), 200)})`);
43551
+ break;
43552
+ case "session.step.failed":
43553
+ this.note("harness_error", `an OpenCode step failed: ${redactAndCap(errorMessage(data.error), 200)}`);
43554
+ break;
43555
+ case "session.execution.failed": {
43556
+ const error = { status: statusOf(data.error), message: errorMessage(data.error) };
43557
+ if (isBudgetRefusal(error)) out.budgetStop = true;
43558
+ else out.providerError = redactAndCap(error.message, 400);
43559
+ break;
43560
+ }
43561
+ case "session.execution.succeeded":
43562
+ case "session.execution.interrupted":
43563
+ case "session.idle":
43564
+ out.settled = true;
43565
+ break;
43566
+ default:
43567
+ break;
43568
+ }
43569
+ return out;
43570
+ }
43571
+ onToolCalled(data) {
43572
+ const callId = str6(data.id);
43573
+ const name = this.toolNames.get(callId) ?? "tool";
43574
+ const tool = openCodeToolNameWord(name);
43575
+ const span = this.deps.agentSpan?.start(`tool.${tool}`);
43576
+ this.openTools.set(callId, { span, tool });
43577
+ this.toolCalls++;
43578
+ const input = isRecord8(data.input) ? data.input : void 0;
43579
+ if (this.pendingNarration) {
43580
+ this.emit({ type: "assistant", text: redactSecrets(this.pendingNarration) });
43581
+ this.pendingNarration = void 0;
43582
+ }
43583
+ const command2 = tool === "bash" && typeof input?.command === "string" ? { command: redactAndCap(input.command, COMMAND_CAP) } : {};
43584
+ this.emit({
43585
+ type: "tool_call",
43586
+ tool,
43587
+ summary: redactAndCap(describePiToolCall(tool, input)),
43588
+ callId,
43589
+ ...command2,
43590
+ ...span ? { spanId: span.id } : {}
43591
+ });
43592
+ }
43593
+ onToolSettled(data, ok2, out) {
43594
+ const callId = str6(data.id);
43595
+ const open2 = this.openTools.get(callId);
43596
+ this.openTools.delete(callId);
43597
+ const tool = open2?.tool ?? openCodeToolNameWord(this.toolNames.get(callId) ?? "tool");
43598
+ const text = ok2 ? contentText(data.content) : errorMessage(data.error) || contentText(data.content);
43599
+ const failedInText = this.deps.textFailing?.has(tool) === true && toolTextFailed(text);
43600
+ const exit = tool === "bash" ? piBashExit(text, !ok2) : { failed: !ok2 || failedInText };
43601
+ const settledOk = !exit.failed;
43602
+ this.emit({
43603
+ type: "tool_result",
43604
+ tool,
43605
+ ok: settledOk,
43606
+ callId,
43607
+ ...exit.exitCode !== void 0 ? { exitCode: exit.exitCode } : {},
43608
+ ...prepareToolResult(text),
43609
+ ...open2?.span ? { spanId: open2.span.id } : {}
43610
+ });
43611
+ open2?.span?.end(settledOk ? "ok" : "error", { callId, ok: settledOk });
43612
+ if (this.deps.relayedToolNames.has(this.toolNames.get(callId) ?? "")) return;
43613
+ const executed = data.executed === true;
43614
+ const decision = this.answered.get(callId);
43615
+ if (decision === void 0 && !executed) {
43616
+ const name = this.toolNames.get(callId);
43617
+ const absent = name !== void 0 && !openCodeBuiltinToolsFor(this.deps.rules.identity).includes(name);
43618
+ if (absent)
43619
+ this.note(
43620
+ "tool_refused",
43621
+ `${tool} refused: the run's identity has no ${tool} tool (the deny rules removed it), so the call ran nothing`
43622
+ );
43623
+ else
43624
+ this.note(
43625
+ "harness_error",
43626
+ `OpenCode settled ${tool} (call ${callId}) before it ran, with no ask the bot answered${text ? `: ${redactAndCap(text, 200)}` : ""}`
43627
+ );
43628
+ } else if (decision === void 0) {
43629
+ this.bypass(out, `OpenCode ran ${tool} (call ${callId}) with no ask the bot answered \u2014 a call with no ask`);
43630
+ } else if (decision === "reject" && ok2) {
43631
+ this.bypass(
43632
+ out,
43633
+ `OpenCode ran ${tool} (call ${callId}) after the bot's reject \u2014 a success after the bot's refusal`
43634
+ );
43635
+ }
43636
+ }
43637
+ bypass(out, detail) {
43638
+ out.bypass = new OpenCodeGateBypassedError(detail);
43639
+ this.note("harness_error", `${out.bypass.message} \u2014 the run is stopped`);
43640
+ }
43641
+ onPermissionAsked(request2, out) {
43642
+ const callId = request2.source?.id ?? request2.id;
43643
+ if (this.answered.has(callId)) return;
43644
+ const verdict = judgeOpenCodeAsk(
43645
+ request2.action,
43646
+ Array.isArray(request2.resources) ? request2.resources : [],
43647
+ this.deps.rules,
43648
+ this.deps.relayedToolNames
43649
+ );
43650
+ this.answered.set(callId, verdict.reply);
43651
+ this.decidedReplies.set(request2.id, { reply: verdict.reply, echoed: false });
43652
+ if (verdict.reply === "reject")
43653
+ this.note("tool_refused", `${verdict.tool} refused: ${redactAndCap(verdict.message ?? "", 300)}`);
43654
+ out.replies.push({
43655
+ requestID: request2.id,
43656
+ callId,
43657
+ reply: verdict.reply,
43658
+ ...verdict.message ? { message: verdict.message } : {}
43659
+ });
43660
+ }
43661
+ onPermissionReplied(data, out) {
43662
+ const requestID = str6(data.requestID);
43663
+ const reply = str6(data.reply);
43664
+ const decided = this.decidedReplies.get(requestID);
43665
+ if (decided === void 0) {
43666
+ this.bypass(
43667
+ out,
43668
+ `a permission.replied for request ${requestID}, which the bot never decided \u2014 a reply the bot did not send (it raced the bot, or answered an ask the bot never saw)`
43669
+ );
43670
+ return;
43671
+ }
43672
+ if (reply !== decided.reply) {
43673
+ this.bypass(
43674
+ out,
43675
+ `a permission.replied answering \`${reply}\` where the bot decided \`${decided.reply}\` (request ${requestID}) \u2014 a reply that differs from the bot's decision, overriding it`
43676
+ );
43677
+ return;
43678
+ }
43679
+ if (decided.echoed) {
43680
+ this.bypass(out, `a second permission.replied for request ${requestID} \u2014 a duplicate reply the bot did not send`);
43681
+ return;
43682
+ }
43683
+ decided.echoed = true;
43684
+ }
43685
+ onPermissionsRefill(pending2) {
43686
+ const out = { replies: [], settled: false };
43687
+ for (const request2 of pending2) this.onPermissionAsked(request2, out);
43688
+ return out;
43689
+ }
43690
+ onMessagesRefill(messages) {
43691
+ this.mirrorChain = this.mirrorChain.then(() => this.syncMirror(messages));
43692
+ return { replies: [], settled: false };
43693
+ }
43694
+ /** The store's messages as the ledger's steps: the pi-shaped turns projected
43695
+ * from the store (each assistant message split into its assistant turn and a
43696
+ * user turn for its tool results), fed to the mirror past the high-water mark
43697
+ * so a re-refill writes no step twice (an upsert by message id). */
43698
+ async syncMirror(messages) {
43699
+ if (!this.deps.onStep) return;
43700
+ const turns = projectStore(messages, this.deps.seedLength);
43701
+ for (let i = this.fedTurns; i < turns.length; i++) {
43702
+ const turn = turns[i];
43703
+ if (turn.role === "assistant") {
43704
+ const hasTool = turn.content.some((p) => p.type === "tool_use");
43705
+ const text = turn.content.filter((p) => p.type === "text").map((p) => p.text).join("\n").trim();
43706
+ if (hasTool) {
43707
+ this.turns++;
43708
+ } else if (text) {
43709
+ this.answerText = text;
43710
+ }
43711
+ await this.mirror.onMessage(assistantMessage2(turn), ++this.turnCounted);
43712
+ } else {
43713
+ await this.mirror.onMessage(userMessage(turn), this.turnCounted);
43714
+ }
43715
+ }
43716
+ this.fedTurns = turns.length;
43717
+ }
43718
+ onCompactionEnded(data) {
43719
+ const text = typeof data.text === "string" ? data.text : void 0;
43720
+ if (!text) {
43721
+ this.note("compacted", "OpenCode compacted the context; the transcript keeps the originals");
43722
+ return;
43723
+ }
43724
+ const entry = { summary: text };
43725
+ this.note("compacted", `OpenCode compacted the context (${str6(data.reason)}); the transcript keeps the originals`);
43726
+ if (this.deps.onStep)
43727
+ this.mirrorChain = this.mirrorChain.then(() => void this.mirror.onCompaction(entry, this.turnCounted));
43728
+ }
43729
+ };
43730
+ }
43731
+ });
43732
+
43733
+ // ../../src/core/harness/opencode/harness.ts
43734
+ function modelRef2(run2, variant) {
43735
+ return { providerID: run2.model.provider, id: run2.model.id, ...variant ? { variant } : {} };
43736
+ }
43737
+ async function feedEnd(container, conn) {
43738
+ let off = conn.feedOffset;
43739
+ for (; ; ) {
43740
+ const chunk = await container.readLog(conn.paths.feed, off, LOG_READ_BYTES);
43741
+ off += chunk.length;
43742
+ if (chunk.length < LOG_READ_BYTES) return off;
43743
+ }
43744
+ }
43745
+ function resumeOpenCodeFacts(resume) {
43746
+ const facts2 = resume?.facts;
43747
+ return facts2 !== void 0 && facts2.harness === "opencode" ? facts2 : void 0;
43748
+ }
43749
+ async function openOpenCodeRun(deps, run2, settings) {
43750
+ const now = () => deps.clock();
43751
+ const emit = (event) => run2.onEvent?.(event.at === void 0 ? { ...event, at: now() } : event);
43752
+ const note = (kind, summary) => {
43753
+ run2.onProgress?.(summary);
43754
+ emit({ type: "run_note", kind, summary });
43755
+ };
43756
+ const identity2 = run2.agent.identity;
43757
+ const writeUp = {};
43758
+ const live = {
43759
+ runId: run2.runId,
43760
+ tools: run2.tools,
43761
+ toolContext: run2.toolContext,
43762
+ ...run2.backend ? { backend: run2.backend } : {},
43763
+ rules: { ...run2.rules, identity: identity2 },
43764
+ emit,
43765
+ toolSpan: () => void 0,
43766
+ gateSaw: () => {
43767
+ },
43768
+ toolsBlocked: () => writeUp.blocked,
43769
+ callSeen: async () => {
43770
+ },
43771
+ callEnded: async () => {
43772
+ }
43773
+ };
43774
+ const resumeFacts = resumeOpenCodeFacts(run2.resume);
43775
+ const relaunch = run2.resume?.relaunch !== void 0;
43776
+ const forget = relaunch && deps.registry.get(run2.runId) !== void 0 ? deps.registry.replace(live) : deps.registry.register(live);
43777
+ const relayCalls = deps.registry.calls(run2.runId);
43778
+ for (const s of run2.resume?.settlements ?? [])
43779
+ relayCalls?.settle(s.toolUse.id, { content: [{ type: "text", text: openCodeSettlementNote(s) }], isError: true });
43780
+ const paths = openCodeRunPaths(run2.runId);
43781
+ const spec = {
43782
+ runId: run2.runId,
43783
+ paths,
43784
+ model: { id: run2.model.id, providerType: run2.model.providerType, maxTokens: run2.agent.maxTokens },
43785
+ harnessUrl: deps.harnessUrl,
43786
+ identity: identity2,
43787
+ system: run2.system,
43788
+ relayTools: run2.tools.map((t) => t.name),
43789
+ ...settings.compaction ? { compaction: settings.compaction } : {}
43790
+ };
43791
+ let started;
43792
+ let replaced = false;
43793
+ const here = await deps.container.identity().catch(() => void 0);
43794
+ const end = async () => {
43795
+ forget();
43796
+ if (replaced || started === void 0) return;
43797
+ await deps.container.kill(started.pid).catch(() => {
43798
+ });
43799
+ await deps.container.kill(started.tailerPid).catch(() => {
43800
+ });
43801
+ await deps.container.remove(started.paths).catch(() => {
43802
+ });
43803
+ };
43804
+ try {
43805
+ if (relaunch) {
43806
+ const rl = run2.resume?.relaunch;
43807
+ note(
43808
+ "resumed",
43809
+ `relaunched after the container was replaced (${rl?.from ?? "unknown"} \u2192 ${rl?.to ?? here ?? "unknown"}); a fresh server was started on the record with ${Math.round((run2.resume?.remainingMs ?? 0) / 6e4)} min of budget left`
43810
+ );
43811
+ } else if (resumeFacts !== void 0) {
43812
+ const elsewhere = resumeFacts.container !== void 0 && here !== void 0 && resumeFacts.container !== here;
43813
+ if (elsewhere) {
43814
+ note(
43815
+ "resumed",
43816
+ `resumed after a restart: the row's OpenCode (pid ${resumeFacts.pid}) ran in container ${resumeFacts.container}, not the one this run was handed (${here}); it was neither probed nor ended here, and a fresh server was started on the record`
43817
+ );
43818
+ } else if (await serverAnswers(deps.container, resumeFacts)) {
43819
+ note(
43820
+ "resumed",
43821
+ `resumed after a restart: the row's OpenCode (pid ${resumeFacts.pid}) still answers in this container; ended it and its tailer before a fresh start on the record`
43822
+ );
43823
+ await deps.container.kill(resumeFacts.pid).catch(() => {
43824
+ });
43825
+ if (resumeFacts.tailerPid !== void 0) await deps.container.kill(resumeFacts.tailerPid).catch(() => {
43826
+ });
43827
+ await deps.container.remove(openCodeRunPathsAt(resumeFacts.root)).catch(() => {
43828
+ });
43829
+ } else {
43830
+ if (resumeFacts.root !== paths.dir)
43831
+ await deps.container.remove(openCodeRunPathsAt(resumeFacts.root)).catch(() => {
43832
+ });
43833
+ note(
43834
+ "resumed",
43835
+ `resumed after a restart: the row's OpenCode did not answer in this container; a fresh server was started on the record with ${Math.round((run2.resume?.remainingMs ?? 0) / 6e4)} min of budget left`
43836
+ );
43837
+ }
43838
+ }
43839
+ started = await launchOpenCode(
43840
+ {
43841
+ container: deps.container,
43842
+ clock: deps.clock,
43843
+ sleep: deps.sleep,
43844
+ ...deps.pollMs !== void 0 ? { pollMs: deps.pollMs } : {}
43845
+ },
43846
+ spec,
43847
+ deps.bearer
43848
+ );
43849
+ const auth = { Authorization: openCodeAuthHeader(started.password) };
43850
+ const at = now();
43851
+ const cwd = deps.container.cwd(started.paths, run2.rules.checkout);
43852
+ let sessionID;
43853
+ const importInto = async (messages, opts) => {
43854
+ const body = openCodeImportBody(messages, opts);
43855
+ const res = await request(deps.container, started, auth, OPENCODE_ROUTES["session.import"], body);
43856
+ if (res.status < 200 || res.status >= 300)
43857
+ throw new Error(`OpenCode refused the session import (${res.status}): ${redactAndCap(res.body, 200)}`);
43858
+ };
43859
+ if (run2.resume !== void 0 && run2.resume.messages.length > 0) {
43860
+ sessionID = openCodeSessionId(run2.runId);
43861
+ const settlements = new Map(run2.resume.settlements.map((s) => [s.toolUse.id, openCodeSettlementNote(s)]));
43862
+ await importInto(run2.resume.messages, {
43863
+ sessionID,
43864
+ location: { directory: cwd },
43865
+ model: modelRef2(run2, void 0),
43866
+ agent: OPENCODE_AGENT,
43867
+ at,
43868
+ ...run2.resume.compactions ? { compactions: run2.resume.compactions.map((c) => c.entry) } : {},
43869
+ settlements
43870
+ });
43871
+ } else {
43872
+ const { seed } = openCodeSeedAndRequest(run2.messages);
43873
+ if (seed.length > 0) {
43874
+ sessionID = openCodeSessionId(run2.runId);
43875
+ await importInto(seed, {
43876
+ sessionID,
43877
+ location: { directory: cwd },
43878
+ model: modelRef2(run2, void 0),
43879
+ agent: OPENCODE_AGENT,
43880
+ at
43881
+ });
43882
+ } else {
43883
+ sessionID = openCodeSessionId(run2.runId);
43884
+ const res = await request(deps.container, started, auth, OPENCODE_ROUTES["session.create"], {
43885
+ id: sessionID,
43886
+ agent: OPENCODE_AGENT,
43887
+ location: { directory: cwd },
43888
+ model: modelRef2(run2, void 0)
43889
+ });
43890
+ if (res.status < 200 || res.status >= 300)
43891
+ throw new Error(`OpenCode refused the session create (${res.status}): ${redactAndCap(res.body, 200)}`);
43892
+ const created = parseSessionId(res.body);
43893
+ if (created !== void 0) sessionID = created;
43894
+ }
43895
+ }
43896
+ const conn = {
43897
+ container: deps.container,
43898
+ paths: started.paths,
43899
+ port: started.port,
43900
+ password: started.password,
43901
+ sessionID,
43902
+ feedOffset: started.feedOffset,
43903
+ tailerPid: started.tailerPid,
43904
+ ...here !== void 0 ? { containerWord: here } : {},
43905
+ writeUp
43906
+ };
43907
+ run2.saveFacts?.(
43908
+ openCodeFacts(
43909
+ { pid: started.pid, port: started.port, paths: started.paths, tailerPid: started.tailerPid },
43910
+ {
43911
+ sessionID,
43912
+ logOffset: started.feedOffset,
43913
+ bearer: deps.bearer,
43914
+ ...here !== void 0 ? { container: here } : {},
43915
+ relaunches: resumeFacts?.relaunches ?? 0
43916
+ }
43917
+ )
43918
+ );
43919
+ let draining = true;
43920
+ const drainer = drainFollowUps(deps, run2, conn, () => draining, emit, note);
43921
+ let answer;
43922
+ try {
43923
+ ({ answer } = await driveOpenCode(deps, run2, conn));
43924
+ } finally {
43925
+ draining = false;
43926
+ await drainer.catch(() => {
43927
+ });
43928
+ }
43929
+ return {
43930
+ answer,
43931
+ followUp: async (input) => {
43932
+ const turnEnd = await feedEnd(deps.container, conn);
43933
+ const previous = live.toolContext;
43934
+ live.toolContext = input.toolContext;
43935
+ try {
43936
+ const { answer: turnAnswer } = await driveOpenCode(
43937
+ deps,
43938
+ {
43939
+ ...run2,
43940
+ messages: [{ role: "user", content: [{ type: "text", text: input.text }] }],
43941
+ agent: { ...run2.agent, maxTurns: input.maxTurns, maxMinutes: input.maxMinutes },
43942
+ toolContext: input.toolContext,
43943
+ ...input.span ? { span: input.span } : {},
43944
+ resume: void 0,
43945
+ inbox: void 0,
43946
+ // The post-turn is not mirrored to the ledger — the loop's
43947
+ // transcript is the record — but the bridge needs a step sink to
43948
+ // read the answer, so a discard stands in.
43949
+ onStep: async () => {
43950
+ }
43951
+ },
43952
+ { ...conn, feedOffset: turnEnd }
43953
+ );
43954
+ return turnAnswer;
43955
+ } finally {
43956
+ live.toolContext = previous;
43957
+ }
43958
+ },
43959
+ end
43960
+ };
43961
+ } catch (err2) {
43962
+ if (err2 instanceof HarnessContainerReplacedError) replaced = true;
43963
+ await end();
43964
+ throw err2;
43965
+ }
43966
+ }
43967
+ async function serverAnswers(container, facts2) {
43968
+ try {
43969
+ await container.request(openCodeRunPathsAt(facts2.root), {
43970
+ method: "GET",
43971
+ port: facts2.port,
43972
+ path: OPENCODE_ROUTES["health.get"].path
43973
+ });
43974
+ return true;
43975
+ } catch (err2) {
43976
+ if (isContainerGone(err2)) throw err2;
43977
+ return false;
43978
+ }
43979
+ }
43980
+ function drainFollowUps(deps, run2, conn, running, emit, note) {
43981
+ const auth = { Authorization: openCodeAuthHeader(conn.password) };
43982
+ const routes = openCodeSessionRoutes(conn.sessionID);
43983
+ return (async () => {
43984
+ while (running()) {
43985
+ const inputs = run2.inbox?.drain() ?? [];
43986
+ for (const input of inputs) {
43987
+ let stagedLine = "";
43988
+ if (run2.stageFollowUps) {
43989
+ try {
43990
+ stagedLine = await run2.stageFollowUps([input]);
43991
+ } catch (err2) {
43992
+ stagedLine = `Attached files could not be staged: ${err2 instanceof Error ? err2.message : String(err2)}`;
43993
+ note("follow_up", `staging the follow-up's files failed: ${redactSecrets(stagedLine)}`);
43994
+ }
43995
+ }
43996
+ const prompt = followUpPrompt([input]);
43997
+ const text = stagedLine ? `${prompt}
43998
+
43999
+ ${stagedLine}` : prompt;
44000
+ let failure2;
44001
+ try {
44002
+ const res = await conn.container.request(conn.paths, {
44003
+ method: routes["session.prompt"].method,
44004
+ port: conn.port,
44005
+ path: routes["session.prompt"].path,
44006
+ secretHeaders: auth,
44007
+ headers: { "content-type": "application/json" },
44008
+ body: JSON.stringify({ text, delivery: "steer" })
44009
+ });
44010
+ if (res.status < 200 || res.status >= 300) failure2 = `the server answered ${res.status}`;
44011
+ } catch (err2) {
44012
+ failure2 = err2 instanceof Error ? err2.message : String(err2);
44013
+ }
44014
+ if (failure2 !== void 0) {
44015
+ note(
44016
+ "follow_up",
44017
+ `follow-up not delivered \u2014 the steer did not reach the session (${redactAndCap(failure2, 200)}); handed back to the inbox for a fresh turn: ${redactSecrets(followUpSnippet(input))}`
44018
+ );
44019
+ run2.inbox?.requeue(inputs.slice(inputs.indexOf(input)));
44020
+ return;
44021
+ }
44022
+ const source2 = {
44023
+ ...input.sourceUrl ? { url: input.sourceUrl } : {},
44024
+ ...input.userName ? { user: input.userName } : {},
44025
+ ...input.from ? { run: input.from.runId } : {}
44026
+ };
44027
+ emit({
44028
+ type: "input",
44029
+ text: redactSecrets(input.text),
44030
+ messageId: followUpMessageId(input),
44031
+ ...Object.keys(source2).length > 0 ? { source: source2 } : {}
44032
+ });
44033
+ note("follow_up", `follow-up folded in: ${redactSecrets(followUpSnippet(input))}`);
44034
+ }
44035
+ if (!running()) break;
44036
+ await deps.sleep(deps.tickMs ?? 1e3);
44037
+ }
44038
+ })();
44039
+ }
44040
+ function request(container, started, auth, route3, body) {
44041
+ return container.request(started.paths, {
44042
+ method: route3.method,
44043
+ port: started.port,
44044
+ path: route3.path,
44045
+ secretHeaders: auth,
44046
+ ...body !== void 0 ? { headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}
44047
+ });
44048
+ }
44049
+ function parseSessionId(body) {
44050
+ try {
44051
+ const value = JSON.parse(body);
44052
+ return typeof value.data?.id === "string" ? value.data.id : void 0;
44053
+ } catch {
44054
+ return void 0;
44055
+ }
44056
+ }
44057
+ var OpenCodeHarness;
44058
+ var init_harness2 = __esm({
44059
+ "../../src/core/harness/opencode/harness.ts"() {
44060
+ "use strict";
44061
+ init_threadAdmission();
44062
+ init_runEvents();
44063
+ init_container();
44064
+ init_contract4();
44065
+ init_dispositions();
44066
+ init_bridge3();
44067
+ init_client2();
44068
+ init_process2();
44069
+ init_session2();
44070
+ OpenCodeHarness = class {
44071
+ constructor(settings = {}) {
44072
+ this.settings = settings;
44073
+ }
44074
+ settings;
44075
+ name = "opencode";
44076
+ /** OpenCode accepts an imported session as its own history: a seed is
44077
+ * that history written for it, a rebuild a rewrite of it. */
44078
+ history = "authored-session";
44079
+ dispositions = OPENCODE_EVENT_DISPOSITION;
44080
+ /** Switchboard's effort tier in OpenCode's word: the tier selects a model
44081
+ * `variant` on the session's model ref (item 11: variants are declared per
44082
+ * model and a session selects one by id). Stage A's configuration writer
44083
+ * declares no reasoning variants, so there is none to select and the tier is
44084
+ * left to OpenCode's default — the honest answer until a deployment declares
44085
+ * them (a follow-up to the configuration word). */
44086
+ effort(_tier) {
44087
+ return void 0;
44088
+ }
44089
+ /** OpenCode's own tools a run of this identity holds — what the deny rules
44090
+ * leave visible — in the record's shared vocabulary (`shell → bash`,
44091
+ * `glob → find`), so the conformance table, which reads the record, sees the
44092
+ * same words a tool call lands under and across harnesses. */
44093
+ builtinTools(identity2) {
44094
+ return openCodeBuiltinToolsFor(identity2).map(openCodeToolNameWord);
44095
+ }
44096
+ open(deps, run2) {
44097
+ return openOpenCodeRun(deps, run2, this.settings);
44098
+ }
44099
+ /** Where the row's OpenCode is (the survival clause), before any pid is
44100
+ * probed: another harness's facts answer `another-harness` with no command;
44101
+ * a row naming another container than this run was handed is
44102
+ * `another-container` (a pid here is a stranger's); else the recorded port is
44103
+ * probed — any HTTP answer (even the 401 the password-guarded health returns
44104
+ * without the bearer, which `find` does not hold) means the server is up
44105
+ * (`alive-here`), a connection that reaches no server means `dead`. A
44106
+ * container gone under the probe is the typed error, as pi's `find` rethrows. */
44107
+ async find(facts2, container) {
44108
+ if (facts2.harness !== "opencode") return "another-harness";
44109
+ const oc = facts2;
44110
+ const here = await container.identity();
44111
+ if (oc.container !== void 0 && here !== void 0 && oc.container !== here) return "another-container";
44112
+ try {
44113
+ const paths = openCodeRunPathsAt(oc.root);
44114
+ await container.request(paths, { method: "GET", port: oc.port, path: OPENCODE_ROUTES["health.get"].path });
44115
+ return "alive-here";
44116
+ } catch (err2) {
44117
+ if (isContainerGone(err2)) throw err2;
44118
+ return "dead";
44119
+ }
44120
+ }
44121
+ /** End the OpenCode a previous generation left behind — the server and its
44122
+ * tailer at the pids the facts name, the root removed — best-effort, like the
44123
+ * session's end; another harness's facts are left alone. */
44124
+ async end(facts2, container) {
44125
+ if (facts2.harness !== "opencode") return;
44126
+ const oc = facts2;
44127
+ await container.kill(oc.pid).catch(() => {
44128
+ });
44129
+ if (oc.tailerPid !== void 0) await container.kill(oc.tailerPid).catch(() => {
44130
+ });
44131
+ await container.remove(openCodeRunPathsAt(oc.root)).catch(() => {
44132
+ });
44133
+ }
44134
+ };
44135
+ }
44136
+ });
44137
+
41760
44138
  // ../../src/core/runLedger/takeover.ts
41761
44139
  var LedgerTakeover;
41762
44140
  var init_takeover = __esm({
@@ -44894,8 +47272,8 @@ async function handleMcpMessage(identity2, rawBody, deps, options) {
44894
47272
  return { status: 202 };
44895
47273
  }
44896
47274
  const params = typeof msg.params === "object" && msg.params !== null && !Array.isArray(msg.params) ? msg.params : {};
44897
- const request = { id: readId(msg), method: msg.method, params };
44898
- const body = await route2(request, identity2, deps, options, lookup2);
47275
+ const request2 = { id: readId(msg), method: msg.method, params };
47276
+ const body = await route2(request2, identity2, deps, options, lookup2);
44899
47277
  return { status: 200, body };
44900
47278
  }
44901
47279
  function createMcpHandler(deps, options) {
@@ -45132,20 +47510,20 @@ function residentsFleetTone(records) {
45132
47510
  return tones.size === 1 && tones.has("green") ? "green" : "grey";
45133
47511
  }
45134
47512
  function residentSlug(record2) {
45135
- return str6(record2.resource).replace(/^repo:/, "");
47513
+ return str7(record2.resource).replace(/^repo:/, "");
45136
47514
  }
45137
47515
  function residentLive(record2) {
45138
47516
  const live = rec(record2.live);
45139
47517
  if (typeof live.error === "string") return { ...live, state: "unreachable", reason: live.error };
45140
- return { ...live, state: str6(live.state) || "unknown", reason: str6(live.reason) };
47518
+ return { ...live, state: str7(live.state) || "unknown", reason: str7(live.reason) };
45141
47519
  }
45142
- var RESIDENT_SLUG_RE, str6, rec;
47520
+ var RESIDENT_SLUG_RE, str7, rec;
45143
47521
  var init_residentsModel = __esm({
45144
47522
  "../../src/channels/residentsModel.ts"() {
45145
47523
  "use strict";
45146
47524
  init_residentDiskBudget();
45147
47525
  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])?$/;
45148
- str6 = (v) => typeof v === "string" ? v : typeof v === "number" ? String(v) : "";
47526
+ str7 = (v) => typeof v === "string" ? v : typeof v === "number" ? String(v) : "";
45149
47527
  rec = (v) => v && typeof v === "object" ? v : {};
45150
47528
  }
45151
47529
  });
@@ -46200,6 +48578,707 @@ var init_residentsView = __esm({
46200
48578
  }
46201
48579
  });
46202
48580
 
48581
+ // ../../src/channels/accessAuth.ts
48582
+ import { createPublicKey, verify as cryptoVerify } from "node:crypto";
48583
+ function isServiceToken(identity2) {
48584
+ return identity2.sub === "" && typeof identity2.commonName === "string" && identity2.commonName !== "";
48585
+ }
48586
+ function decodeBase64Url(part) {
48587
+ if (part === "" || /[^A-Za-z0-9_-]/.test(part)) return null;
48588
+ return Buffer.from(part, "base64url");
48589
+ }
48590
+ function decodeJsonSegment(part) {
48591
+ const buf2 = decodeBase64Url(part);
48592
+ if (!buf2) return null;
48593
+ try {
48594
+ const parsed = JSON.parse(buf2.toString("utf8"));
48595
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
48596
+ return parsed;
48597
+ } catch {
48598
+ return null;
48599
+ }
48600
+ }
48601
+ async function resolveKey(kid, config, deps) {
48602
+ const cache2 = deps.cache ?? new JwksCache();
48603
+ const cached = cache2.get(kid, deps.now());
48604
+ if (cached) return cached;
48605
+ const ttlMs = (deps.ttlSeconds ?? DEFAULT_JWKS_TTL_SECONDS) * 1e3;
48606
+ const minRefetchIntervalMs = deps.minRefetchIntervalMs ?? DEFAULT_MIN_REFETCH_INTERVAL_MS;
48607
+ const certsUrl = `https://${config.teamDomain}${CERTS_PATH}`;
48608
+ await cache2.refresh(() => deps.fetchJwks(certsUrl), deps.now, ttlMs, minRefetchIntervalMs);
48609
+ return cache2.get(kid, deps.now());
48610
+ }
48611
+ function claimsValid(claims, config, now) {
48612
+ const nowSec = Math.floor(now / 1e3);
48613
+ if (claims.iss !== `https://${config.teamDomain}`) return false;
48614
+ const aud = claims.aud;
48615
+ const audOk = typeof aud === "string" ? aud === config.aud : Array.isArray(aud) && aud.includes(config.aud);
48616
+ if (!audOk) return false;
48617
+ if (typeof claims.exp !== "number" || claims.exp <= nowSec) return false;
48618
+ if (typeof claims.nbf === "number" && claims.nbf > nowSec + SKEW_SECONDS) return false;
48619
+ if (typeof claims.iat === "number" && claims.iat > nowSec + SKEW_SECONDS) return false;
48620
+ return hasSubject(claims);
48621
+ }
48622
+ function hasSubject(claims) {
48623
+ if (typeof claims.sub === "string" && claims.sub !== "") return true;
48624
+ const subEmpty = claims.sub === void 0 || claims.sub === "";
48625
+ return subEmpty && typeof claims.common_name === "string" && claims.common_name !== "";
48626
+ }
48627
+ async function verifyAccessJwt(token2, config, deps) {
48628
+ if (typeof token2 !== "string") return null;
48629
+ const parts = token2.split(".");
48630
+ if (parts.length !== 3) return null;
48631
+ const [rawHeader, rawPayload, rawSignature] = parts;
48632
+ const header4 = decodeJsonSegment(rawHeader);
48633
+ const payload = decodeJsonSegment(rawPayload);
48634
+ if (!header4 || !payload) return null;
48635
+ if (header4.alg !== "RS256") return null;
48636
+ if (typeof header4.kid !== "string" || header4.kid === "") return null;
48637
+ const signature = decodeBase64Url(rawSignature);
48638
+ if (!signature) return null;
48639
+ const jwk = await resolveKey(header4.kid, config, deps);
48640
+ if (!jwk) return null;
48641
+ let signatureOk;
48642
+ try {
48643
+ const key = createPublicKey({ format: "jwk", key: jwk });
48644
+ signatureOk = cryptoVerify("RSA-SHA256", Buffer.from(`${rawHeader}.${rawPayload}`), key, signature);
48645
+ } catch {
48646
+ return null;
48647
+ }
48648
+ if (!signatureOk) return null;
48649
+ if (!claimsValid(payload, config, deps.now())) return null;
48650
+ if (typeof payload.sub !== "string" || payload.sub === "") {
48651
+ return { sub: "", commonName: payload.common_name };
48652
+ }
48653
+ return {
48654
+ sub: payload.sub,
48655
+ email: typeof payload.email === "string" ? payload.email : void 0
48656
+ };
48657
+ }
48658
+ function normalizeTeamDomain(raw) {
48659
+ return raw.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
48660
+ }
48661
+ function parseAccessConfig(env) {
48662
+ const teamDomain = normalizeTeamDomain(env.ACCESS_TEAM_DOMAIN ?? "");
48663
+ const aud = (env.ACCESS_AUD ?? "").trim();
48664
+ if (teamDomain === "" || aud === "") return null;
48665
+ return { teamDomain, aud };
48666
+ }
48667
+ var CERTS_PATH, DEFAULT_JWKS_TTL_SECONDS, DEFAULT_MIN_REFETCH_INTERVAL_MS, SKEW_SECONDS, httpJwksFetcher, JwksCache;
48668
+ var init_accessAuth = __esm({
48669
+ "../../src/channels/accessAuth.ts"() {
48670
+ "use strict";
48671
+ CERTS_PATH = "/cdn-cgi/access/certs";
48672
+ DEFAULT_JWKS_TTL_SECONDS = 3600;
48673
+ DEFAULT_MIN_REFETCH_INTERVAL_MS = 3e4;
48674
+ SKEW_SECONDS = 60;
48675
+ httpJwksFetcher = async (certsUrl) => {
48676
+ const res = await fetch(certsUrl);
48677
+ if (!res.ok) throw new Error(`JWKS fetch failed: HTTP ${res.status}`);
48678
+ const body = await res.json();
48679
+ const keys = body?.keys;
48680
+ if (!Array.isArray(keys)) throw new Error("JWKS response missing keys[]");
48681
+ return keys;
48682
+ };
48683
+ JwksCache = class {
48684
+ entries = /* @__PURE__ */ new Map();
48685
+ /** Ms of the last SUCCESSFUL JWKS fetch, or null before the first one. Drives
48686
+ * the negative-cache interval in `refresh`; a failed fetch does not set it. */
48687
+ lastFetchAt = null;
48688
+ /** The in-flight refresh shared by concurrent callers (single-flight); null
48689
+ * when no fetch is running. It never rejects — `runFetch` swallows failures to
48690
+ * fail closed — so both the owner and any joiners resolve cleanly. */
48691
+ inFlight = null;
48692
+ /** The fresh JWK for `kid`, or null if absent/expired (expired entries are
48693
+ * evicted lazily). `now` is milliseconds. */
48694
+ get(kid, now) {
48695
+ const hit = this.entries.get(kid);
48696
+ if (!hit) return null;
48697
+ if (hit.expiresAt <= now) {
48698
+ this.entries.delete(kid);
48699
+ return null;
48700
+ }
48701
+ return hit.jwk;
48702
+ }
48703
+ /** Store every kid'd key from a freshly fetched JWKS with the given absolute
48704
+ * expiry (ms). Keyless entries are skipped. */
48705
+ put(keys, expiresAt) {
48706
+ for (const jwk of keys) {
48707
+ if (typeof jwk.kid === "string" && jwk.kid !== "") {
48708
+ this.entries.set(jwk.kid, { jwk, expiresAt });
48709
+ }
48710
+ }
48711
+ }
48712
+ /**
48713
+ * Refresh the JWKS to (try to) learn an unknown kid, with two guards against
48714
+ * the unknown-kid amplification vector:
48715
+ * - single-flight: if a refresh is already running, join it instead of
48716
+ * firing another — N concurrent unknown kids ⇒ at most one `fetch` call.
48717
+ * - negative cache: once a fetch has succeeded, another is suppressed until
48718
+ * `minRefetchIntervalMs` has elapsed, so an unknown kid resolves to null
48719
+ * WITHOUT a network round-trip inside that window. A genuinely rotated kid
48720
+ * is still picked up on the first refresh after the interval.
48721
+ * Fail-closed: a fetch failure leaves the store unchanged and does not advance
48722
+ * the last-fetch time. `now` is read lazily (ms) so the injected clock can move
48723
+ * across the await.
48724
+ */
48725
+ async refresh(fetch2, now, ttlMs, minRefetchIntervalMs) {
48726
+ if (this.inFlight) {
48727
+ await this.inFlight;
48728
+ return;
48729
+ }
48730
+ if (this.lastFetchAt !== null && now() - this.lastFetchAt < minRefetchIntervalMs) {
48731
+ return;
48732
+ }
48733
+ this.inFlight = this.runFetch(fetch2, now, ttlMs);
48734
+ try {
48735
+ await this.inFlight;
48736
+ } finally {
48737
+ this.inFlight = null;
48738
+ }
48739
+ }
48740
+ /** Perform one JWKS fetch and fold the result into the store. Never rejects:
48741
+ * on any failure the store is left unchanged (fail closed) and `lastFetchAt`
48742
+ * is not advanced, so `resolveKey` returns null and a later verify may retry. */
48743
+ async runFetch(fetch2, now, ttlMs) {
48744
+ try {
48745
+ const keys = await fetch2();
48746
+ const fetchedAt = now();
48747
+ this.put(keys, fetchedAt + ttlMs);
48748
+ this.lastFetchAt = fetchedAt;
48749
+ } catch {
48750
+ }
48751
+ }
48752
+ };
48753
+ }
48754
+ });
48755
+
48756
+ // ../../src/channels/commandHttp.ts
48757
+ function normalizePath2(pathname) {
48758
+ let decoded = pathname;
48759
+ try {
48760
+ decoded = decodeURIComponent(pathname);
48761
+ } catch {
48762
+ }
48763
+ return decoded.replace(/\/{2,}/g, "/");
48764
+ }
48765
+ function isCommandPath(pathname) {
48766
+ const p = normalizePath2(pathname).toLowerCase();
48767
+ return p === API_ROOT || p === `${API_ROOT}/` || p.startsWith(`${API_ROOT}/`);
48768
+ }
48769
+ function commandIdFromPath(pathname) {
48770
+ const p = normalizePath2(pathname);
48771
+ if (!p.startsWith(`${API_ROOT}/`)) return void 0;
48772
+ const rest = p.slice(API_ROOT.length + 1);
48773
+ return COMMAND_ID.test(rest) ? rest : void 0;
48774
+ }
48775
+ function callerIdFor(identity2) {
48776
+ return isServiceToken(identity2) ? `access:svc:${identity2.commonName}` : `access:${identity2.sub}`;
48777
+ }
48778
+ function accessActor(identity2, grantsFor) {
48779
+ return isServiceToken(identity2) ? resolveActor({ surface: "access-service", subjectId: identity2.commonName }, grantsFor) : resolveActor({ surface: "access-browser", subjectId: identity2.sub }, grantsFor);
48780
+ }
48781
+ function serviceTokenAllowed(pathname, identity2) {
48782
+ return !isServiceToken(identity2) || isCommandPath(pathname);
48783
+ }
48784
+ async function resolveAccessActor(identity2, opts) {
48785
+ const actor = accessActor(identity2, opts.grantsFor);
48786
+ if (isServiceToken(identity2) || !identity2.email || !opts.personByEmail) return actor;
48787
+ const person = await opts.personByEmail(identity2.email).catch(() => void 0);
48788
+ if (!person || !person.id.startsWith("slack:")) return actor;
48789
+ return {
48790
+ ...actor,
48791
+ self: [actor.id, person.id],
48792
+ asUser: { id: person.id, ...person.name ? { name: person.name } : {} }
48793
+ };
48794
+ }
48795
+ async function callerFor(identity2, opts) {
48796
+ return {
48797
+ kind: "access",
48798
+ id: callerIdFor(identity2),
48799
+ actor: await resolveAccessActor(identity2, opts),
48800
+ ...identity2.email && !isServiceToken(identity2) ? { email: identity2.email } : {}
48801
+ };
48802
+ }
48803
+ function hostOf(url) {
48804
+ if (!url) return void 0;
48805
+ try {
48806
+ return new URL(url).host.toLowerCase();
48807
+ } catch {
48808
+ return void 0;
48809
+ }
48810
+ }
48811
+ function header2(req, name) {
48812
+ const v = req.headers[name];
48813
+ return Array.isArray(v) ? v[0] : v;
48814
+ }
48815
+ function originAllowed(req, opts) {
48816
+ const site = header2(req, "sec-fetch-site");
48817
+ if (site !== void 0 && site !== "same-origin" && site !== "none") return false;
48818
+ const origin = header2(req, "origin");
48819
+ if (origin === void 0) return true;
48820
+ const expected = originOf(opts.publicBaseUrl);
48821
+ if (expected !== void 0) return originOf(origin) === expected;
48822
+ const host = header2(req, "host")?.toLowerCase();
48823
+ const actual = hostOf(origin);
48824
+ return host !== void 0 && actual !== void 0 && actual === host;
48825
+ }
48826
+ function originOf(url) {
48827
+ if (!url) return void 0;
48828
+ try {
48829
+ const o = new URL(url).origin;
48830
+ return o === "null" ? void 0 : o.toLowerCase();
48831
+ } catch {
48832
+ return void 0;
48833
+ }
48834
+ }
48835
+ function isJsonContentType(req) {
48836
+ const ct = header2(req, "content-type");
48837
+ return ct !== void 0 && ct.split(";")[0].trim().toLowerCase() === "application/json";
48838
+ }
48839
+ function createCommandHttpHandler(commands, opts) {
48840
+ const maxBytes = opts.maxBodyBytes ?? MAX_BODY_BYTES2;
48841
+ const send = (res, status3, body, extra = {}) => {
48842
+ res.writeHead(status3, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...extra });
48843
+ res.end(JSON.stringify(body));
48844
+ };
48845
+ const refuse3 = (res, status3, code, error, extra = {}) => send(res, status3, { error, code }, extra);
48846
+ return async (req, res, identity2) => {
48847
+ const url = new URL(req.url ?? "/", "http://placeholder.invalid");
48848
+ const id = commandIdFromPath(url.pathname);
48849
+ const cmd = id === void 0 ? void 0 : commands.get(id);
48850
+ if (!cmd || !CommandRegistry.exposedTo(cmd, "access")) {
48851
+ refuse3(res, 404, "not_found", "unknown command");
48852
+ return;
48853
+ }
48854
+ const method = (req.method ?? "GET").toUpperCase();
48855
+ const allow = cmd.effect === "write" ? "POST" : "GET, POST";
48856
+ if (method !== "POST" && (cmd.effect === "write" || method !== "GET")) {
48857
+ refuse3(res, 405, "method_not_allowed", `${cmd.id} accepts ${allow}`, { allow });
48858
+ return;
48859
+ }
48860
+ if (method === "POST" && !isJsonContentType(req)) {
48861
+ refuse3(res, 415, "unsupported_media_type", "content-type must be application/json");
48862
+ return;
48863
+ }
48864
+ if (cmd.effect === "write" && !originAllowed(req, opts)) {
48865
+ refuse3(res, 403, "forbidden_origin", "cross-origin writes are refused");
48866
+ return;
48867
+ }
48868
+ const caller = await callerFor(identity2, opts);
48869
+ if (CommandRegistry.refuses(cmd, caller)) {
48870
+ refuse3(res, ERROR_STATUS.unauthorized, "unauthorized", `${caller.id} is not allowed to run ${cmd.id}`);
48871
+ return;
48872
+ }
48873
+ let named;
48874
+ let keys;
48875
+ if (method === "GET") {
48876
+ named = Object.fromEntries(url.searchParams);
48877
+ keys = "kebab";
48878
+ } else {
48879
+ const read = await readBody(req, maxBytes);
48880
+ if (!read.ok) {
48881
+ refuse3(res, 413, "payload_too_large", "request body too large");
48882
+ req.destroy();
48883
+ return;
48884
+ }
48885
+ let body;
48886
+ try {
48887
+ body = read.body.trim() === "" ? {} : JSON.parse(read.body);
48888
+ } catch {
48889
+ refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
48890
+ return;
48891
+ }
48892
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
48893
+ refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
48894
+ return;
48895
+ }
48896
+ named = body;
48897
+ keys = "camel";
48898
+ }
48899
+ const input = namedToInput(cmd, named, keys);
48900
+ if ("error" in input) {
48901
+ refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", input.error);
48902
+ return;
48903
+ }
48904
+ const result = await commands.invoke(cmd.id, input, caller);
48905
+ if (!result.ok) {
48906
+ refuse3(res, result.status, result.error, result.message);
48907
+ return;
48908
+ }
48909
+ send(res, 200, result.value);
48910
+ };
48911
+ }
48912
+ var API_ROOT;
48913
+ var init_commandHttp = __esm({
48914
+ "../../src/channels/commandHttp.ts"() {
48915
+ "use strict";
48916
+ init_actor();
48917
+ init_commandRegistry();
48918
+ init_commandSurface();
48919
+ init_accessAuth();
48920
+ init_http();
48921
+ API_ROOT = "/api";
48922
+ }
48923
+ });
48924
+
48925
+ // ../../src/channels/web.ts
48926
+ import { randomBytes as randomBytes6 } from "node:crypto";
48927
+ function parseThreadsRoute(pathname) {
48928
+ if (pathname === "/threads" || pathname === "/threads/") return { kind: "new" };
48929
+ const m = /^\/threads\/([^/]+)(\/send)?\/?$/.exec(pathname);
48930
+ if (!m) return null;
48931
+ let id;
48932
+ try {
48933
+ id = decodeURIComponent(m[1]);
48934
+ } catch {
48935
+ return null;
48936
+ }
48937
+ if (!CONVERSATION_ID_RE.test(id) && !THREAD_KEY_RE.test(id)) return null;
48938
+ return m[2] ? { kind: "send", id } : { kind: "thread", id };
48939
+ }
48940
+ function mintConversationId() {
48941
+ return randomBytes6(CONVERSATION_ID_BYTES).toString("hex");
48942
+ }
48943
+ function threadKeyFor(sub, id) {
48944
+ return id.includes(":") ? id : `${PLATFORM5}:${sub}:${id}`;
48945
+ }
48946
+ function ownLane(sub, threadKey) {
48947
+ return threadKey.startsWith(`${PLATFORM5}:${sub}:`);
48948
+ }
48949
+ function conversationIdOf(sub, threadKey) {
48950
+ return ownLane(sub, threadKey) ? threadKey.slice(`${PLATFORM5}:${sub}:`.length) : threadKey;
48951
+ }
48952
+ function requesterOf(actor, identity2) {
48953
+ if (actor.asUser)
48954
+ return {
48955
+ userId: actor.asUser.id,
48956
+ ...actor.asUser.name ? { userName: actor.asUser.name } : {},
48957
+ authenticatedAs: actor.id
48958
+ };
48959
+ return { userId: actor.id, ...identity2.email ? { userName: identity2.email } : {} };
48960
+ }
48961
+ function threadTitle(firstRequest, max = TITLE_MAX2) {
48962
+ const line3 = firstRequest.split(/\r?\n/).find((l) => l.trim() !== "")?.trim() ?? "";
48963
+ if (line3.length <= max) return line3 || "New conversation";
48964
+ return `${line3.slice(0, max - 1).trimEnd()}\u2026`;
48965
+ }
48966
+ function turnOf(view, token2) {
48967
+ const { events, route: _route, ...rest } = view;
48968
+ let request2 = "";
48969
+ let answer;
48970
+ let route3;
48971
+ for (const e of events ?? []) {
48972
+ if (e.type === "input") {
48973
+ if (request2 === "") request2 = e.text;
48974
+ } else if (e.type === "answer") answer = e.text;
48975
+ else if (e.type === "route" && route3 === void 0) route3 = { preset: e.preset, reason: e.reason };
48976
+ }
48977
+ return {
48978
+ ...rest,
48979
+ ...token2 !== void 0 ? { token: token2 } : {},
48980
+ request: request2,
48981
+ ...answer !== void 0 ? { answer } : {},
48982
+ ...route3 !== void 0 ? { route: route3 } : {}
48983
+ };
48984
+ }
48985
+ function historyOf(turns) {
48986
+ const items = [];
48987
+ for (const t of turns) {
48988
+ if (t.request) items.push({ role: "user", text: t.request, at: t.receivedAt ?? t.startedAt });
48989
+ if (t.answer !== void 0 && t.finishedAt !== void 0)
48990
+ items.push({ role: "assistant", text: t.answer, at: t.finishedAt });
48991
+ }
48992
+ return items;
48993
+ }
48994
+ function threadsOf(runs, limit = RAIL_THREADS) {
48995
+ const groups = /* @__PURE__ */ new Map();
48996
+ for (const run2 of runs) {
48997
+ if (!run2.threadKey) continue;
48998
+ const at = run2.finishedAt ?? run2.startedAt;
48999
+ const g = groups.get(run2.threadKey);
49000
+ if (g) {
49001
+ g.runs.push(run2);
49002
+ g.lastAt = Math.max(g.lastAt, at);
49003
+ g.live ||= !run2.finished;
49004
+ } else {
49005
+ groups.set(run2.threadKey, {
49006
+ threadKey: run2.threadKey,
49007
+ surface: platformOf2(run2.channelId ?? run2.threadKey),
49008
+ runs: [run2],
49009
+ lastAt: at,
49010
+ live: !run2.finished
49011
+ });
49012
+ }
49013
+ }
49014
+ const out = [...groups.values()].sort((a, b) => b.lastAt - a.lastAt).slice(0, limit);
49015
+ for (const g of out) g.runs.sort((a, b) => a.startedAt - b.startedAt);
49016
+ return out;
49017
+ }
49018
+ function platformOf2(id) {
49019
+ const colon = id.indexOf(":");
49020
+ return colon > 0 ? id.slice(0, colon) : "unknown";
49021
+ }
49022
+ function paletteCommands(list, actor) {
49023
+ return list.filter((cmd) => cmd.surfaces?.chat !== false).filter((cmd) => authorize(actor, cmd.action, { type: "command", id: cmd.id }).allow).map((cmd) => ({ chat: chatForm(cmd.id), describe: cmd.describe })).sort((a, b) => a.chat.localeCompare(b.chat));
49024
+ }
49025
+ function suggestionsFor(input) {
49026
+ const repo = input.repos[0];
49027
+ const chips = [];
49028
+ if (repo) chips.push(`review the open PR on ${repo}`);
49029
+ else if (input.capabilities.github) chips.push("review a pull request \u2014 paste its link");
49030
+ if (input.failed) chips.push("why did my last run fail?");
49031
+ else if (input.any) chips.push("what did my last run do?");
49032
+ chips.push("what agent and model do I get here?");
49033
+ if (input.capabilities.mcp) chips.push("connect an MCP server");
49034
+ chips.push("What can Switchboard do?");
49035
+ return chips;
49036
+ }
49037
+ function createWebChatHandler(deps) {
49038
+ const now = deps.now ?? systemClock;
49039
+ const mintId = deps.mintId ?? mintConversationId;
49040
+ const warn = deps.warn ?? ((m) => console.warn(`[web] ${m}`));
49041
+ const dispatchFn = deps.dispatch ?? dispatch;
49042
+ const maxBytes = deps.maxBodyBytes ?? MAX_BODY_BYTES2;
49043
+ const retentionDays = deps.retention ? deps.retention.retentionDays : null;
49044
+ const subOf = (actor) => actor.id.replace(/^access:/, "");
49045
+ const liveToken = (run2) => run2.finished ? void 0 : deps.registry.getById(run2.id)?.token;
49046
+ async function turnsOf(threadKey, actor) {
49047
+ const listed = await deps.service.listRuns({
49048
+ status: "all",
49049
+ visibleTo: readableRuns(actor),
49050
+ threadKey,
49051
+ limit: CONVERSATION_RUNS
49052
+ });
49053
+ if (listed.storeUnavailable) warn(`the run store is unavailable: ${threadKey} seeds its live runs only`);
49054
+ const runs = [...listed.runs].sort((a, b) => a.startedAt - b.startedAt);
49055
+ const turns = await Promise.all(
49056
+ runs.map(async (run2) => {
49057
+ const read = await deps.service.getRun(run2.id, { include: "messages" });
49058
+ return turnOf(read.ok ? read.value : { ...run2 }, liveToken(run2));
49059
+ })
49060
+ );
49061
+ return { turns, runs };
49062
+ }
49063
+ async function railOf(actor, sub) {
49064
+ const listed = await deps.service.listRuns({
49065
+ status: "all",
49066
+ visibleTo: allOf([readableRuns(actor), ownedBy(actor)]),
49067
+ limit: RAIL_RUNS
49068
+ });
49069
+ const groups = threadsOf(listed.runs);
49070
+ const rows = await Promise.all(
49071
+ groups.map(async (g) => {
49072
+ const first = g.runs[0];
49073
+ const read = await deps.service.getRun(first.id, { include: "messages" });
49074
+ const request2 = read.ok ? turnOf(read.value).request : "";
49075
+ return {
49076
+ id: conversationIdOf(sub, g.threadKey),
49077
+ title: threadTitle(request2 || first.label || first.id),
49078
+ lastAt: g.lastAt,
49079
+ runs: g.runs.length,
49080
+ live: g.live,
49081
+ surface: g.surface
49082
+ };
49083
+ })
49084
+ );
49085
+ return { rows, runs: listed.runs };
49086
+ }
49087
+ async function seedFor(ctx, conversation, threadKey, open2) {
49088
+ const sub = subOf(ctx.actor);
49089
+ const rail = await railOf(ctx.actor, sub);
49090
+ const mine = rail.runs;
49091
+ const repos = [...new Set(mine.map((r) => r.repo).filter((r) => r !== void 0))];
49092
+ const foreign = !ownLane(sub, threadKey);
49093
+ return {
49094
+ page: "home",
49095
+ conversation,
49096
+ turns: open2.turns,
49097
+ conversations: rail.rows,
49098
+ viewer: { name: ctx.actor.asUser?.name ?? ctx.identity.email ?? sub },
49099
+ sendUrl: `/threads/${encodeURIComponent(conversation)}/send`,
49100
+ ...foreign ? {
49101
+ elsewhere: {
49102
+ surface: platformOf2(threadKey),
49103
+ ...(() => {
49104
+ const url = open2.runs.find((r) => r.sourceUrl)?.sourceUrl;
49105
+ return url ? { url } : {};
49106
+ })()
49107
+ }
49108
+ } : {},
49109
+ now: now(),
49110
+ retentionDays,
49111
+ suggestions: suggestionsFor({
49112
+ repos,
49113
+ failed: mine.some((r) => r.status === "failed"),
49114
+ any: mine.length > 0,
49115
+ capabilities: deps.capabilities
49116
+ }),
49117
+ commands: paletteCommands(deps.commands.list(), ctx.actor)
49118
+ };
49119
+ }
49120
+ async function send(req, res, ctx, id) {
49121
+ const answer = (status3, body) => {
49122
+ res.writeHead(status3, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
49123
+ res.end(JSON.stringify(body));
49124
+ };
49125
+ if (!originAllowed(req, { publicBaseUrl: deps.publicBaseUrl })) {
49126
+ answer(403, { error: "forbidden_origin" });
49127
+ req.destroy();
49128
+ return;
49129
+ }
49130
+ const ct = (Array.isArray(req.headers["content-type"]) ? req.headers["content-type"][0] : req.headers["content-type"])?.split(";")[0].trim().toLowerCase();
49131
+ if (ct !== "application/json") {
49132
+ answer(415, { error: "unsupported_media_type" });
49133
+ req.destroy();
49134
+ return;
49135
+ }
49136
+ const sub = subOf(ctx.actor);
49137
+ const threadKey = threadKeyFor(sub, id);
49138
+ if (!ownLane(sub, threadKey)) {
49139
+ answer(403, { error: "forbidden", detail: `this thread lives on ${platformOf2(threadKey)}; reply there` });
49140
+ req.destroy();
49141
+ return;
49142
+ }
49143
+ const read = await readBody(req, maxBytes);
49144
+ if (!read.ok) {
49145
+ answer(413, { error: "request body too large" });
49146
+ req.destroy();
49147
+ return;
49148
+ }
49149
+ let text;
49150
+ try {
49151
+ const parsed = JSON.parse(read.body);
49152
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.text !== "string" || parsed.text.trim() === "")
49153
+ throw new Error("shape");
49154
+ text = parsed.text;
49155
+ } catch {
49156
+ answer(400, { error: "`text` is required and must be a non-empty string" });
49157
+ return;
49158
+ }
49159
+ const receivedAt = now();
49160
+ const trace = startRequestRoot(deps.core, { channel: "web", receivedAt });
49161
+ const msg = {
49162
+ ...requesterOf(ctx.actor, ctx.identity),
49163
+ channelId: `${PLATFORM5}:${sub}`,
49164
+ threadKey,
49165
+ text,
49166
+ receivedAt
49167
+ };
49168
+ const io = new WebIO(
49169
+ async (exceptRunId) => historyOf((await turnsOf(threadKey, ctx.actor)).turns.filter((t) => t.id !== exceptRunId))
49170
+ );
49171
+ const done = dispatchFn(deps.core, msg, io, { trace }).catch((err2) => {
49172
+ warn(`dispatch: ${err2 instanceof Error ? err2.message : String(err2)}`);
49173
+ });
49174
+ const started = await Promise.race([io.started, done.then(() => void 0)]);
49175
+ const token2 = started ? deps.registry.getById(started.id)?.token : void 0;
49176
+ if (started && token2 !== void 0) {
49177
+ answer(202, {
49178
+ runId: started.id,
49179
+ viewPath: `/runs/${encodeURIComponent(started.id)}?t=${token2}`,
49180
+ threadKey
49181
+ });
49182
+ return;
49183
+ }
49184
+ await done;
49185
+ const run2 = io.run();
49186
+ answer(200, { reply: io.collected(), ...run2 ? { run: run2 } : {} });
49187
+ }
49188
+ return (req, res, ctx) => {
49189
+ const url = new URL(req.url ?? "/", "http://localhost");
49190
+ const route3 = parseThreadsRoute(url.pathname);
49191
+ if (!route3) return false;
49192
+ const method = (req.method ?? "GET").toUpperCase();
49193
+ const plain5 = (status3, body, extra = {}) => {
49194
+ res.writeHead(status3, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", ...extra });
49195
+ res.end(body);
49196
+ };
49197
+ const failed = (err2) => {
49198
+ const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX2);
49199
+ if (res.headersSent) {
49200
+ warn(`render failed after the head was sent: ${reason2}`);
49201
+ res.end();
49202
+ return;
49203
+ }
49204
+ plain5(502, `threads unavailable: ${reason2}`);
49205
+ };
49206
+ if (route3.kind === "send") {
49207
+ if (method !== "POST") {
49208
+ plain5(405, "method not allowed", { allow: "POST" });
49209
+ return true;
49210
+ }
49211
+ send(req, res, ctx, route3.id).catch(failed);
49212
+ return true;
49213
+ }
49214
+ if (method !== "GET") {
49215
+ plain5(405, "method not allowed", { allow: "GET" });
49216
+ return true;
49217
+ }
49218
+ const render3 = (title, seed) => {
49219
+ res.writeHead(200, WEB_HTML_HEADERS);
49220
+ res.end(deps.shell(title, seed));
49221
+ };
49222
+ const sub = subOf(ctx.actor);
49223
+ if (route3.kind === "new") {
49224
+ const conversation = mintId();
49225
+ seedFor(ctx, conversation, threadKeyFor(sub, conversation), { turns: [], runs: [] }).then((seed) => render3("Threads", seed)).catch(failed);
49226
+ return true;
49227
+ }
49228
+ const threadKey = threadKeyFor(sub, route3.id);
49229
+ turnsOf(threadKey, ctx.actor).then(async (open2) => {
49230
+ if (open2.runs.length === 0 && !ownLane(sub, threadKey)) {
49231
+ res.writeHead(404, WEB_HTML_HEADERS);
49232
+ res.end(deps.shell("Run not found", { page: "runNotFound", retentionDays }));
49233
+ return;
49234
+ }
49235
+ const seed = await seedFor(ctx, route3.id, threadKey, open2);
49236
+ const liveCount = open2.runs.filter((r) => !r.finished).length;
49237
+ render3(`${liveCount > 0 ? `(${liveCount}) ` : ""}Threads`, seed);
49238
+ }).catch(failed);
49239
+ return true;
49240
+ };
49241
+ }
49242
+ var PLATFORM5, CONVERSATION_ID_BYTES, CONVERSATION_ID_RE, THREAD_KEY_RE, RAIL_THREADS, CONVERSATION_RUNS, RAIL_RUNS, TITLE_MAX2, UPSTREAM_REASON_MAX2, WebIO;
49243
+ var init_web2 = __esm({
49244
+ "../../src/channels/web.ts"() {
49245
+ "use strict";
49246
+ init_authz();
49247
+ init_commandSurface();
49248
+ init_dispatcher();
49249
+ init_requestTrace();
49250
+ init_clock();
49251
+ init_commandHttp();
49252
+ init_http();
49253
+ init_viewer();
49254
+ init_webShell();
49255
+ PLATFORM5 = "web";
49256
+ CONVERSATION_ID_BYTES = 10;
49257
+ CONVERSATION_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
49258
+ THREAD_KEY_RE = /^[a-z]+:[A-Za-z0-9_.:@+-]{1,200}$/;
49259
+ RAIL_THREADS = 20;
49260
+ CONVERSATION_RUNS = 20;
49261
+ RAIL_RUNS = 100;
49262
+ TITLE_MAX2 = 60;
49263
+ UPSTREAM_REASON_MAX2 = 400;
49264
+ WebIO = class extends HttpIO {
49265
+ constructor(turns) {
49266
+ super();
49267
+ this.turns = turns;
49268
+ }
49269
+ turns;
49270
+ runId;
49271
+ runStarted(started) {
49272
+ this.runId = started.id;
49273
+ super.runStarted(started);
49274
+ }
49275
+ history() {
49276
+ return this.turns(this.runId);
49277
+ }
49278
+ };
49279
+ }
49280
+ });
49281
+
46203
49282
  // ../../src/channels/costsView.ts
46204
49283
  function parseCostsRoute(pathname, search = "") {
46205
49284
  const params = new URLSearchParams(search);
@@ -46269,7 +49348,7 @@ function createCostsViewHandler(service, shell) {
46269
49348
  plain2(res, 503, err2.message, { "retry-after": String(NO_SNAPSHOT_RETRY_AFTER_SECONDS) });
46270
49349
  return;
46271
49350
  }
46272
- const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX2);
49351
+ const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX3);
46273
49352
  plain2(res, 502, `cost report unavailable: ${reason2}`);
46274
49353
  };
46275
49354
  if (route3.kind === "users-json") {
@@ -46299,7 +49378,7 @@ function createCostsViewHandler(service, shell) {
46299
49378
  return true;
46300
49379
  };
46301
49380
  }
46302
- var GROUP_RE, SNAPSHOT_COMMAND, UPSTREAM_REASON_MAX2, NO_SNAPSHOT_RETRY_AFTER_SECONDS, orNone;
49381
+ var GROUP_RE, SNAPSHOT_COMMAND, UPSTREAM_REASON_MAX3, NO_SNAPSHOT_RETRY_AFTER_SECONDS, orNone;
46303
49382
  var init_costsView = __esm({
46304
49383
  "../../src/channels/costsView.ts"() {
46305
49384
  "use strict";
@@ -46309,7 +49388,7 @@ var init_costsView = __esm({
46309
49388
  init_webShell();
46310
49389
  GROUP_RE = /^[a-z0-9][a-z0-9-]{0,39}$/;
46311
49390
  SNAPSHOT_COMMAND = { type: "command", id: "costs.snapshot" };
46312
- UPSTREAM_REASON_MAX2 = 400;
49391
+ UPSTREAM_REASON_MAX3 = 400;
46313
49392
  NO_SNAPSHOT_RETRY_AFTER_SECONDS = 60;
46314
49393
  orNone = (read) => read.catch((err2) => {
46315
49394
  if (err2 instanceof NoCostsSnapshotError) return null;
@@ -46383,13 +49462,13 @@ function createDeliveryViewHandler(deps, shell) {
46383
49462
  res.writeHead(200, WEB_HTML_HEADERS);
46384
49463
  res.end(html);
46385
49464
  }).catch((err2) => {
46386
- const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX3);
49465
+ const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX4);
46387
49466
  plain3(res, 502, `delivery sources unavailable: ${reason2}`);
46388
49467
  });
46389
49468
  return true;
46390
49469
  };
46391
49470
  }
46392
- var SEGMENT, DELIVERY_NO_REPOS_MESSAGE, UPSTREAM_REASON_MAX3;
49471
+ var SEGMENT, DELIVERY_NO_REPOS_MESSAGE, UPSTREAM_REASON_MAX4;
46393
49472
  var init_deliveryView = __esm({
46394
49473
  "../../src/channels/deliveryView.ts"() {
46395
49474
  "use strict";
@@ -46399,7 +49478,7 @@ var init_deliveryView = __esm({
46399
49478
  init_webShell();
46400
49479
  SEGMENT = /^(?!\.+$)[\w.-]{1,100}$/;
46401
49480
  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).";
46402
- UPSTREAM_REASON_MAX3 = 400;
49481
+ UPSTREAM_REASON_MAX4 = 400;
46403
49482
  }
46404
49483
  });
46405
49484
 
@@ -46435,6 +49514,20 @@ function plain4(res, status3, body, extra = {}) {
46435
49514
  res.writeHead(status3, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", ...extra });
46436
49515
  res.end(body);
46437
49516
  }
49517
+ function viewerSettingsView(description) {
49518
+ const strip = (s) => {
49519
+ if (!s) return void 0;
49520
+ const { mcpServers: _servers, ...kept } = s;
49521
+ return kept;
49522
+ };
49523
+ return {
49524
+ effective: description.effective,
49525
+ defaults: description.defaults,
49526
+ restrictedAgents: description.restrictedAgents,
49527
+ user: strip(description.user) ?? {},
49528
+ ...description.org ? { org: strip(description.org) } : {}
49529
+ };
49530
+ }
46438
49531
  function channelScopeView(description) {
46439
49532
  const { user: _user, channel, org, ...rest } = description;
46440
49533
  const strip = (s) => {
@@ -46478,11 +49571,15 @@ function createSettingsViewHandler(deps, shell) {
46478
49571
  return { ...channel ? { channel } : {}, servers, canWrite: write };
46479
49572
  }
46480
49573
  async function channelsSeed(caller, channel) {
46481
- const [indexed, shown] = await Promise.all([
49574
+ const [indexed, mine, shown] = await Promise.all([
46482
49575
  deps.commands.invoke("config.overrides", {}, caller),
49576
+ // The viewer's own settings, always: a settings page never has "no data" (record 0041).
49577
+ deps.commands.invoke("config.show", {}, caller),
46483
49578
  channel ? deps.commands.invoke("config.show", { options: { channel } }, caller) : Promise.resolve(void 0)
46484
49579
  ]);
46485
49580
  const out = indexed.ok ? { index: indexed.value.channels ?? [] } : { index: [], unavailable: failureText(indexed) };
49581
+ if (mine.ok) out.viewer = viewerSettingsView(mine.value);
49582
+ else out.viewerUnavailable = failureText(mine);
46486
49583
  if (channel && shown) {
46487
49584
  const write = canWrite(caller, "config:write", "channel", channel);
46488
49585
  out.selected = shown.ok ? { channelId: channel, scope: channelScopeView(shown.value), canWrite: write } : { channelId: channel, refused: failureText(shown), canWrite: write };
@@ -46499,7 +49596,7 @@ function createSettingsViewHandler(deps, shell) {
46499
49596
  }
46500
49597
  const tab = route3.tab === "home" ? homeTab(deps.capabilities) : route3.tab;
46501
49598
  const failed = (err2) => {
46502
- const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX4);
49599
+ const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX5);
46503
49600
  if (res.headersSent) {
46504
49601
  console.error(`[settings] render failed after the head was sent: ${reason2}`);
46505
49602
  res.end();
@@ -46536,14 +49633,14 @@ function createSettingsViewHandler(deps, shell) {
46536
49633
  return true;
46537
49634
  };
46538
49635
  }
46539
- var CHANNEL_ID_RE, UPSTREAM_REASON_MAX4, failureText;
49636
+ var CHANNEL_ID_RE, UPSTREAM_REASON_MAX5, failureText;
46540
49637
  var init_settingsView = __esm({
46541
49638
  "../../src/channels/settingsView.ts"() {
46542
49639
  "use strict";
46543
49640
  init_authorize();
46544
49641
  init_webShell();
46545
49642
  CHANNEL_ID_RE = /^[a-z]+:[A-Za-z0-9_.:-]{1,80}$/;
46546
- UPSTREAM_REASON_MAX4 = 400;
49643
+ UPSTREAM_REASON_MAX5 = 400;
46547
49644
  failureText = (r) => r.message;
46548
49645
  }
46549
49646
  });
@@ -46821,184 +49918,9 @@ var init_installationSettings = __esm({
46821
49918
  }
46822
49919
  });
46823
49920
 
46824
- // ../../src/channels/accessAuth.ts
46825
- import { createPublicKey, verify as cryptoVerify } from "node:crypto";
46826
- function isServiceToken(identity2) {
46827
- return identity2.sub === "" && typeof identity2.commonName === "string" && identity2.commonName !== "";
46828
- }
46829
- function decodeBase64Url(part) {
46830
- if (part === "" || /[^A-Za-z0-9_-]/.test(part)) return null;
46831
- return Buffer.from(part, "base64url");
46832
- }
46833
- function decodeJsonSegment(part) {
46834
- const buf2 = decodeBase64Url(part);
46835
- if (!buf2) return null;
46836
- try {
46837
- const parsed = JSON.parse(buf2.toString("utf8"));
46838
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
46839
- return parsed;
46840
- } catch {
46841
- return null;
46842
- }
46843
- }
46844
- async function resolveKey(kid, config, deps) {
46845
- const cache2 = deps.cache ?? new JwksCache();
46846
- const cached = cache2.get(kid, deps.now());
46847
- if (cached) return cached;
46848
- const ttlMs = (deps.ttlSeconds ?? DEFAULT_JWKS_TTL_SECONDS) * 1e3;
46849
- const minRefetchIntervalMs = deps.minRefetchIntervalMs ?? DEFAULT_MIN_REFETCH_INTERVAL_MS;
46850
- const certsUrl = `https://${config.teamDomain}${CERTS_PATH}`;
46851
- await cache2.refresh(() => deps.fetchJwks(certsUrl), deps.now, ttlMs, minRefetchIntervalMs);
46852
- return cache2.get(kid, deps.now());
46853
- }
46854
- function claimsValid(claims, config, now) {
46855
- const nowSec = Math.floor(now / 1e3);
46856
- if (claims.iss !== `https://${config.teamDomain}`) return false;
46857
- const aud = claims.aud;
46858
- const audOk = typeof aud === "string" ? aud === config.aud : Array.isArray(aud) && aud.includes(config.aud);
46859
- if (!audOk) return false;
46860
- if (typeof claims.exp !== "number" || claims.exp <= nowSec) return false;
46861
- if (typeof claims.nbf === "number" && claims.nbf > nowSec + SKEW_SECONDS) return false;
46862
- if (typeof claims.iat === "number" && claims.iat > nowSec + SKEW_SECONDS) return false;
46863
- return hasSubject(claims);
46864
- }
46865
- function hasSubject(claims) {
46866
- if (typeof claims.sub === "string" && claims.sub !== "") return true;
46867
- const subEmpty = claims.sub === void 0 || claims.sub === "";
46868
- return subEmpty && typeof claims.common_name === "string" && claims.common_name !== "";
46869
- }
46870
- async function verifyAccessJwt(token2, config, deps) {
46871
- if (typeof token2 !== "string") return null;
46872
- const parts = token2.split(".");
46873
- if (parts.length !== 3) return null;
46874
- const [rawHeader, rawPayload, rawSignature] = parts;
46875
- const header4 = decodeJsonSegment(rawHeader);
46876
- const payload = decodeJsonSegment(rawPayload);
46877
- if (!header4 || !payload) return null;
46878
- if (header4.alg !== "RS256") return null;
46879
- if (typeof header4.kid !== "string" || header4.kid === "") return null;
46880
- const signature = decodeBase64Url(rawSignature);
46881
- if (!signature) return null;
46882
- const jwk = await resolveKey(header4.kid, config, deps);
46883
- if (!jwk) return null;
46884
- let signatureOk;
46885
- try {
46886
- const key = createPublicKey({ format: "jwk", key: jwk });
46887
- signatureOk = cryptoVerify("RSA-SHA256", Buffer.from(`${rawHeader}.${rawPayload}`), key, signature);
46888
- } catch {
46889
- return null;
46890
- }
46891
- if (!signatureOk) return null;
46892
- if (!claimsValid(payload, config, deps.now())) return null;
46893
- if (typeof payload.sub !== "string" || payload.sub === "") {
46894
- return { sub: "", commonName: payload.common_name };
46895
- }
46896
- return {
46897
- sub: payload.sub,
46898
- email: typeof payload.email === "string" ? payload.email : void 0
46899
- };
46900
- }
46901
- function normalizeTeamDomain(raw) {
46902
- return raw.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
46903
- }
46904
- function parseAccessConfig(env) {
46905
- const teamDomain = normalizeTeamDomain(env.ACCESS_TEAM_DOMAIN ?? "");
46906
- const aud = (env.ACCESS_AUD ?? "").trim();
46907
- if (teamDomain === "" || aud === "") return null;
46908
- return { teamDomain, aud };
46909
- }
46910
- var CERTS_PATH, DEFAULT_JWKS_TTL_SECONDS, DEFAULT_MIN_REFETCH_INTERVAL_MS, SKEW_SECONDS, httpJwksFetcher, JwksCache;
46911
- var init_accessAuth = __esm({
46912
- "../../src/channels/accessAuth.ts"() {
46913
- "use strict";
46914
- CERTS_PATH = "/cdn-cgi/access/certs";
46915
- DEFAULT_JWKS_TTL_SECONDS = 3600;
46916
- DEFAULT_MIN_REFETCH_INTERVAL_MS = 3e4;
46917
- SKEW_SECONDS = 60;
46918
- httpJwksFetcher = async (certsUrl) => {
46919
- const res = await fetch(certsUrl);
46920
- if (!res.ok) throw new Error(`JWKS fetch failed: HTTP ${res.status}`);
46921
- const body = await res.json();
46922
- const keys = body?.keys;
46923
- if (!Array.isArray(keys)) throw new Error("JWKS response missing keys[]");
46924
- return keys;
46925
- };
46926
- JwksCache = class {
46927
- entries = /* @__PURE__ */ new Map();
46928
- /** Ms of the last SUCCESSFUL JWKS fetch, or null before the first one. Drives
46929
- * the negative-cache interval in `refresh`; a failed fetch does not set it. */
46930
- lastFetchAt = null;
46931
- /** The in-flight refresh shared by concurrent callers (single-flight); null
46932
- * when no fetch is running. It never rejects — `runFetch` swallows failures to
46933
- * fail closed — so both the owner and any joiners resolve cleanly. */
46934
- inFlight = null;
46935
- /** The fresh JWK for `kid`, or null if absent/expired (expired entries are
46936
- * evicted lazily). `now` is milliseconds. */
46937
- get(kid, now) {
46938
- const hit = this.entries.get(kid);
46939
- if (!hit) return null;
46940
- if (hit.expiresAt <= now) {
46941
- this.entries.delete(kid);
46942
- return null;
46943
- }
46944
- return hit.jwk;
46945
- }
46946
- /** Store every kid'd key from a freshly fetched JWKS with the given absolute
46947
- * expiry (ms). Keyless entries are skipped. */
46948
- put(keys, expiresAt) {
46949
- for (const jwk of keys) {
46950
- if (typeof jwk.kid === "string" && jwk.kid !== "") {
46951
- this.entries.set(jwk.kid, { jwk, expiresAt });
46952
- }
46953
- }
46954
- }
46955
- /**
46956
- * Refresh the JWKS to (try to) learn an unknown kid, with two guards against
46957
- * the unknown-kid amplification vector:
46958
- * - single-flight: if a refresh is already running, join it instead of
46959
- * firing another — N concurrent unknown kids ⇒ at most one `fetch` call.
46960
- * - negative cache: once a fetch has succeeded, another is suppressed until
46961
- * `minRefetchIntervalMs` has elapsed, so an unknown kid resolves to null
46962
- * WITHOUT a network round-trip inside that window. A genuinely rotated kid
46963
- * is still picked up on the first refresh after the interval.
46964
- * Fail-closed: a fetch failure leaves the store unchanged and does not advance
46965
- * the last-fetch time. `now` is read lazily (ms) so the injected clock can move
46966
- * across the await.
46967
- */
46968
- async refresh(fetch2, now, ttlMs, minRefetchIntervalMs) {
46969
- if (this.inFlight) {
46970
- await this.inFlight;
46971
- return;
46972
- }
46973
- if (this.lastFetchAt !== null && now() - this.lastFetchAt < minRefetchIntervalMs) {
46974
- return;
46975
- }
46976
- this.inFlight = this.runFetch(fetch2, now, ttlMs);
46977
- try {
46978
- await this.inFlight;
46979
- } finally {
46980
- this.inFlight = null;
46981
- }
46982
- }
46983
- /** Perform one JWKS fetch and fold the result into the store. Never rejects:
46984
- * on any failure the store is left unchanged (fail closed) and `lastFetchAt`
46985
- * is not advanced, so `resolveKey` returns null and a later verify may retry. */
46986
- async runFetch(fetch2, now, ttlMs) {
46987
- try {
46988
- const keys = await fetch2();
46989
- const fetchedAt = now();
46990
- this.put(keys, fetchedAt + ttlMs);
46991
- this.lastFetchAt = fetchedAt;
46992
- } catch {
46993
- }
46994
- }
46995
- };
46996
- }
46997
- });
46998
-
46999
49921
  // ../../src/channels/dashboardAuth.ts
47000
49922
  import { createHash as createHash5, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
47001
- function header2(headers, name) {
49923
+ function header3(headers, name) {
47002
49924
  const raw = headers[name];
47003
49925
  return Array.isArray(raw) ? raw[0] : raw;
47004
49926
  }
@@ -47007,7 +49929,7 @@ function accessVerifier(config, deps) {
47007
49929
  mode: "access",
47008
49930
  describe: () => `access (Cloudflare Access SSO, ${config.teamDomain})`,
47009
49931
  async verify(req) {
47010
- const token2 = header2(req.headers, "cf-access-jwt-assertion");
49932
+ const token2 = header3(req.headers, "cf-access-jwt-assertion");
47011
49933
  if (!token2) return FORBIDDEN;
47012
49934
  const identity2 = await verifyAccessJwt(token2, config, deps);
47013
49935
  return identity2 ? { ok: true, identity: identity2 } : FORBIDDEN;
@@ -47024,7 +49946,7 @@ function tokenVerifier(opts) {
47024
49946
  mode: "token",
47025
49947
  describe: () => `token (bearer from $${opts.env} \u2192 access:${opts.subject})`,
47026
49948
  async verify(req) {
47027
- const auth = header2(req.headers, "authorization");
49949
+ const auth = header3(req.headers, "authorization");
47028
49950
  if (!auth) return FORBIDDEN;
47029
49951
  const m = /^Bearer\s+(\S+)\s*$/i.exec(auth);
47030
49952
  if (!m) return FORBIDDEN;
@@ -47205,7 +50127,7 @@ async function start2(svc, nonce, identity2, res) {
47205
50127
  );
47206
50128
  return page(res, statusFor(result.refusal.kind), "Connect link unavailable", `<p class="err">${esc(message4)}</p>`);
47207
50129
  }
47208
- const host = hostOf(result.redirectUrl);
50130
+ const host = hostOf2(result.redirectUrl);
47209
50131
  page(
47210
50132
  res,
47211
50133
  200,
@@ -47316,12 +50238,12 @@ function readForm(req) {
47316
50238
  }
47317
50239
  function entryForm(server, nonce) {
47318
50240
  if (server.auth === "oauth") {
47319
- const host = hostOf(server.url);
50241
+ const host = hostOf2(server.url);
47320
50242
  return `<p>Connect <strong>${esc(server.name)}</strong> (<code>${esc(server.url)}</code>) by signing in with ${esc(host)}. You will be sent to its sign-in page and back here; the resulting credential is encrypted and stored for your runs only \u2014 it never appears in Slack, logs, or run records.</p><form method="post" action="${MCP_CONNECT_PREFIX}${esc(nonce)}"><input type="hidden" name="action" value="start"><button type="submit">Continue to ${esc(host)}</button></form>`;
47321
50243
  }
47322
50244
  return `<p>Paste the API token for <strong>${esc(server.name)}</strong> (<code>${esc(server.url)}</code>). It is encrypted and stored for your runs only; it never appears in Slack, logs, or run records.</p><form method="post" action="${MCP_CONNECT_PREFIX}${esc(nonce)}"><label for="token">Token</label><input id="token" name="token" type="password" autocomplete="off" required maxlength="8192"><button type="submit">Connect</button></form>`;
47323
50245
  }
47324
- function hostOf(url) {
50246
+ function hostOf2(url) {
47325
50247
  try {
47326
50248
  return new URL(url).host;
47327
50249
  } catch {
@@ -48078,8 +51000,8 @@ async function generatedUnitOf(instance, unit, readers) {
48078
51000
  const url = resume.url ?? `https://github.com/${instance.repo}/pull/${resume.pr}`;
48079
51001
  return generatedUnit(unit.unit, `Resume the review loop of ${url}`);
48080
51002
  }
48081
- const request = await readers.readShipRequest();
48082
- const task = request !== void 0 ? shipTaskText(parseDirectives(request).text, instance.repo) : "";
51003
+ const request2 = await readers.readShipRequest();
51004
+ const task = request2 !== void 0 ? shipTaskText(parseDirectives(request2).text, instance.repo) : "";
48083
51005
  return generatedUnit(unit.unit, task || "Implement the task this thread's ship request describes.");
48084
51006
  }
48085
51007
  async function contractFor(instance, unit, readers) {
@@ -49033,20 +51955,24 @@ async function merge(body, deps, subject2) {
49033
51955
  at
49034
51956
  });
49035
51957
  if (checks.failed.length > 0) return refused6(`CI is red at \`${headSha.slice(0, 7)}\`: ${checks.failed.join(", ")}`);
49036
- if (checks.total === 0)
51958
+ if (checks.total === 0) {
51959
+ deps.noteMergeWait?.(headSha, id.value, at);
49037
51960
  return json2(200, {
49038
51961
  ok: true,
49039
51962
  outcome: "pending",
49040
51963
  reason: `no check has reported at \`${headSha.slice(0, 7)}\` yet`,
49041
51964
  at
49042
51965
  });
49043
- if (checks.pending.length > 0)
51966
+ }
51967
+ if (checks.pending.length > 0) {
51968
+ deps.noteMergeWait?.(headSha, id.value, at);
49044
51969
  return json2(200, {
49045
51970
  ok: true,
49046
51971
  outcome: "pending",
49047
51972
  reason: `${checks.pending.length} check(s) still running at \`${headSha.slice(0, 7)}\`: ${checks.pending.join(", ")}`,
49048
51973
  at
49049
51974
  });
51975
+ }
49050
51976
  let merged;
49051
51977
  try {
49052
51978
  merged = await deps.mergePullRequest(pr, {
@@ -49309,6 +52235,109 @@ var init_adminCoordinator = __esm({
49309
52235
  }
49310
52236
  });
49311
52237
 
52238
+ // ../../src/core/coordinator/checksIntake.ts
52239
+ async function verifyWebhookSignature(secret2, rawBody, signature) {
52240
+ const key = await crypto.subtle.importKey("raw", encoder2.encode(secret2), { name: "HMAC", hash: "SHA-256" }, false, [
52241
+ "sign"
52242
+ ]);
52243
+ const digest = new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder2.encode(rawBody)));
52244
+ const expected = `sha256=${toHex(digest)}`;
52245
+ if (signature.length !== expected.length) return false;
52246
+ let diff = 0;
52247
+ for (let i = 0; i < expected.length; i++) diff |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
52248
+ return diff === 0;
52249
+ }
52250
+ function createMergeWaitRegistry(ttlMs = MERGE_WAIT_TTL_MS) {
52251
+ const byHead = /* @__PURE__ */ new Map();
52252
+ return {
52253
+ note(headSha, instanceId, now) {
52254
+ const entry = byHead.get(headSha) ?? /* @__PURE__ */ new Map();
52255
+ entry.set(instanceId, now);
52256
+ byHead.set(headSha, entry);
52257
+ },
52258
+ waitingAt(headSha, now) {
52259
+ const entry = byHead.get(headSha);
52260
+ if (!entry) return [];
52261
+ const alive = [];
52262
+ for (const [id, at] of entry) {
52263
+ if (now - at > ttlMs) entry.delete(id);
52264
+ else alive.push(id);
52265
+ }
52266
+ if (entry.size === 0) byHead.delete(headSha);
52267
+ return alive;
52268
+ }
52269
+ };
52270
+ }
52271
+ async function handleCheckRunIntake(headers, rawBody, deps) {
52272
+ if (deps.secret === void 0 || deps.secret === "") return { status: 503, body: { error: "disabled" } };
52273
+ if (headers.signature === void 0 || !await verifyWebhookSignature(deps.secret, rawBody, headers.signature))
52274
+ return { status: 401, body: { error: "unauthorized" } };
52275
+ if (headers.event !== "check_run") return { status: 200, body: { ok: true, ignored: "event" } };
52276
+ let payload;
52277
+ try {
52278
+ payload = JSON.parse(rawBody);
52279
+ } catch {
52280
+ return { status: 400, body: { error: "invalid_json" } };
52281
+ }
52282
+ if (typeof payload !== "object" || payload === null) return { status: 400, body: { error: "invalid_body" } };
52283
+ const p = payload;
52284
+ if (p.action !== "completed") return { status: 200, body: { ok: true, ignored: "action" } };
52285
+ const headSha = p.check_run?.head_sha;
52286
+ const repo = p.repository?.full_name;
52287
+ if (typeof headSha !== "string" || headSha === "" || typeof repo !== "string" || repo === "")
52288
+ return { status: 400, body: { error: "invalid_body" } };
52289
+ if (!await deps.checksSettled(repo, headSha)) return { status: 200, body: { ok: true, settled: false } };
52290
+ const instances = await deps.instancesWaitingAt(headSha);
52291
+ const sends = [];
52292
+ for (const instance of instances) sends.push(await sendChecksSettled(deps.workflow, instance, headSha, deps.now()));
52293
+ return {
52294
+ status: 200,
52295
+ body: { ok: true, settled: true, sent: sends.filter((s) => s.kind === "sent").length, of: instances.length }
52296
+ };
52297
+ }
52298
+ var GITHUB_SIGNATURE_HEADER, GITHUB_EVENT_HEADER, encoder2, toHex, MERGE_WAIT_TTL_MS;
52299
+ var init_checksIntake = __esm({
52300
+ "../../src/core/coordinator/checksIntake.ts"() {
52301
+ "use strict";
52302
+ init_contract2();
52303
+ GITHUB_SIGNATURE_HEADER = "x-hub-signature-256";
52304
+ GITHUB_EVENT_HEADER = "x-github-event";
52305
+ encoder2 = new TextEncoder();
52306
+ toHex = (bytes) => [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
52307
+ MERGE_WAIT_TTL_MS = 60 * 6e4;
52308
+ }
52309
+ });
52310
+
52311
+ // ../../src/channels/githubWebhook.ts
52312
+ function createGithubWebhookHandler(deps) {
52313
+ return async (req, res) => {
52314
+ const answer = (status3, body2) => {
52315
+ res.writeHead(status3, { "content-type": "application/json" });
52316
+ res.end(JSON.stringify(body2));
52317
+ };
52318
+ if (req.method !== "POST") return answer(405, { error: "method_not_allowed" });
52319
+ const body = await readBody(req, MAX_BODY_BYTES4);
52320
+ if (!body.ok) return answer(413, { error: "too_large" });
52321
+ const result = await handleCheckRunIntake(
52322
+ { event: one(req.headers[GITHUB_EVENT_HEADER]), signature: one(req.headers[GITHUB_SIGNATURE_HEADER]) },
52323
+ body.body,
52324
+ deps
52325
+ );
52326
+ answer(result.status, result.body);
52327
+ };
52328
+ }
52329
+ var GITHUB_WEBHOOK_PATH, MAX_BODY_BYTES4, one;
52330
+ var init_githubWebhook = __esm({
52331
+ "../../src/channels/githubWebhook.ts"() {
52332
+ "use strict";
52333
+ init_checksIntake();
52334
+ init_http();
52335
+ GITHUB_WEBHOOK_PATH = "/webhooks/github";
52336
+ MAX_BODY_BYTES4 = 1024 * 1024;
52337
+ one = (v) => Array.isArray(v) ? v[0] : v;
52338
+ }
52339
+ });
52340
+
49312
52341
  // ../../src/core/scheduleStore.ts
49313
52342
  function buildScheduleStore(cfg, secrets, warn) {
49314
52343
  const worker = cfg?.worker;
@@ -49375,175 +52404,6 @@ var init_scheduleStore = __esm({
49375
52404
  }
49376
52405
  });
49377
52406
 
49378
- // ../../src/channels/commandHttp.ts
49379
- function normalizePath2(pathname) {
49380
- let decoded = pathname;
49381
- try {
49382
- decoded = decodeURIComponent(pathname);
49383
- } catch {
49384
- }
49385
- return decoded.replace(/\/{2,}/g, "/");
49386
- }
49387
- function isCommandPath(pathname) {
49388
- const p = normalizePath2(pathname).toLowerCase();
49389
- return p === API_ROOT || p === `${API_ROOT}/` || p.startsWith(`${API_ROOT}/`);
49390
- }
49391
- function commandIdFromPath(pathname) {
49392
- const p = normalizePath2(pathname);
49393
- if (!p.startsWith(`${API_ROOT}/`)) return void 0;
49394
- const rest = p.slice(API_ROOT.length + 1);
49395
- return COMMAND_ID.test(rest) ? rest : void 0;
49396
- }
49397
- function callerIdFor(identity2) {
49398
- return isServiceToken(identity2) ? `access:svc:${identity2.commonName}` : `access:${identity2.sub}`;
49399
- }
49400
- function accessActor(identity2, grantsFor) {
49401
- return isServiceToken(identity2) ? resolveActor({ surface: "access-service", subjectId: identity2.commonName }, grantsFor) : resolveActor({ surface: "access-browser", subjectId: identity2.sub }, grantsFor);
49402
- }
49403
- function serviceTokenAllowed(pathname, identity2) {
49404
- return !isServiceToken(identity2) || isCommandPath(pathname);
49405
- }
49406
- async function resolveAccessActor(identity2, opts) {
49407
- const actor = accessActor(identity2, opts.grantsFor);
49408
- if (isServiceToken(identity2) || !identity2.email || !opts.personByEmail) return actor;
49409
- const person = await opts.personByEmail(identity2.email).catch(() => void 0);
49410
- if (!person || !person.id.startsWith("slack:")) return actor;
49411
- return {
49412
- ...actor,
49413
- self: [actor.id, person.id],
49414
- asUser: { id: person.id, ...person.name ? { name: person.name } : {} }
49415
- };
49416
- }
49417
- async function callerFor(identity2, opts) {
49418
- return {
49419
- kind: "access",
49420
- id: callerIdFor(identity2),
49421
- actor: await resolveAccessActor(identity2, opts),
49422
- ...identity2.email && !isServiceToken(identity2) ? { email: identity2.email } : {}
49423
- };
49424
- }
49425
- function hostOf2(url) {
49426
- if (!url) return void 0;
49427
- try {
49428
- return new URL(url).host.toLowerCase();
49429
- } catch {
49430
- return void 0;
49431
- }
49432
- }
49433
- function header3(req, name) {
49434
- const v = req.headers[name];
49435
- return Array.isArray(v) ? v[0] : v;
49436
- }
49437
- function originAllowed(req, opts) {
49438
- const site = header3(req, "sec-fetch-site");
49439
- if (site !== void 0 && site !== "same-origin" && site !== "none") return false;
49440
- const origin = header3(req, "origin");
49441
- if (origin === void 0) return true;
49442
- const expected = originOf(opts.publicBaseUrl);
49443
- if (expected !== void 0) return originOf(origin) === expected;
49444
- const host = header3(req, "host")?.toLowerCase();
49445
- const actual = hostOf2(origin);
49446
- return host !== void 0 && actual !== void 0 && actual === host;
49447
- }
49448
- function originOf(url) {
49449
- if (!url) return void 0;
49450
- try {
49451
- const o = new URL(url).origin;
49452
- return o === "null" ? void 0 : o.toLowerCase();
49453
- } catch {
49454
- return void 0;
49455
- }
49456
- }
49457
- function isJsonContentType(req) {
49458
- const ct = header3(req, "content-type");
49459
- return ct !== void 0 && ct.split(";")[0].trim().toLowerCase() === "application/json";
49460
- }
49461
- function createCommandHttpHandler(commands, opts) {
49462
- const maxBytes = opts.maxBodyBytes ?? MAX_BODY_BYTES2;
49463
- const send = (res, status3, body, extra = {}) => {
49464
- res.writeHead(status3, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...extra });
49465
- res.end(JSON.stringify(body));
49466
- };
49467
- const refuse3 = (res, status3, code, error, extra = {}) => send(res, status3, { error, code }, extra);
49468
- return async (req, res, identity2) => {
49469
- const url = new URL(req.url ?? "/", "http://placeholder.invalid");
49470
- const id = commandIdFromPath(url.pathname);
49471
- const cmd = id === void 0 ? void 0 : commands.get(id);
49472
- if (!cmd || !CommandRegistry.exposedTo(cmd, "access")) {
49473
- refuse3(res, 404, "not_found", "unknown command");
49474
- return;
49475
- }
49476
- const method = (req.method ?? "GET").toUpperCase();
49477
- const allow = cmd.effect === "write" ? "POST" : "GET, POST";
49478
- if (method !== "POST" && (cmd.effect === "write" || method !== "GET")) {
49479
- refuse3(res, 405, "method_not_allowed", `${cmd.id} accepts ${allow}`, { allow });
49480
- return;
49481
- }
49482
- if (method === "POST" && !isJsonContentType(req)) {
49483
- refuse3(res, 415, "unsupported_media_type", "content-type must be application/json");
49484
- return;
49485
- }
49486
- if (cmd.effect === "write" && !originAllowed(req, opts)) {
49487
- refuse3(res, 403, "forbidden_origin", "cross-origin writes are refused");
49488
- return;
49489
- }
49490
- const caller = await callerFor(identity2, opts);
49491
- if (CommandRegistry.refuses(cmd, caller)) {
49492
- refuse3(res, ERROR_STATUS.unauthorized, "unauthorized", `${caller.id} is not allowed to run ${cmd.id}`);
49493
- return;
49494
- }
49495
- let named;
49496
- let keys;
49497
- if (method === "GET") {
49498
- named = Object.fromEntries(url.searchParams);
49499
- keys = "kebab";
49500
- } else {
49501
- const read = await readBody(req, maxBytes);
49502
- if (!read.ok) {
49503
- refuse3(res, 413, "payload_too_large", "request body too large");
49504
- req.destroy();
49505
- return;
49506
- }
49507
- let body;
49508
- try {
49509
- body = read.body.trim() === "" ? {} : JSON.parse(read.body);
49510
- } catch {
49511
- refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
49512
- return;
49513
- }
49514
- if (typeof body !== "object" || body === null || Array.isArray(body)) {
49515
- refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
49516
- return;
49517
- }
49518
- named = body;
49519
- keys = "camel";
49520
- }
49521
- const input = namedToInput(cmd, named, keys);
49522
- if ("error" in input) {
49523
- refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", input.error);
49524
- return;
49525
- }
49526
- const result = await commands.invoke(cmd.id, input, caller);
49527
- if (!result.ok) {
49528
- refuse3(res, result.status, result.error, result.message);
49529
- return;
49530
- }
49531
- send(res, 200, result.value);
49532
- };
49533
- }
49534
- var API_ROOT;
49535
- var init_commandHttp = __esm({
49536
- "../../src/channels/commandHttp.ts"() {
49537
- "use strict";
49538
- init_actor();
49539
- init_commandRegistry();
49540
- init_commandSurface();
49541
- init_accessAuth();
49542
- init_http();
49543
- API_ROOT = "/api";
49544
- }
49545
- });
49546
-
49547
52407
  // ../../src/index.ts
49548
52408
  var index_exports = {};
49549
52409
  __export(index_exports, {
@@ -49683,13 +52543,19 @@ async function runBot() {
49683
52543
  const residentFleet = fleetWatcher ?? NO_FLEET;
49684
52544
  const runBearers = new RunBearerStore({ clock: systemClock });
49685
52545
  const harnesses = new HarnessRegistry();
52546
+ const roster = {
52547
+ pi: new PiHarness(config.config.pi?.compaction ? { compaction: config.config.pi.compaction } : {}),
52548
+ opencode: new OpenCodeHarness(
52549
+ config.config.opencode?.compaction ? { compaction: config.config.opencode.compaction } : {}
52550
+ )
52551
+ };
49686
52552
  const deps = {
49687
52553
  config,
49688
52554
  completions,
49689
52555
  spanLog,
49690
52556
  runBearers,
49691
52557
  harness: {
49692
- harness: new PiHarness(config.config.pi?.compaction ? { compaction: config.config.pi.compaction } : {}),
52558
+ harnesses: roster,
49693
52559
  registry: harnesses,
49694
52560
  ...process.env.PUBLIC_BASE_URL ? { harnessUrl: process.env.PUBLIC_BASE_URL } : {},
49695
52561
  ...process.env.PORT ? { loopbackUrl: `http://127.0.0.1:${process.env.PORT}` } : {}
@@ -49806,6 +52672,21 @@ async function runBot() {
49806
52672
  clock: systemClock
49807
52673
  });
49808
52674
  const harnessRoutes = createHarnessRoutesHandler({ bearers: runBearers, harnesses, takeover });
52675
+ const mergeWaits = createMergeWaitRegistry();
52676
+ const checksWorkflow = shimWorkflowSender({
52677
+ baseUrl: process.env.PUBLIC_BASE_URL,
52678
+ tokens: processSecrets.get("SWITCHBOARD_INGRESS_TOKENS")
52679
+ });
52680
+ const githubWebhook = createGithubWebhookHandler({
52681
+ secret: processSecrets.get("GITHUB_WEBHOOK_SECRET")?.reveal(),
52682
+ checksSettled: async (repo, headSha) => {
52683
+ const checks = await fetchCommitChecks(repo, headSha);
52684
+ return checks !== void 0 && checks.total > 0 && checks.pending.length === 0;
52685
+ },
52686
+ instancesWaitingAt: (headSha) => Promise.resolve(mergeWaits.waitingAt(headSha, systemClock())),
52687
+ workflow: checksWorkflow,
52688
+ now: systemClock
52689
+ });
49809
52690
  const coordinatorAdmin = createAdminCoordinatorHandler({
49810
52691
  tokens: processSecrets.get("SWITCHBOARD_INGRESS_TOKENS"),
49811
52692
  grantsFor: (id) => config.grantsFor(id),
@@ -49830,7 +52711,8 @@ async function runBot() {
49830
52711
  mergePullRequest,
49831
52712
  selfIdentity: resolveGithubIdentity,
49832
52713
  runHistoryWriter,
49833
- channelVisibilityOf: (channelId) => channelVisibilityOf(deps, channelId)
52714
+ channelVisibilityOf: (channelId) => channelVisibilityOf(deps, channelId),
52715
+ noteMergeWait: (headSha, instanceId, at) => mergeWaits.note(headSha, instanceId, at)
49834
52716
  });
49835
52717
  const cronArmed = Object.values(auth.tokens).some((id) => id.subject === "cron");
49836
52718
  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"})`;
@@ -49861,6 +52743,16 @@ async function runBot() {
49861
52743
  },
49862
52744
  shell
49863
52745
  );
52746
+ const webChat = createWebChatHandler({
52747
+ core: deps,
52748
+ service: runsService,
52749
+ registry: defaultRunRegistry,
52750
+ commands,
52751
+ shell,
52752
+ capabilities,
52753
+ retention: capabilities.runHistory && runHistoryCfg ? { retentionDays: retentionPolicyOf(runHistoryCfg).retentionDays } : null,
52754
+ publicBaseUrl: process.env.PUBLIC_BASE_URL
52755
+ });
49864
52756
  const deliveryState = !capabilities.github ? "GET /delivery (503 \u2014 no GitHub credential)" : deliveryService.repos().length === 0 ? "GET /delivery (503 \u2014 no delivery.repos)" : `GET /delivery (${deliveryService.repos().join(",")}; snapshot every ${deliveryEveryMinutes} min)`;
49865
52757
  const tokenCount = Object.keys(auth.tokens).length;
49866
52758
  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)";
@@ -49915,6 +52807,10 @@ async function runBot() {
49915
52807
  ingress(req, res);
49916
52808
  return;
49917
52809
  }
52810
+ if (path === GITHUB_WEBHOOK_PATH) {
52811
+ void githubWebhook(req, res);
52812
+ return;
52813
+ }
49918
52814
  if (path === "/mcp") {
49919
52815
  mcp2(req, res);
49920
52816
  return;
@@ -49963,7 +52859,7 @@ async function runBot() {
49963
52859
  return;
49964
52860
  }
49965
52861
  if (webAssets.serve(req, res)) return;
49966
- if (isCommandPath(path) || isConnectPath(path) || path === "/runs" || path.startsWith("/runs/") || path === "/residents" || path.startsWith("/residents/") || path === "/costs" || path === "/costs.json" || path.startsWith("/costs/") || path === "/delivery" || path === "/delivery.json" || path.startsWith("/delivery/") || path === "/settings" || path.startsWith("/settings/")) {
52862
+ if (isCommandPath(path) || isConnectPath(path) || path === "/runs" || path.startsWith("/runs/") || path === "/residents" || path.startsWith("/residents/") || path === "/costs" || path === "/costs.json" || path.startsWith("/costs/") || path === "/delivery" || path === "/delivery.json" || path.startsWith("/delivery/") || path === "/settings" || path.startsWith("/settings/") || path === "/threads" || path.startsWith("/threads/")) {
49967
52863
  dashboardAuth.verify(req).then(async (gate) => {
49968
52864
  if (!gate.ok) {
49969
52865
  res.writeHead(gate.status, { "content-type": "text/plain; charset=utf-8" });
@@ -49985,6 +52881,7 @@ async function runBot() {
49985
52881
  personByEmail
49986
52882
  });
49987
52883
  if (liveView2(req, res, { actor })) return;
52884
+ if (webChat(req, res, { actor, identity: gate.identity })) return;
49988
52885
  if (mcpConnectView(req, res, gate.identity)) return;
49989
52886
  if (residentsView(req, res, { actor })) return;
49990
52887
  if (costsView(req, res, { identity: gate.identity, actor })) return;
@@ -50000,7 +52897,7 @@ async function runBot() {
50000
52897
  return;
50001
52898
  }
50002
52899
  if (path === "/") {
50003
- res.writeHead(302, { location: "/runs" });
52900
+ res.writeHead(302, { location: "/threads" });
50004
52901
  res.end();
50005
52902
  return;
50006
52903
  }
@@ -50041,7 +52938,7 @@ async function runBot() {
50041
52938
  }).listen(Number(process.env.PORT), () => {
50042
52939
  httpListeningAt = systemClock();
50043
52940
  console.log(
50044
- `http server on :${process.env.PORT} (health + POST /ingress + POST /mcp + model proxy (POST ${ANTHROPIC_MESSAGES_PATH}, POST ${OPENAI_CHAT_COMPLETIONS_PATH}) + ${liveViewState} + ${schedulesState} + ${residentsState} + ${costsState} + ${deliveryState} + GET /settings + ${commandHttpState} + /docs \u2192 ${PROJECT_DOCS_URL}; ${tokenCount > 0 ? `${tokenCount} ingress token(s)` : "ingress + MCP DISABLED \u2014 no tokens configured"}; ${accessState})`
52941
+ `http server on :${process.env.PORT} (health + POST /ingress + POST /mcp + model proxy (POST ${ANTHROPIC_MESSAGES_PATH}, POST ${OPENAI_CHAT_COMPLETIONS_PATH}) + ${liveViewState} + ${schedulesState} + ${residentsState} + ${costsState} + ${deliveryState} + GET /settings + GET /threads (+ POST /threads/<id>/send) + ${commandHttpState} + /docs \u2192 ${PROJECT_DOCS_URL}; ${tokenCount > 0 ? `${tokenCount} ingress token(s)` : "ingress + MCP DISABLED \u2014 no tokens configured"}; ${accessState})`
50045
52942
  );
50046
52943
  });
50047
52944
  }
@@ -50212,6 +53109,7 @@ var init_index = __esm({
50212
53109
  init_packageRoot();
50213
53110
  init_webShell();
50214
53111
  init_residentsView();
53112
+ init_web2();
50215
53113
  init_costsView();
50216
53114
  init_deliveryView();
50217
53115
  init_settingsView();
@@ -50259,6 +53157,7 @@ var init_index = __esm({
50259
53157
  init_modelProxy();
50260
53158
  init_runBearers();
50261
53159
  init_piHarness();
53160
+ init_harness2();
50262
53161
  init_relay();
50263
53162
  init_harnessRoutes();
50264
53163
  init_adminTraceLog();
@@ -50273,6 +53172,9 @@ var init_index = __esm({
50273
53172
  init_docsLink();
50274
53173
  init_dispatcher();
50275
53174
  init_adminCoordinator();
53175
+ init_githubWebhook();
53176
+ init_checksIntake();
53177
+ init_instancesClient();
50276
53178
  init_instanceStore();
50277
53179
  init_githubPulls();
50278
53180
  init_githubApi();
@@ -50329,6 +53231,7 @@ init_residentFleet();
50329
53231
  init_statusCardFrame();
50330
53232
  init_piAi();
50331
53233
  init_piHarness();
53234
+ init_harness2();
50332
53235
  init_relay();
50333
53236
  init_takeover();
50334
53237
  init_runBearers();
@@ -50688,12 +53591,18 @@ async function main() {
50688
53591
  const runBearers = new RunBearerStore({ clock: systemClock });
50689
53592
  const harnesses = new HarnessRegistry();
50690
53593
  const loopback = await askLoopbackServer({ bearers: runBearers, harnesses, config });
53594
+ const roster = {
53595
+ pi: new PiHarness(config.config.pi?.compaction ? { compaction: config.config.pi.compaction } : {}),
53596
+ opencode: new OpenCodeHarness(
53597
+ config.config.opencode?.compaction ? { compaction: config.config.opencode.compaction } : {}
53598
+ )
53599
+ };
50691
53600
  const deps = {
50692
53601
  config,
50693
53602
  completions,
50694
53603
  runBearers,
50695
53604
  harness: {
50696
- harness: new PiHarness(config.config.pi?.compaction ? { compaction: config.config.pi.compaction } : {}),
53605
+ harnesses: roster,
50697
53606
  registry: harnesses,
50698
53607
  harnessUrl: loopback.url,
50699
53608
  loopbackUrl: loopback.url