@akira-tl/forgerelay 0.8.8 → 0.8.10

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 (264) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +10 -18
  3. package/capabilities/code-intelligence/GUIDE.md +7 -3
  4. package/capabilities/host-integration/GUIDE.md +2 -2
  5. package/capabilities/subagents/GUIDE.md +9 -0
  6. package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +6 -0
  7. package/dist/activity/{audit-store.js → history/audit-store.js} +129 -21
  8. package/dist/activity/history/bash-output-store.js +250 -0
  9. package/dist/activity/{host-turn-store.js → history/host-turn-store.js} +4 -1
  10. package/dist/activity/{query-service.js → history/query-service.js} +13 -7
  11. package/dist/activity/{lifecycle.js → runtime/lifecycle.js} +1 -1
  12. package/dist/activity/{mcp-query-tools.js → runtime/mcp-query-tools.js} +3 -3
  13. package/dist/activity/storage/history.js +12 -0
  14. package/dist/activity/storage/paths.js +56 -0
  15. package/dist/activity/storage/segmented-log.js +149 -0
  16. package/dist/cli/init.js +180 -0
  17. package/dist/cli/setup-support.js +181 -0
  18. package/dist/cli.js +23 -216
  19. package/dist/lsp/code-intelligence.js +18 -4
  20. package/dist/lsp/runtime/diagnostic-snapshots.js +52 -1
  21. package/dist/lsp/runtime/managed-language-servers.js +172 -0
  22. package/dist/lsp/runtime/manager.js +37 -2
  23. package/dist/lsp/runtime/process-launch.js +3 -0
  24. package/dist/lsp/test-support/server-fixture.js +14 -11
  25. package/dist/mcp/filesystem/filesystem-tools.js +245 -0
  26. package/dist/mcp/hooks/command-runner.js +79 -0
  27. package/dist/{hook-cli.js → mcp/hooks/hook-cli.js} +1 -1
  28. package/dist/{hooks.js → mcp/hooks/hooks.js} +3 -80
  29. package/dist/{oauth-provider.js → mcp/oauth/oauth-provider.js} +1 -1
  30. package/dist/{oauth-store.js → mcp/oauth/oauth-store.js} +5 -4
  31. package/dist/{operations → mcp/operations}/native-bulk-mutations.js +2 -2
  32. package/dist/mcp/panel/app.js +143 -0
  33. package/dist/{mcp-app-template.js → mcp/panel/mcp-app-template.js} +2 -38
  34. package/dist/{process-platform.js → mcp/process/process-platform.js} +21 -9
  35. package/dist/{process-sessions.js → mcp/process/process-sessions.js} +3 -3
  36. package/dist/mcp/process/runtime.js +257 -0
  37. package/dist/mcp/process/tools.js +346 -0
  38. package/dist/mcp/server/core/activity-support.js +70 -0
  39. package/dist/{capabilities.js → mcp/server/core/capabilities.js} +5 -3
  40. package/dist/{capability-registry.js → mcp/server/core/capability-registry.js} +9 -4
  41. package/dist/mcp/server/core/capability-support.js +90 -0
  42. package/dist/mcp/server/core/schemas.js +216 -0
  43. package/dist/mcp/server/core/tool-support.js +210 -0
  44. package/dist/mcp/server/core/version.js +9 -0
  45. package/dist/mcp/server/operations/runtime/filesystem-tools.js +455 -0
  46. package/dist/mcp/server/operations/runtime/mutation-diagnostics.js +54 -0
  47. package/dist/mcp/server/operations/runtime/operation-runtime.js +524 -0
  48. package/dist/mcp/server/transport/http-server.js +301 -0
  49. package/dist/mcp/server/transport/http-support.js +43 -0
  50. package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +242 -0
  51. package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
  52. package/dist/mcp/server/workspace/runtime/workspace-open.js +555 -0
  53. package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
  54. package/dist/mcp/server-instructions.js +1 -1
  55. package/dist/{config.js → runtime/config/config.js} +4 -3
  56. package/dist/{user-config.js → runtime/config/user-config.js} +14 -63
  57. package/dist/{db → runtime/state/db}/client.js +1 -1
  58. package/dist/{db → runtime/state/db}/migrations.js +34 -3
  59. package/dist/runtime/state/lock/file-lock.js +130 -0
  60. package/dist/runtime/testing/server-fixture.js +190 -0
  61. package/dist/server.js +133 -4189
  62. package/dist/subagents/providers/adapters/claude.js +86 -52
  63. package/dist/subagents/providers/adapters/codex.js +103 -39
  64. package/dist/subagents/providers/adapters/opencode.js +114 -84
  65. package/dist/subagents/providers/adapters/pi.js +59 -8
  66. package/dist/subagents/providers/availability.js +15 -30
  67. package/dist/subagents/providers/runtime/external-command.js +56 -0
  68. package/dist/subagents/sessions/capability.js +1 -1
  69. package/dist/subagents/sessions/execution.js +1 -1
  70. package/dist/subagents/sessions/mcp/activity-audit-test-support.js +30 -0
  71. package/dist/subagents/sessions/store.js +1 -1
  72. package/dist/ui/.vite/manifest.json +342 -410
  73. package/dist/ui/activity-panel-app.html +2 -4
  74. package/dist/ui/assets/activity-panel-app-7Xe0alhm.css +1 -0
  75. package/dist/ui/assets/activity-panel-app-Sl1Jro8v.js +115 -0
  76. package/dist/ui/assets/angular-html-B_yn91wW.js +1 -0
  77. package/dist/ui/assets/{angular-ts-Bxh4-O1-.js → angular-ts-DSgtloKz.js} +1 -1
  78. package/dist/ui/assets/{apl-BmYHKQ6m.js → apl-NatWtF8X.js} +1 -1
  79. package/dist/ui/assets/{astro-C51vqDKq.js → astro-BYFEVBuN.js} +1 -1
  80. package/dist/ui/assets/{blade-CdhVsmwq.js → blade-DoO7yvMR.js} +1 -1
  81. package/dist/ui/assets/c-Rl2hMCcg.js +1 -0
  82. package/dist/ui/assets/{cobol-Cf6azoN_.js → cobol-ByC5mWiB.js} +1 -1
  83. package/dist/ui/assets/{coffee-vVdGzlmt.js → coffee-Dez77Qov.js} +1 -1
  84. package/dist/ui/assets/cpp-DtaAQT9h.js +1 -0
  85. package/dist/ui/assets/{crystal-5UZy6KIX.js → crystal-Bgmbf8ec.js} +1 -1
  86. package/dist/ui/assets/css-C1tjBtVN.js +1 -0
  87. package/dist/ui/assets/{edge-CESgo4ia.js → edge-DeH6u8Wg.js} +1 -1
  88. package/dist/ui/assets/{elixir-9FKfw4cJ.js → elixir-_H3KnmWT.js} +1 -1
  89. package/dist/ui/assets/{elm-Dv7A0aLa.js → elm-CsCe3U28.js} +1 -1
  90. package/dist/ui/assets/{erb-DHFmDvM0.js → erb-CbM-L2g8.js} +1 -1
  91. package/dist/ui/assets/{git-rebase-utbU6CzV.js → git-rebase-D0s5pDh9.js} +1 -1
  92. package/dist/ui/assets/{glimmer-js-t2kiCkyB.js → glimmer-js-BfOToq_A.js} +1 -1
  93. package/dist/ui/assets/{glimmer-ts-F0Jv5okV.js → glimmer-ts-CCUOkL2K.js} +1 -1
  94. package/dist/ui/assets/glsl-DH4ec9dE.js +1 -0
  95. package/dist/ui/assets/graphql-DilAMBoI.js +1 -0
  96. package/dist/ui/assets/{hack-8yjdbaSA.js → hack-BDkNDZlF.js} +1 -1
  97. package/dist/ui/assets/haml-D_QBp6K7.js +1 -0
  98. package/dist/ui/assets/{handlebars-BBFEg4Lo.js → handlebars-DePUrTpC.js} +1 -1
  99. package/dist/ui/assets/heavy-payload-DOwZpO6n.js +290 -0
  100. package/dist/ui/assets/html-By4dNnM4.js +1 -0
  101. package/dist/ui/assets/{html-derivative-DJAQcZyc.js → html-derivative-DDw9Ef4Q.js} +1 -1
  102. package/dist/ui/assets/{http-Dg5LSYRK.js → http-B0QxWc9M.js} +1 -1
  103. package/dist/ui/assets/{hurl-Y5uYzMZR.js → hurl-vMQ4lda1.js} +1 -1
  104. package/dist/ui/assets/java-DYRe2lWG.js +1 -0
  105. package/dist/ui/assets/javascript-CxyGqwcL.js +1 -0
  106. package/dist/ui/assets/{jinja-Bt30h8Ik.js → jinja-JUwV-fZt.js} +1 -1
  107. package/dist/ui/assets/{jison-D5uZ1Nyo.js → jison-7ZqCEvS8.js} +1 -1
  108. package/dist/ui/assets/json-dbltgiiA.js +1 -0
  109. package/dist/ui/assets/jsx-oDopT7YQ.js +1 -0
  110. package/dist/ui/assets/{julia-76hRaaGz.js → julia-CkKiNSav.js} +1 -1
  111. package/dist/ui/assets/{just-CDE7W0vW.js → just-DuqCAKJ3.js} +1 -1
  112. package/dist/ui/assets/{latex-CVO2Byab.js → latex-sudcRDSn.js} +1 -1
  113. package/dist/ui/assets/{liquid-CA-AS3EX.js → liquid-Di-08Js7.js} +1 -1
  114. package/dist/ui/assets/lua-YlqdMoV0.js +1 -0
  115. package/dist/ui/assets/{marko-_BWrPfny.js → marko-CJRNyJDb.js} +1 -1
  116. package/dist/ui/assets/{mdc-PyJv85eS.js → mdc-t12CzZHs.js} +1 -1
  117. package/dist/ui/assets/{nginx-DAWVw8yu.js → nginx-57-b4XwT.js} +1 -1
  118. package/dist/ui/assets/{nim-CKE1dzpU.js → nim-BzrswYQa.js} +1 -1
  119. package/dist/ui/assets/{perl-BBhWva19.js → perl-D9JR_iIh.js} +1 -1
  120. package/dist/ui/assets/{php-ChIeaAoF.js → php-Py2gFHil.js} +1 -1
  121. package/dist/ui/assets/{pug-CMgI5afi.js → pug-PzLLy0h1.js} +1 -1
  122. package/dist/ui/assets/{qml-CjxTkcFH.js → qml-By8fwgV4.js} +1 -1
  123. package/dist/ui/assets/r-CcezTbLq.js +1 -0
  124. package/dist/ui/assets/{razor-i4DbHr8p.js → razor-dG-vdhL_.js} +1 -1
  125. package/dist/ui/assets/regexp-BlsQ-N_8.js +1 -0
  126. package/dist/ui/assets/{rst-oy_97Kl4.js → rst-C2awDCat.js} +1 -1
  127. package/dist/ui/assets/{ruby-C5zYZIyl.js → ruby-zA2W_49d.js} +1 -1
  128. package/dist/ui/assets/{sas-BnMEh2S3.js → sas-D49bhyaN.js} +1 -1
  129. package/dist/ui/assets/scss-BQb-cp01.js +1 -0
  130. package/dist/ui/assets/shellscript-Dga4H_Ni.js +1 -0
  131. package/dist/ui/assets/{shellsession-BpE5o7_3.js → shellsession-n3-5CqcF.js} +1 -1
  132. package/dist/ui/assets/{soy-COesmhTY.js → soy-XYBOf_BA.js} +1 -1
  133. package/dist/ui/assets/sql-ET4u5Cpl.js +1 -0
  134. package/dist/ui/assets/{stata-CGQEuSZq.js → stata-CLHcJHO_.js} +1 -1
  135. package/dist/ui/assets/{surrealql-Bz4JbU-9.js → surrealql-BdjQ60T4.js} +1 -1
  136. package/dist/ui/assets/{svelte-1jM2ejXj.js → svelte-B2RPtKJe.js} +1 -1
  137. package/dist/ui/assets/{templ-C-VwELcp.js → templ-CrByPW_W.js} +1 -1
  138. package/dist/ui/assets/{tex-t1rUiQCm.js → tex-DZBkg363.js} +1 -1
  139. package/dist/ui/assets/{ts-tags-Bewoa6pS.js → ts-tags-DJR3Gphb.js} +1 -1
  140. package/dist/ui/assets/tsx-CajzROyy.js +1 -0
  141. package/dist/ui/assets/{twig-DVg-vELl.js → twig-BZjk5Zda.js} +1 -1
  142. package/dist/ui/assets/typescript-B-EWToLj.js +1 -0
  143. package/dist/ui/assets/{vue-wPQ7UlMy.js → vue-D_rHCCr8.js} +1 -1
  144. package/dist/ui/assets/{vue-html-BHtJ8geX.js → vue-html-CC4WlvVt.js} +1 -1
  145. package/dist/ui/assets/{vue-vine-CNSGAWwc.js → vue-vine-BgNMVgpk.js} +1 -1
  146. package/dist/ui/assets/xml-BI86-WbH.js +1 -0
  147. package/dist/ui/assets/{xsl-3_vJ27aE.js → xsl-DXeLZV4J.js} +1 -1
  148. package/dist/ui/assets/yaml-60_PFlWa.js +1 -0
  149. package/dist/workspaces/bootstrap.js +72 -0
  150. package/dist/workspaces/context.js +337 -0
  151. package/dist/workspaces/conversation-test-support.js +86 -0
  152. package/dist/{git-worktrees.js → workspaces/git/git-worktrees.js} +1 -1
  153. package/dist/workspaces/inventory.js +131 -0
  154. package/dist/workspaces/paths.js +55 -0
  155. package/dist/{remote-auth.js → workspaces/relay/auth/remote-auth.js} +2 -2
  156. package/dist/workspaces/relay/result-support.js +111 -0
  157. package/dist/workspaces/relay/tests/test-support.js +203 -0
  158. package/dist/{remote-mcp-connection-pool.js → workspaces/relay/transport/remote-mcp-connection-pool.js} +1 -1
  159. package/dist/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +30 -3
  160. package/dist/workspaces/relay/types.js +1 -0
  161. package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
  162. package/dist/workspaces/resources/resource-monitor.js +377 -0
  163. package/dist/workspaces/resources/skills.js +283 -0
  164. package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
  165. package/dist/workspaces/sessions.js +460 -0
  166. package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
  167. package/dist/workspaces.js +77 -993
  168. package/docs/chatgpt-coding-workflow.md +7 -14
  169. package/docs/configuration.md +26 -34
  170. package/docs/gotchas.md +0 -30
  171. package/docs/roadmap.md +6 -10
  172. package/docs/security.md +1 -5
  173. package/docs/setup.md +0 -11
  174. package/package.json +6 -9
  175. package/scripts/ci/architecture.mjs +109 -0
  176. package/scripts/ci/verify.mjs +5 -0
  177. package/scripts/debug/accept/bootstrap.mjs +326 -0
  178. package/scripts/debug/accept/harness.mjs +76 -0
  179. package/scripts/debug/accept/support.mjs +432 -0
  180. package/scripts/debug/accept.mjs +43 -806
  181. package/scripts/debug/relay-accept/support.mjs +135 -0
  182. package/scripts/debug/relay-accept.mjs +26 -115
  183. package/scripts/debug/traffic/support.mjs +167 -0
  184. package/scripts/debug/traffic/traffic-audit.mjs +4 -161
  185. package/scripts/{lsp-interop-support.test.mjs → lsp/interop-support.test.mjs} +1 -1
  186. package/scripts/{lsp-interop.mjs → lsp/interop.mjs} +3 -3
  187. package/scripts/release/publish.mjs +12 -9
  188. package/scripts/release/release-gate.test.mjs +3 -1
  189. package/scripts/release/release-version.test.mjs +13 -1
  190. package/scripts/release-parity.mjs +1 -1
  191. package/scripts/release-proof.mjs +15 -0
  192. package/scripts/release-proof.test.mjs +27 -1
  193. package/scripts/release-version.mjs +16 -1
  194. package/scripts/wiki/sync.mjs +21 -10
  195. package/dist/activity/bash-output-store.js +0 -127
  196. package/dist/pi-tools.js +0 -110
  197. package/dist/skills.js +0 -127
  198. package/dist/ui/assets/activity-panel-app-7AckaxIR.js +0 -2
  199. package/dist/ui/assets/angular-html-BkgDQl-G.js +0 -1
  200. package/dist/ui/assets/c-IBzBpxAO.js +0 -1
  201. package/dist/ui/assets/chunk-EyZ2wyi3.js +0 -1
  202. package/dist/ui/assets/cpp-CVHeO-Jk.js +0 -1
  203. package/dist/ui/assets/css-DEfus9J5.js +0 -1
  204. package/dist/ui/assets/glsl-_ooCjaeB.js +0 -1
  205. package/dist/ui/assets/graphql-B9LqP-cY.js +0 -1
  206. package/dist/ui/assets/haml-BU5EoZBn.js +0 -1
  207. package/dist/ui/assets/heavy-payload-Bol1_mcs.js +0 -4
  208. package/dist/ui/assets/html-D-C6YZk4.js +0 -1
  209. package/dist/ui/assets/java-ppHvycj5.js +0 -1
  210. package/dist/ui/assets/javascript-DDTjKLlQ.js +0 -1
  211. package/dist/ui/assets/json-Cw91r8h1.js +0 -1
  212. package/dist/ui/assets/jsx-04X2FT_1.js +0 -1
  213. package/dist/ui/assets/lua-CiyBp52Y.js +0 -1
  214. package/dist/ui/assets/r-DfFga4si.js +0 -1
  215. package/dist/ui/assets/regexp-iC9_nbSI.js +0 -1
  216. package/dist/ui/assets/review-payload-BqJp0c5e.js +0 -1
  217. package/dist/ui/assets/scrollbar-CvE-I-jG.js +0 -287
  218. package/dist/ui/assets/scss-BfZXsAWQ.js +0 -1
  219. package/dist/ui/assets/shellscript-BC2srNaB.js +0 -1
  220. package/dist/ui/assets/sql-Ccv_yeUm.js +0 -1
  221. package/dist/ui/assets/tsx-CTXzrBkN.js +0 -1
  222. package/dist/ui/assets/typescript--ElHoFkw.js +0 -1
  223. package/dist/ui/assets/workspace-app-BLW7p6IZ.js +0 -114
  224. package/dist/ui/assets/workspace-app-BjNqR0en.js +0 -1
  225. package/dist/ui/assets/workspace-app-CmaYU4DW.js +0 -3
  226. package/dist/ui/assets/workspace-app-D2bJ5fjt.css +0 -1
  227. package/dist/ui/assets/workspace-lifecycle-app-BVbI2Ilf.js +0 -1
  228. package/dist/ui/assets/xml-JnmX6vyS.js +0 -1
  229. package/dist/ui/assets/yaml-OAcZkP-w.js +0 -1
  230. package/dist/ui/workspace-app.html +0 -18
  231. package/dist/ui/workspace-lifecycle-app.html +0 -18
  232. package/docs/agent-profile-schema.md +0 -114
  233. package/docs/artifact-exchange.md +0 -65
  234. package/docs/versioning.md +0 -205
  235. package/skills/subagent-delegation/SKILL.md +0 -132
  236. /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
  237. /package/dist/activity/{process-output-audit.js → runtime/process-output-audit.js} +0 -0
  238. /package/dist/{artifact-error.js → mcp/artifacts/artifact-error.js} +0 -0
  239. /package/dist/{artifact-tools.js → mcp/artifacts/artifact-tools.js} +0 -0
  240. /package/dist/{incoming-artifacts.js → mcp/artifacts/incoming-artifacts.js} +0 -0
  241. /package/dist/{advertised-files.js → mcp/filesystem/advertised-files.js} +0 -0
  242. /package/dist/{apply-patch.js → mcp/filesystem/apply-patch.js} +0 -0
  243. /package/dist/{file-mutations.js → mcp/filesystem/file-mutations.js} +0 -0
  244. /package/dist/{roots.js → mcp/filesystem/roots.js} +0 -0
  245. /package/dist/{oauth → mcp/oauth}/public-url.js +0 -0
  246. /package/dist/{oauth → mcp/oauth}/router.js +0 -0
  247. /package/dist/{operations → mcp/operations}/batch/executor.js +0 -0
  248. /package/dist/{operations → mcp/operations}/batch/scheduler.js +0 -0
  249. /package/dist/{operations → mcp/operations}/batch/types.js +0 -0
  250. /package/dist/{operations → mcp/operations}/bulk-mutation.js +0 -0
  251. /package/dist/{operations → mcp/operations}/bulk-read.js +0 -0
  252. /package/dist/{operations → mcp/operations}/core-operation-executor.js +0 -0
  253. /package/dist/{request-meta.js → mcp/request-meta.js} +0 -0
  254. /package/dist/{mcp-sessions.js → mcp/server/transport/mcp-sessions.js} +0 -0
  255. /package/dist/{server-shutdown.js → mcp/server/transport/server-shutdown.js} +0 -0
  256. /package/dist/{logger.js → runtime/logging/logger.js} +0 -0
  257. /package/dist/{db → runtime/state/db}/schema.js +0 -0
  258. /package/dist/{composite-activity.js → workspaces/composite/composite-activity.js} +0 -0
  259. /package/dist/{composite-workspaces.js → workspaces/composite/composite-workspaces.js} +0 -0
  260. /package/dist/{git.js → workspaces/git/git.js} +0 -0
  261. /package/dist/{workspace-presentation.js → workspaces/presentation/workspace-presentation.js} +0 -0
  262. /package/dist/{workspace-task-reminders.js → workspaces/tasks/workspace-task-reminders.js} +0 -0
  263. /package/dist/{workspace-tasks.js → workspaces/tasks/workspace-tasks.js} +0 -0
  264. /package/scripts/{lsp-interop-support.mjs → lsp/interop-support.mjs} +0 -0
@@ -14,10 +14,12 @@ import { join, resolve } from "node:path";
14
14
  import { setTimeout as delay } from "node:timers/promises";
15
15
  import { activityRefreshDelayMs } from "../../../src/ui/activity/model.ts";
16
16
  import { debugRoot, repoRoot } from "../runtime.mjs";
17
+ import { jsonRequest, curlRequest, jsonLogEntries, requestLogEntries, requestBodiesForRpc, countBy, waitForHealth, stopServer, assertPortsFree, jsonBytes, formatBytes } from "./support.mjs";
18
+
17
19
 
18
20
  const mode = process.argv[2] ?? "all";
19
21
  if (!["all", "local", "relay"].includes(mode)) {
20
- throw new Error("Usage: scripts/debug/traffic/run.sh [all|local|relay]");
22
+ throw new Error("Usage: npm run traffic:audit -- [all|local|relay]");
21
23
  }
22
24
 
23
25
  const trafficRoot = resolve(debugRoot, "traffic-audit");
@@ -637,7 +639,7 @@ function authorizeHost(baseUrl, mcpUrl, ownerToken) {
637
639
  redirect_uri: redirectUri,
638
640
  code_challenge: challenge,
639
641
  code_challenge_method: "S256",
640
- scope: "devspace",
642
+ scope: "forgerelay",
641
643
  resource: mcpUrl,
642
644
  state: "traffic-audit",
643
645
  owner_token: ownerToken,
@@ -746,162 +748,3 @@ function parseMcpMessage(body, expectedId) {
746
748
  return messages.find((message) => message.id === expectedId) ?? messages[0];
747
749
  }
748
750
 
749
- function jsonRequest(url, options = {}) {
750
- const response = curlRequest({
751
- method: options.method ?? "GET",
752
- url,
753
- headers: options.headers,
754
- body: options.body,
755
- });
756
- return { ...response, json: JSON.parse(response.body) };
757
- }
758
-
759
- function curlRequest({ method = "GET", url, headers = {}, body }) {
760
- const marker = `__FORGERELAY_TRAFFIC_${randomUUID()}__`;
761
- const args = [
762
- "--silent",
763
- "--show-error",
764
- "--max-time",
765
- "20",
766
- "--request",
767
- method,
768
- "--dump-header",
769
- "-",
770
- "--output",
771
- "-",
772
- "--write-out",
773
- `\n${marker}%{http_code}|%{size_upload}|%{size_download}|%{size_request}|%{size_header}`,
774
- ];
775
- for (const [name, value] of Object.entries(headers)) args.push("--header", `${name}: ${value}`);
776
- if (body !== undefined) args.push("--data-binary", "@-");
777
- args.push(url);
778
-
779
- const result = spawnSync("curl", args, {
780
- cwd: repoRoot,
781
- input: body,
782
- encoding: "utf8",
783
- maxBuffer: 40 * 1024 * 1024,
784
- });
785
- if (result.status !== 0) {
786
- throw new Error(`curl ${method} ${url} failed: ${result.stderr.trim() || `exit ${result.status}`}`);
787
- }
788
- const statusMarker = `\n${marker}`;
789
- const markerIndex = result.stdout.lastIndexOf(statusMarker);
790
- assert.notEqual(markerIndex, -1, `curl response did not contain status marker for ${url}`);
791
- const rawResponse = result.stdout.slice(0, markerIndex);
792
- const stats = result.stdout.slice(markerIndex + statusMarker.length).trim().split("|");
793
- const [status, sizeUpload, sizeDownload, sizeRequest, sizeHeader] = stats.map(Number);
794
- const separator = rawResponse.indexOf("\r\n\r\n") >= 0 ? "\r\n\r\n" : "\n\n";
795
- const headerEnd = rawResponse.indexOf(separator);
796
- assert.notEqual(headerEnd, -1, `curl response did not contain headers for ${url}`);
797
- const headerBlock = rawResponse.slice(0, headerEnd);
798
- const responseBody = rawResponse.slice(headerEnd + separator.length);
799
- const responseHeaders = new Map();
800
- for (const line of headerBlock.split(/\r?\n/).slice(1)) {
801
- const colon = line.indexOf(":");
802
- if (colon < 0) continue;
803
- responseHeaders.set(line.slice(0, colon).trim().toLowerCase(), line.slice(colon + 1).trim());
804
- }
805
- return {
806
- status,
807
- headers: responseHeaders,
808
- body: responseBody,
809
- sizeUpload,
810
- sizeDownload,
811
- sizeRequest,
812
- sizeHeader,
813
- };
814
- }
815
-
816
- function jsonLogEntries(path) {
817
- if (!existsSync(path)) return [];
818
- return readFileSync(path, "utf8")
819
- .split(/\r?\n/)
820
- .filter(Boolean)
821
- .flatMap((line) => {
822
- try {
823
- return [JSON.parse(line)];
824
- } catch {
825
- return [];
826
- }
827
- });
828
- }
829
-
830
- function requestLogEntries(path) {
831
- return jsonLogEntries(path).filter((entry) => entry.event === "http_request");
832
- }
833
-
834
- function requestBodiesForRpc(entries, rpcTarget) {
835
- const requestIds = new Set(entries
836
- .filter((entry) => entry.event === "mcp_request" && entry.rpcTarget === rpcTarget)
837
- .map((entry) => entry.requestId)
838
- .filter(Boolean));
839
- return entries
840
- .filter((entry) => entry.event === "http_request" && requestIds.has(entry.requestId))
841
- .map((entry) => Number(entry.contentLength ?? 0))
842
- .filter((value) => Number.isFinite(value));
843
- }
844
-
845
- function countBy(values, keyFor) {
846
- const result = {};
847
- for (const value of values) {
848
- const key = keyFor(value);
849
- result[key] = (result[key] ?? 0) + 1;
850
- }
851
- return result;
852
- }
853
-
854
- async function waitForHealth(child, baseUrl) {
855
- for (let attempt = 0; attempt < 80; attempt += 1) {
856
- if (child.exitCode !== null) throw new Error(`debug server exited before health check: ${child.exitCode}`);
857
- try {
858
- const response = jsonRequest(`${baseUrl}/healthz`);
859
- if (response.status === 200) return;
860
- } catch {
861
- // still starting
862
- }
863
- await delay(100);
864
- }
865
- throw new Error(`debug server did not become healthy on ${baseUrl}`);
866
- }
867
-
868
- async function stopServer(child) {
869
- if (child.exitCode !== null) return;
870
- child.kill("SIGTERM");
871
- const exited = once(child, "exit");
872
- await Promise.race([
873
- exited,
874
- delay(3000).then(() => {
875
- if (child.exitCode === null) child.kill("SIGKILL");
876
- }),
877
- ]);
878
- }
879
-
880
- async function assertPortsFree(ports) {
881
- const script = [
882
- "const net=require('node:net');",
883
- `const ports=${JSON.stringify(ports)};`,
884
- "let pending=ports.length; let bad=[];",
885
- "for(const port of ports){const s=net.connect({host:'127.0.0.1',port}); s.setTimeout(250);",
886
- "s.once('connect',()=>{bad.push(port);s.destroy();done();});",
887
- "s.once('error',()=>{s.destroy();done();}); s.once('timeout',()=>{s.destroy();done();});}",
888
- "function done(){if(--pending===0){if(bad.length){console.error('busy:'+bad.join(','));process.exit(2)}}}",
889
- ].join("");
890
- const result = spawnSync(process.execPath, ["-e", script], { encoding: "utf8" });
891
- if (result.status !== 0) throw new Error(`reserved debug port already in use: ${result.stderr.trim()}`);
892
- }
893
-
894
- function jsonBytes(value) {
895
- if (value === undefined) return 0;
896
- return Buffer.byteLength(JSON.stringify(value), "utf8");
897
- }
898
-
899
- function formatBytes(value) {
900
- const bytes = Number(value);
901
- if (!Number.isFinite(bytes)) return String(value);
902
- if (bytes < 1024) return `${bytes.toFixed(0)} B`;
903
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
904
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`;
905
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
906
- }
907
-
@@ -3,7 +3,7 @@ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { delimiter, join } from "node:path";
5
5
  import test from "node:test";
6
- import { findExecutable, probeExecutable } from "./lsp-interop-support.mjs";
6
+ import { findExecutable, probeExecutable } from "./interop-support.mjs";
7
7
 
8
8
  const root = mkdtempSync(join(tmpdir(), "forgerelay-lsp-preflight-test-"));
9
9
 
@@ -2,10 +2,10 @@ import assert from "node:assert/strict";
2
2
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { CodeIntelligenceManager } from "../dist/lsp/runtime/manager.js";
6
- import { findExecutable, probeExecutable } from "./lsp-interop-support.mjs";
5
+ import { CodeIntelligenceManager } from "../../dist/lsp/runtime/manager.js";
6
+ import { findExecutable, probeExecutable } from "./interop-support.mjs";
7
7
 
8
- const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
8
+ const repoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
9
9
  const interopRoot = resolve(repoRoot, ".forgerelay-debug", "lsp-interop");
10
10
  const fixtures = [
11
11
  {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execFileSync, spawnSync } from "node:child_process";
4
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
4
+ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
@@ -28,6 +28,7 @@ if (head !== tagHead) {
28
28
  if (!gitSucceeds(["merge-base", "--is-ancestor", tagHead, "origin/main"])) {
29
29
  throw new Error(`${releaseTag} does not point to a commit contained in origin/main`);
30
30
  }
31
+ const notesPath = prepareReleaseNotes(releaseTag);
31
32
 
32
33
  const packages = readdirSync(packageDir).filter((name) => name.endsWith(".tgz"));
33
34
  if (packages.length !== 1) {
@@ -53,7 +54,6 @@ if (npmPackageExists(packageSpec)) {
53
54
  if (ghReleaseExists(releaseTag)) {
54
55
  console.log(`GitHub Release ${releaseTag} already exists; leaving it unchanged.`);
55
56
  } else {
56
- const notesPath = prepareReleaseNotes(releaseTag);
57
57
  const args = [
58
58
  "release",
59
59
  "create",
@@ -128,13 +128,16 @@ function ghReleaseExists(tag) {
128
128
 
129
129
  function prepareReleaseNotes(tag) {
130
130
  const manualNotes = join(repoRoot, "docs", "releases", `${tag}.md`);
131
- const notes = existsSync(manualNotes)
132
- ? readFileSync(manualNotes, "utf8")
133
- : execFileSync(process.execPath, ["scripts/release-version.mjs", "notes", tag], {
134
- cwd: repoRoot,
135
- encoding: "utf8",
136
- stdio: ["ignore", "pipe", "inherit"],
137
- });
131
+ let notes;
132
+ try {
133
+ notes = readFileSync(manualNotes, "utf8");
134
+ } catch (error) {
135
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
136
+ throw new Error(`Missing dedicated release notes: docs/releases/${tag}.md`);
137
+ }
138
+ throw error;
139
+ }
140
+ if (!notes.trim()) throw new Error(`Dedicated release notes are empty: docs/releases/${tag}.md`);
138
141
  const tempRoot = resolve(process.env.RUNNER_TEMP ?? join(repoRoot, ".forgerelay-debug"));
139
142
  mkdirSync(tempRoot, { recursive: true });
140
143
  const notesPath = join(tempRoot, `release-notes-${tag}.md`);
@@ -69,7 +69,9 @@ test("release runtime and local parity share the checked-in Node contract", asyn
69
69
 
70
70
  const source = await readFile(resolve(repoRoot, "scripts/release-parity.mjs"), "utf8");
71
71
  assert.match(source, /readFileSync\(join\(repoRoot, "\.nvmrc"\), "utf8"\)/);
72
- assert.match(source, /const NPM_VERSION = "10\.9\.3"/);
72
+ assert.match(source, /const NPM_VERSION = "11\.19\.1"/);
73
+ const ciWorkflow = await readFile(resolve(repoRoot, ".github/workflows/ci.yml"), "utf8");
74
+ assert.match(ciWorkflow, /npm install --global npm@11\.19\.1/);
73
75
  assert.ok(source.includes('["npm", "ci", "--no-audit", "--no-fund"]'));
74
76
  assert.ok(source.includes('["npm", "run", "ci:verify"]'));
75
77
  assert.ok(source.includes('["npm", "run", "release:pack"]'));
@@ -14,6 +14,7 @@ async function createFixture(t) {
14
14
  const root = await mkdtemp(join(tmpdir(), "forgerelay-release-version-"));
15
15
  t.after(() => rm(root, { recursive: true, force: true }));
16
16
  await mkdir(join(root, "scripts"), { recursive: true });
17
+ await mkdir(join(root, "docs", "releases"), { recursive: true });
17
18
  await cp(sourceScript, join(root, "scripts", "release-version.mjs"));
18
19
 
19
20
  const pkg = {
@@ -88,8 +89,19 @@ test("release version tooling prepares and validates an rc release", async (t) =
88
89
  const checked = await runRelease(root, "check");
89
90
  assert.match(checked.stdout, /release metadata is consistent at 0\.6\.0-rc\.1/);
90
91
 
92
+ await assert.rejects(
93
+ () => runRelease(root, "tag", "v0.6.0-rc.1"),
94
+ (error) => {
95
+ assert.match(String(error.stderr ?? error), /missing dedicated release notes: docs\/releases\/v0\.6\.0-rc\.1\.md/);
96
+ return true;
97
+ },
98
+ );
99
+ await writeFile(
100
+ join(root, "docs", "releases", "v0.6.0-rc.1.md"),
101
+ "# ForgeRelay v0.6.0-rc.1\n\nRelease candidate notes.\n",
102
+ );
91
103
  const tagged = await runRelease(root, "tag", "v0.6.0-rc.1");
92
- assert.match(tagged.stdout, /release tag v0\.6\.0-rc\.1 matches package version and changelog/);
104
+ assert.match(tagged.stdout, /release tag v0\.6\.0-rc\.1 matches package version, changelog, and dedicated release notes/);
93
105
 
94
106
  const notes = await runRelease(root, "notes", "v0.6.0-rc.1");
95
107
  assert.match(notes.stdout, /Release candidate behavior/);
@@ -5,7 +5,7 @@ import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
- const NPM_VERSION = "10.9.3";
8
+ const NPM_VERSION = "11.19.1";
9
9
  const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
10
  const NODE_VERSION = readFileSync(join(repoRoot, ".nvmrc"), "utf8").trim();
11
11
  const debugRoot = join(repoRoot, ".forgerelay-debug");
@@ -29,6 +29,20 @@ function packageVersion() {
29
29
  return pkg.version;
30
30
  }
31
31
 
32
+ function requireDedicatedReleaseNotes(version) {
33
+ const path = join(process.cwd(), "docs", "releases", `v${version}.md`);
34
+ let notes;
35
+ try {
36
+ notes = readFileSync(path, "utf8");
37
+ } catch (error) {
38
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
39
+ throw new Error(`missing dedicated release notes: docs/releases/v${version}.md`);
40
+ }
41
+ throw error;
42
+ }
43
+ if (!notes.trim()) throw new Error(`dedicated release notes are empty: docs/releases/v${version}.md`);
44
+ }
45
+
32
46
  function gitDir() {
33
47
  const path = git(["rev-parse", "--git-dir"]);
34
48
  return path.startsWith("/") ? path : join(process.cwd(), path);
@@ -138,6 +152,7 @@ function checkHookTag() {
138
152
  if (tag !== expectedTag) {
139
153
  throw new Error(`tag ${tag} does not match package version ${version}; expected ${expectedTag}`);
140
154
  }
155
+ requireDedicatedReleaseNotes(version);
141
156
 
142
157
  let tagHead;
143
158
  try {
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { execFile } from "node:child_process";
3
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import test from "node:test";
@@ -25,6 +25,8 @@ test("release tag hook gate uses repository facts instead of requiring a local p
25
25
  const root = await mkdtemp(join(tmpdir(), "forgerelay-release-proof-"));
26
26
  t.after(() => rm(root, { recursive: true, force: true }));
27
27
  await writeFile(join(root, "package.json"), JSON.stringify({ version: "1.2.3" }) + "\n");
28
+ await mkdir(join(root, "docs", "releases"), { recursive: true });
29
+ await writeFile(join(root, "docs", "releases", "v1.2.3.md"), "# v1.2.3\n\nDedicated notes.\n");
28
30
  await writeFile(join(root, "tracked.txt"), "verified\n");
29
31
  await git(root, ["init"]);
30
32
  await git(root, ["config", "user.email", "proof@example.com"]);
@@ -102,10 +104,34 @@ test("release tag hook gate uses repository facts instead of requiring a local p
102
104
  );
103
105
  });
104
106
 
107
+ test("release tag hook gate rejects a matching tag when dedicated notes are missing", async (t) => {
108
+ const root = await mkdtemp(join(tmpdir(), "forgerelay-release-proof-"));
109
+ t.after(() => rm(root, { recursive: true, force: true }));
110
+ await writeFile(join(root, "package.json"), JSON.stringify({ version: "1.4.0" }) + "\n");
111
+ await git(root, ["init"]);
112
+ await git(root, ["config", "user.email", "proof@example.com"]);
113
+ await git(root, ["config", "user.name", "Release Proof Test"]);
114
+ await git(root, ["add", "."]);
115
+ await git(root, ["commit", "-m", "release 1.4.0"]);
116
+ await git(root, ["tag", "v1.4.0"]);
117
+
118
+ await assert.rejects(
119
+ () => runProof(root, "check-hook", {
120
+ FORGERELAY_HOOK_PAYLOAD: JSON.stringify({ command: "git push origin v1.4.0" }),
121
+ }),
122
+ (error) => {
123
+ assert.match(String(error.stderr ?? error), /missing dedicated release notes: docs\/releases\/v1\.4\.0\.md/);
124
+ return true;
125
+ },
126
+ );
127
+ });
128
+
105
129
  test("release tag hook gate accepts an rc tag for the package version", async (t) => {
106
130
  const root = await mkdtemp(join(tmpdir(), "forgerelay-release-proof-"));
107
131
  t.after(() => rm(root, { recursive: true, force: true }));
108
132
  await writeFile(join(root, "package.json"), JSON.stringify({ version: "0.6.0-rc.1" }) + "\n");
133
+ await mkdir(join(root, "docs", "releases"), { recursive: true });
134
+ await writeFile(join(root, "docs", "releases", "v0.6.0-rc.1.md"), "# v0.6.0-rc.1\n\nDedicated RC notes.\n");
109
135
  await git(root, ["init"]);
110
136
  await git(root, ["config", "user.email", "proof@example.com"]);
111
137
  await git(root, ["config", "user.name", "Release Proof Test"]);
@@ -35,7 +35,8 @@ switch (command) {
35
35
  if (value !== expectedTag) {
36
36
  fail(`tag ${JSON.stringify(value)} does not match package version; expected ${expectedTag}`);
37
37
  }
38
- console.log(`release tag ${value} matches package version and changelog`);
38
+ await requireReleaseNotes(state.pkg.version);
39
+ console.log(`release tag ${value} matches package version, changelog, and dedicated release notes`);
39
40
  break;
40
41
  }
41
42
  case "prepare": {
@@ -203,6 +204,20 @@ function getUnreleasedBody(changelog) {
203
204
  return changelog.slice(bodyStart, bodyEnd).trim();
204
205
  }
205
206
 
207
+ async function requireReleaseNotes(version) {
208
+ const path = resolve(repoRoot, "docs", "releases", `v${version}.md`);
209
+ let content;
210
+ try {
211
+ content = await readFile(path, "utf8");
212
+ } catch (error) {
213
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
214
+ fail(`missing dedicated release notes: docs/releases/v${version}.md`);
215
+ }
216
+ throw error;
217
+ }
218
+ if (!content.trim()) fail(`dedicated release notes are empty: docs/releases/v${version}.md`);
219
+ }
220
+
206
221
  function getReleaseBody(changelog, version) {
207
222
  const heading = `## [${version}]`;
208
223
  const headingIndex = changelog.indexOf(heading);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import {
4
- cpSync,
4
+ copyFileSync,
5
5
  existsSync,
6
6
  mkdtempSync,
7
7
  readdirSync,
@@ -49,7 +49,13 @@ function validateWikiSource(directory) {
49
49
  }
50
50
 
51
51
  const files = listFiles(directory);
52
- const names = new Set(files.map((file) => file.relativePath));
52
+ const destinationNames = files.map((file) => wikiDestinationName(file.relativePath));
53
+ const names = new Set(destinationNames);
54
+ if (names.size !== destinationNames.length) {
55
+ const duplicates = destinationNames.filter((name, index) => destinationNames.indexOf(name) !== index);
56
+ fail(`Wiki source contains duplicate flattened page names:
57
+ ${[...new Set(duplicates)].map((name) => `- ${name}`).join("\n")}`);
58
+ }
53
59
 
54
60
  for (const required of ["Home.md", "_Sidebar.md"]) {
55
61
  if (!names.has(required)) {
@@ -59,7 +65,7 @@ function validateWikiSource(directory) {
59
65
 
60
66
  const invalidNames = files
61
67
  .map((file) => file.relativePath)
62
- .filter((name) => !isSafeWikiPath(name));
68
+ .filter((name) => !isSafeWikiSourcePath(name));
63
69
  if (invalidNames.length > 0) {
64
70
  fail(`Wiki source contains unsupported paths:\n${invalidNames.map((name) => `- ${name}`).join("\n")}`);
65
71
  }
@@ -67,7 +73,7 @@ function validateWikiSource(directory) {
67
73
  const markdownNames = new Set(
68
74
  files
69
75
  .filter((file) => file.relativePath.endsWith(".md"))
70
- .map((file) => file.relativePath),
76
+ .map((file) => wikiDestinationName(file.relativePath)),
71
77
  );
72
78
  const brokenLinks = [];
73
79
 
@@ -86,7 +92,7 @@ function validateWikiSource(directory) {
86
92
  }
87
93
 
88
94
  const candidate = target.endsWith(".md") ? target : `${target}.md`;
89
- if (!markdownNames.has(candidate)) {
95
+ if (!markdownNames.has(wikiDestinationName(candidate))) {
90
96
  brokenLinks.push(`${file.relativePath}: ${rawTarget}`);
91
97
  }
92
98
  }
@@ -169,8 +175,8 @@ function mirrorSource(source, destination) {
169
175
  rmSync(join(destination, entry), { recursive: true, force: true });
170
176
  }
171
177
 
172
- for (const entry of readdirSync(source)) {
173
- cpSync(join(source, entry), join(destination, entry), { recursive: true });
178
+ for (const file of listFiles(source)) {
179
+ copyFileSync(file.absolutePath, join(destination, wikiDestinationName(file.relativePath)));
174
180
  }
175
181
  }
176
182
 
@@ -191,10 +197,15 @@ function listFiles(directory, prefix = "") {
191
197
  return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
192
198
  }
193
199
 
194
- function isSafeWikiPath(relativePath) {
200
+ function wikiDestinationName(relativePath) {
201
+ return basename(relativePath);
202
+ }
203
+
204
+ function isSafeWikiSourcePath(relativePath) {
195
205
  if (!relativePath.endsWith(".md")) return false;
196
- if (relativePath.includes("/")) return false;
197
- return !/[\\:*?"<>|]/.test(relativePath) && basename(relativePath) === relativePath;
206
+ return relativePath
207
+ .split("/")
208
+ .every((segment) => segment.length > 0 && !/[\\:*?"<>|]/.test(segment));
198
209
  }
199
210
 
200
211
  function isExternalOrAnchorLink(target) {
@@ -1,127 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { StringDecoder } from "node:string_decoder";
3
- import { openDatabase } from "../db/client.js";
4
- export class BashOutputStore {
5
- database;
6
- now;
7
- nextOutputId;
8
- nextSequences = new Map();
9
- constructor(stateDir, options = {}) {
10
- this.database = openDatabase(stateDir);
11
- this.now = options.now ?? (() => new Date());
12
- this.nextOutputId = options.outputId ?? (() => `out_${randomUUID().replaceAll("-", "")}`);
13
- }
14
- begin(input) {
15
- const outputId = this.nextOutputId();
16
- const startedAt = this.now().toISOString();
17
- this.database.sqlite.prepare(`insert into bash_output_streams (
18
- id, activity_id, turn_id, conversation_scope_id, process_id,
19
- workspace_id, workspace_root, command, tty, status, timed_out, started_at
20
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', 0, ?)`).run(outputId, input.activityId, input.turnId, input.conversationScopeId ?? null, input.processId, input.workspaceId, input.workspaceRoot, input.command, input.tty ? 1 : 0, startedAt);
21
- this.nextSequences.set(outputId, 1);
22
- return outputId;
23
- }
24
- append(outputId, channel, data) {
25
- const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
26
- if (bytes.length === 0)
27
- return;
28
- const sequence = this.nextSequence(outputId);
29
- this.database.sqlite.prepare(`insert into bash_output_chunks (output_id, sequence, channel, data, created_at)
30
- values (?, ?, ?, ?, ?)`).run(outputId, sequence, channel, bytes, this.now().toISOString());
31
- }
32
- markReturned(outputId) {
33
- this.database.sqlite.prepare("update bash_output_streams set returned = 1 where id = ?").run(outputId);
34
- }
35
- claimCompletion(outputId) {
36
- const claimedAt = this.now().toISOString();
37
- const claimed = this.database.sqlite.prepare(`update bash_output_streams
38
- set completion_claimed_at = ?
39
- where id = ? and returned = 1 and status != 'running' and completion_claimed_at is null`).run(claimedAt, outputId);
40
- return claimed.changes === 1 ? this.read(outputId) : undefined;
41
- }
42
- finish(outputId, input) {
43
- const status = input.error || input.timedOut || input.signal || (input.exitCode !== undefined && input.exitCode !== 0)
44
- ? "failed"
45
- : "done";
46
- this.database.sqlite.prepare(`update bash_output_streams
47
- set status = ?, exit_code = ?, signal = ?, timed_out = ?, error = ?, finished_at = ?
48
- where id = ?`).run(status, input.exitCode ?? null, input.signal ?? null, input.timedOut ? 1 : 0, input.error ?? null, this.now().toISOString(), outputId);
49
- }
50
- read(outputId) {
51
- const stream = this.database.sqlite.prepare("select * from bash_output_streams where id = ?").get(outputId);
52
- if (!stream)
53
- return undefined;
54
- const rows = this.database.sqlite.prepare(`select * from bash_output_chunks
55
- where output_id = ?
56
- order by sequence asc`).all(outputId);
57
- const decoded = decodeChunks(rows);
58
- return {
59
- outputId: stream.id,
60
- activityId: stream.activity_id,
61
- turnId: stream.turn_id,
62
- ...(stream.conversation_scope_id ? { conversationScopeId: stream.conversation_scope_id } : {}),
63
- processId: stream.process_id,
64
- workspaceId: stream.workspace_id,
65
- workspaceRoot: stream.workspace_root,
66
- command: stream.command,
67
- tty: stream.tty === 1,
68
- output: decoded.map((chunk) => chunk.data).join(""),
69
- chunks: decoded,
70
- status: isBashOutputStatus(stream.status) ? stream.status : "failed",
71
- ...(stream.exit_code !== null ? { exitCode: stream.exit_code } : {}),
72
- ...(stream.signal ? { signal: stream.signal } : {}),
73
- timedOut: stream.timed_out === 1,
74
- ...(stream.error ? { error: stream.error } : {}),
75
- returned: stream.returned === 1,
76
- startedAt: stream.started_at,
77
- ...(stream.finished_at ? { finishedAt: stream.finished_at } : {}),
78
- };
79
- }
80
- close() {
81
- this.database.close();
82
- }
83
- nextSequence(outputId) {
84
- const known = this.nextSequences.get(outputId);
85
- if (known !== undefined) {
86
- this.nextSequences.set(outputId, known + 1);
87
- return known;
88
- }
89
- const row = this.database.sqlite.prepare("select coalesce(max(sequence), 0) as sequence from bash_output_chunks where output_id = ?").get(outputId);
90
- const sequence = row.sequence + 1;
91
- this.nextSequences.set(outputId, sequence + 1);
92
- return sequence;
93
- }
94
- }
95
- function decodeChunks(rows) {
96
- const decoders = new Map();
97
- const chunks = [];
98
- const lastChunkIndex = new Map();
99
- for (const row of rows) {
100
- if (!isBashOutputChannel(row.channel)) {
101
- throw new Error(`Unknown Bash output channel: ${row.channel}`);
102
- }
103
- const decoder = decoders.get(row.channel) ?? new StringDecoder("utf8");
104
- decoders.set(row.channel, decoder);
105
- const data = decoder.write(row.data);
106
- chunks.push({ sequence: row.sequence, channel: row.channel, data });
107
- lastChunkIndex.set(row.channel, chunks.length - 1);
108
- }
109
- for (const [channel, decoder] of decoders) {
110
- const tail = decoder.end();
111
- if (!tail)
112
- continue;
113
- const index = lastChunkIndex.get(channel);
114
- if (index === undefined)
115
- continue;
116
- const chunk = chunks[index];
117
- if (chunk)
118
- chunk.data += tail;
119
- }
120
- return chunks;
121
- }
122
- function isBashOutputChannel(value) {
123
- return value === "stdout" || value === "stderr" || value === "pty" || value === "process";
124
- }
125
- function isBashOutputStatus(value) {
126
- return value === "running" || value === "done" || value === "failed";
127
- }