@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
@@ -0,0 +1,135 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
4
+ import { once } from "node:events";
5
+ import { connect } from "node:net";
6
+ import {
7
+ existsSync,
8
+ mkdirSync,
9
+ readFileSync,
10
+ readdirSync,
11
+ rmSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+ import { setTimeout as delay } from "node:timers/promises";
16
+ import { debugRoot, repoRoot } from "../runtime.mjs";
17
+
18
+ export function relayAcceptanceTopology() {
19
+ const gatewayPort = 7677;
20
+ const executionPort = 7678;
21
+ const gatewayBaseUrl = `http://127.0.0.1:${gatewayPort}`;
22
+ const gatewayMcpUrl = `${gatewayBaseUrl}/mcp`;
23
+ const executionBaseUrl = `http://127.0.0.1:${executionPort}`;
24
+ const acceptanceRoot = resolve(debugRoot, "relay-acceptance");
25
+ const gatewayConfigDir = join(acceptanceRoot, "gateway", "config");
26
+ const gatewayStateDir = join(acceptanceRoot, "gateway", "state");
27
+ const gatewayWorktreeRoot = join(acceptanceRoot, "gateway", "worktrees");
28
+ const gatewayProjects = join(acceptanceRoot, "gateway-projects");
29
+ const gatewayLocalProject = join(gatewayProjects, "local");
30
+ const executionConfigDir = join(acceptanceRoot, "execution", "config");
31
+ const executionStateDir = join(acceptanceRoot, "execution", "state");
32
+ const executionWorktreeRoot = join(acceptanceRoot, "execution", "worktrees");
33
+ const executionProjects = join(acceptanceRoot, "execution-projects");
34
+ const executionCheckout = join(executionProjects, "checkout");
35
+ const executionWorktreeSource = join(executionProjects, "worktree-source");
36
+ const bootstrapSecret = "RELAY_ACCEPTANCE_BOOTSTRAP_SECRET";
37
+ const checkoutTaskBody = "RELAY_ACCEPTANCE_EXECUTION_TASK_BODY";
38
+ const worktreeTaskBody = "RELAY_ACCEPTANCE_WORKTREE_TASK_BODY";
39
+ const compositeMemberTaskBody = "RELAY_ACCEPTANCE_COMPOSITE_MEMBER_TASK_BODY";
40
+ const compositeTaskBody = "RELAY_ACCEPTANCE_COMPOSITE_OWN_TASK_BODY";
41
+ return {
42
+ gatewayPort, executionPort, gatewayBaseUrl, gatewayMcpUrl, executionBaseUrl,
43
+ acceptanceRoot, gatewayConfigDir, gatewayStateDir, gatewayWorktreeRoot,
44
+ gatewayProjects, gatewayLocalProject, executionConfigDir, executionStateDir,
45
+ executionWorktreeRoot, executionProjects, executionCheckout, executionWorktreeSource,
46
+ bootstrapSecret, checkoutTaskBody, worktreeTaskBody, compositeMemberTaskBody, compositeTaskBody,
47
+ };
48
+ }
49
+
50
+ export async function assertPortsFree(ports) {
51
+ for (const port of ports) {
52
+ const occupied = await new Promise((resolvePromise) => {
53
+ const socket = connect({ host: "127.0.0.1", port });
54
+ socket.once("connect", () => {
55
+ socket.destroy();
56
+ resolvePromise(true);
57
+ });
58
+ socket.once("error", () => resolvePromise(false));
59
+ });
60
+ assert.equal(occupied, false, `reserved debug port ${port} is already in use; refusing to touch the existing process`);
61
+ }
62
+ }
63
+
64
+ export function setupGitProject(root) {
65
+ mkdirSync(root, { recursive: true });
66
+ writeFileSync(join(root, "README.md"), "Relay managed worktree acceptance\n");
67
+ runGit(root, ["init"]);
68
+ runGit(root, ["config", "user.email", "forgerelay-debug@example.com"]);
69
+ runGit(root, ["config", "user.name", "ForgeRelay Debug"]);
70
+ runGit(root, ["add", "."]);
71
+ runGit(root, ["commit", "-m", "Initial Relay acceptance commit"]);
72
+ }
73
+
74
+ export function runGit(cwd, args) {
75
+ const result = spawnSync("git", args, { cwd, encoding: "utf8" });
76
+ if (result.status !== 0) {
77
+ throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`);
78
+ }
79
+ }
80
+
81
+ export function taskStateFiles(stateDir) {
82
+ const workspacesDir = join(stateDir, "workspaces");
83
+ if (!existsSync(workspacesDir)) return [];
84
+ const files = [];
85
+ for (const entry of readdirSync(workspacesDir, { withFileTypes: true })) {
86
+ if (!entry.isDirectory()) continue;
87
+ const candidate = join(workspacesDir, entry.name, "tasks.json");
88
+ if (existsSync(candidate)) files.push(candidate);
89
+ }
90
+ return files;
91
+ }
92
+
93
+ export function findTaskStateFile(stateDir, needle) {
94
+ return taskStateFiles(stateDir).find((path) => readFileSync(path, "utf8").includes(needle));
95
+ }
96
+
97
+ export function assertTaskBodyAbsentFromGateway(gatewayStateDir, body) {
98
+ const leaked = taskStateFiles(gatewayStateDir).filter((path) => readFileSync(path, "utf8").includes(body));
99
+ assert.deepEqual(leaked, [], `Gateway stored an Execution-owned Task body in: ${leaked.join(", ")}`);
100
+ }
101
+
102
+ export function assertSafeRelayInspection(result, { executionWorkspaceId, executionBaseUrl, executionPort, executionOwnerToken, bootstrapSecret, remoteRecord, taskBodies }) {
103
+ const json = JSON.stringify(result);
104
+ for (const forbidden of [
105
+ executionWorkspaceId,
106
+ executionBaseUrl,
107
+ `127.0.0.1:${executionPort}`,
108
+ executionOwnerToken,
109
+ remoteRecord.accessToken,
110
+ remoteRecord.refreshToken,
111
+ bootstrapSecret,
112
+ "remoteInstanceId",
113
+ "remoteWorkspaceId",
114
+ "sshRoute",
115
+ ...taskBodies,
116
+ ]) {
117
+ assert.equal(json.includes(forbidden), false, `Relay inspection leaked ${forbidden}`);
118
+ }
119
+ assert.doesNotMatch(json, /"ws_[0-9a-f]{10}"/);
120
+ }
121
+
122
+ export function assertToolOk(result, label) {
123
+ assert.equal(result.isError, undefined, `${label}: ${toolText(result)}`);
124
+ }
125
+
126
+ export function toolText(result) {
127
+ return (result.content ?? [])
128
+ .filter((entry) => entry.type === "text")
129
+ .map((entry) => entry.text)
130
+ .join("\n");
131
+ }
132
+
133
+ export function pass(label, detail) {
134
+ console.log(`PASS ${label}: ${detail}`);
135
+ }
@@ -14,31 +14,17 @@ import {
14
14
  import { join, resolve } from "node:path";
15
15
  import { setTimeout as delay } from "node:timers/promises";
16
16
  import { debugRoot, repoRoot } from "./runtime.mjs";
17
-
18
- const gatewayPort = 7677;
19
- const executionPort = 7678;
20
- const gatewayBaseUrl = `http://127.0.0.1:${gatewayPort}`;
21
- const gatewayMcpUrl = `${gatewayBaseUrl}/mcp`;
22
- const executionBaseUrl = `http://127.0.0.1:${executionPort}`;
23
- const acceptanceRoot = resolve(debugRoot, "relay-acceptance");
24
- const gatewayConfigDir = join(acceptanceRoot, "gateway", "config");
25
- const gatewayStateDir = join(acceptanceRoot, "gateway", "state");
26
- const gatewayWorktreeRoot = join(acceptanceRoot, "gateway", "worktrees");
27
- const gatewayProjects = join(acceptanceRoot, "gateway-projects");
28
- const gatewayLocalProject = join(gatewayProjects, "local");
29
- const executionConfigDir = join(acceptanceRoot, "execution", "config");
30
- const executionStateDir = join(acceptanceRoot, "execution", "state");
31
- const executionWorktreeRoot = join(acceptanceRoot, "execution", "worktrees");
32
- const executionProjects = join(acceptanceRoot, "execution-projects");
33
- const executionCheckout = join(executionProjects, "checkout");
34
- const executionWorktreeSource = join(executionProjects, "worktree-source");
17
+ import { relayAcceptanceTopology, assertPortsFree, setupGitProject, runGit, taskStateFiles, findTaskStateFile, assertTaskBodyAbsentFromGateway, assertSafeRelayInspection, assertToolOk, toolText, pass } from "./relay-accept/support.mjs";
18
+
19
+ const {
20
+ gatewayPort, executionPort, gatewayBaseUrl, gatewayMcpUrl, executionBaseUrl,
21
+ acceptanceRoot, gatewayConfigDir, gatewayStateDir, gatewayWorktreeRoot,
22
+ gatewayProjects, gatewayLocalProject, executionConfigDir, executionStateDir,
23
+ executionWorktreeRoot, executionProjects, executionCheckout, executionWorktreeSource,
24
+ bootstrapSecret, checkoutTaskBody, worktreeTaskBody, compositeMemberTaskBody, compositeTaskBody,
25
+ } = relayAcceptanceTopology();
35
26
  const gatewayOwnerToken = randomBytes(32).toString("base64url");
36
27
  const executionOwnerToken = randomBytes(32).toString("base64url");
37
- const bootstrapSecret = "RELAY_ACCEPTANCE_BOOTSTRAP_SECRET";
38
- const checkoutTaskBody = "RELAY_ACCEPTANCE_EXECUTION_TASK_BODY";
39
- const worktreeTaskBody = "RELAY_ACCEPTANCE_WORKTREE_TASK_BODY";
40
- const compositeMemberTaskBody = "RELAY_ACCEPTANCE_COMPOSITE_MEMBER_TASK_BODY";
41
- const compositeTaskBody = "RELAY_ACCEPTANCE_COMPOSITE_OWN_TASK_BODY";
42
28
 
43
29
  await assertPortsFree([gatewayPort, executionPort]);
44
30
  rmSync(acceptanceRoot, { recursive: true, force: true });
@@ -152,7 +138,7 @@ try {
152
138
  assert.ok(checkoutTaskFile, "Execution Task state file was not found");
153
139
  const checkoutExecutionWorkspaceId = checkoutTaskFile.split("/").at(-2);
154
140
  assert.match(checkoutExecutionWorkspaceId, /^ws_[0-9a-f]{10}$/);
155
- assertTaskBodyAbsentFromGateway(checkoutTaskBody);
141
+ assertTaskBodyAbsentFromGateway(gatewayStateDir, checkoutTaskBody);
156
142
  pass("Execution-owned Task truth", `${checkoutExecutionWorkspaceId} owns the Task state; Gateway has no shadow copy`);
157
143
 
158
144
  for (let index = 0; index < 2; index += 1) {
@@ -188,6 +174,10 @@ try {
188
174
  assert.equal(inspectedCheckout.structuredContent.inspection.taskSummary.lists[0].unfinishedTaskCount, 1);
189
175
  assertSafeRelayInspection(inspectedCheckout, {
190
176
  executionWorkspaceId: checkoutExecutionWorkspaceId,
177
+ executionBaseUrl,
178
+ executionPort,
179
+ executionOwnerToken,
180
+ bootstrapSecret,
191
181
  remoteRecord,
192
182
  taskBodies: [checkoutTaskBody],
193
183
  });
@@ -269,7 +259,7 @@ try {
269
259
  const worktreeTaskFile = findTaskStateFile(executionStateDir, worktreeTaskBody);
270
260
  assert.ok(worktreeTaskFile, "Execution managed-worktree Task state file was not found");
271
261
  const worktreeExecutionWorkspaceId = worktreeTaskFile.split("/").at(-2);
272
- assertTaskBodyAbsentFromGateway(worktreeTaskBody);
262
+ assertTaskBodyAbsentFromGateway(gatewayStateDir, worktreeTaskBody);
273
263
 
274
264
  const worktreeWrite = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "write", {
275
265
  workspaceId: worktreeRelayId,
@@ -298,6 +288,10 @@ try {
298
288
  assert.equal(inspectedWorktree.structuredContent.inspection.rootValid, false);
299
289
  assertSafeRelayInspection(inspectedWorktree, {
300
290
  executionWorkspaceId: worktreeExecutionWorkspaceId,
291
+ executionBaseUrl,
292
+ executionPort,
293
+ executionOwnerToken,
294
+ bootstrapSecret,
301
295
  remoteRecord,
302
296
  taskBodies: [worktreeTaskBody],
303
297
  });
@@ -364,7 +358,7 @@ try {
364
358
  const memberTaskFile = findTaskStateFile(executionStateDir, compositeMemberTaskBody);
365
359
  assert.ok(memberTaskFile, "Execution Composite-member Task state file was not found");
366
360
  const memberExecutionWorkspaceId = memberTaskFile.split("/").at(-2);
367
- assertTaskBodyAbsentFromGateway(compositeMemberTaskBody);
361
+ assertTaskBodyAbsentFromGateway(gatewayStateDir, compositeMemberTaskBody);
368
362
 
369
363
  const compositeOpened = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "open_workspace", {
370
364
  kind: "composite",
@@ -445,6 +439,10 @@ try {
445
439
  assertToolOk(memberInspection, "inspect relayed Composite member");
446
440
  assertSafeRelayInspection(memberInspection, {
447
441
  executionWorkspaceId: memberExecutionWorkspaceId,
442
+ executionBaseUrl,
443
+ executionPort,
444
+ executionOwnerToken,
445
+ bootstrapSecret,
448
446
  remoteRecord,
449
447
  taskBodies: [compositeMemberTaskBody],
450
448
  });
@@ -489,7 +487,7 @@ try {
489
487
  }, conversationB);
490
488
  assertToolOk(restoredMemberTasks, "restore relayed Composite member Tasks");
491
489
  assert.equal(restoredMemberTasks.structuredContent.result.lists[0].tasks[0].subject, "Remote Composite member Task");
492
- assertTaskBodyAbsentFromGateway(compositeMemberTaskBody);
490
+ assertTaskBodyAbsentFromGateway(gatewayStateDir, compositeMemberTaskBody);
493
491
  pass("Composite local + Relay member", "member execution/lifecycle/Task truth stayed with owning Workspace");
494
492
 
495
493
  const deleteComposite = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "close_workspace", {
@@ -616,7 +614,7 @@ function authorizeHost(baseUrl, mcpUrl, ownerToken) {
616
614
  redirect_uri: redirectUri,
617
615
  code_challenge: challenge,
618
616
  code_challenge_method: "S256",
619
- scope: "devspace",
617
+ scope: "forgerelay",
620
618
  resource: mcpUrl,
621
619
  state: "forgerelay-relay-acceptance",
622
620
  owner_token: ownerToken,
@@ -800,90 +798,3 @@ async function stopServer(child) {
800
798
  }),
801
799
  ]);
802
800
  }
803
-
804
- async function assertPortsFree(ports) {
805
- for (const port of ports) {
806
- const occupied = await new Promise((resolvePromise) => {
807
- const socket = connect({ host: "127.0.0.1", port });
808
- socket.once("connect", () => {
809
- socket.destroy();
810
- resolvePromise(true);
811
- });
812
- socket.once("error", () => resolvePromise(false));
813
- });
814
- assert.equal(occupied, false, `reserved debug port ${port} is already in use; refusing to touch the existing process`);
815
- }
816
- }
817
-
818
- function setupGitProject(root) {
819
- mkdirSync(root, { recursive: true });
820
- writeFileSync(join(root, "README.md"), "Relay managed worktree acceptance\n");
821
- runGit(root, ["init"]);
822
- runGit(root, ["config", "user.email", "forgerelay-debug@example.com"]);
823
- runGit(root, ["config", "user.name", "ForgeRelay Debug"]);
824
- runGit(root, ["add", "."]);
825
- runGit(root, ["commit", "-m", "Initial Relay acceptance commit"]);
826
- }
827
-
828
- function runGit(cwd, args) {
829
- const result = spawnSync("git", args, { cwd, encoding: "utf8" });
830
- if (result.status !== 0) {
831
- throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`);
832
- }
833
- }
834
-
835
- function taskStateFiles(stateDir) {
836
- const workspacesDir = join(stateDir, "workspaces");
837
- if (!existsSync(workspacesDir)) return [];
838
- const files = [];
839
- for (const entry of readdirSync(workspacesDir, { withFileTypes: true })) {
840
- if (!entry.isDirectory()) continue;
841
- const candidate = join(workspacesDir, entry.name, "tasks.json");
842
- if (existsSync(candidate)) files.push(candidate);
843
- }
844
- return files;
845
- }
846
-
847
- function findTaskStateFile(stateDir, needle) {
848
- return taskStateFiles(stateDir).find((path) => readFileSync(path, "utf8").includes(needle));
849
- }
850
-
851
- function assertTaskBodyAbsentFromGateway(body) {
852
- const leaked = taskStateFiles(gatewayStateDir).filter((path) => readFileSync(path, "utf8").includes(body));
853
- assert.deepEqual(leaked, [], `Gateway stored an Execution-owned Task body in: ${leaked.join(", ")}`);
854
- }
855
-
856
- function assertSafeRelayInspection(result, { executionWorkspaceId, remoteRecord, taskBodies }) {
857
- const json = JSON.stringify(result);
858
- for (const forbidden of [
859
- executionWorkspaceId,
860
- executionBaseUrl,
861
- `127.0.0.1:${executionPort}`,
862
- executionOwnerToken,
863
- remoteRecord.accessToken,
864
- remoteRecord.refreshToken,
865
- bootstrapSecret,
866
- "remoteInstanceId",
867
- "remoteWorkspaceId",
868
- "sshRoute",
869
- ...taskBodies,
870
- ]) {
871
- assert.equal(json.includes(forbidden), false, `Relay inspection leaked ${forbidden}`);
872
- }
873
- assert.doesNotMatch(json, /"ws_[0-9a-f]{10}"/);
874
- }
875
-
876
- function assertToolOk(result, label) {
877
- assert.equal(result.isError, undefined, `${label}: ${toolText(result)}`);
878
- }
879
-
880
- function toolText(result) {
881
- return (result.content ?? [])
882
- .filter((entry) => entry.type === "text")
883
- .map((entry) => entry.text)
884
- .join("\n");
885
- }
886
-
887
- function pass(label, detail) {
888
- console.log(`PASS ${label}: ${detail}`);
889
- }
@@ -0,0 +1,167 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import { once } from "node:events";
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { setTimeout as delay } from "node:timers/promises";
7
+ import { repoRoot } from "../runtime.mjs";
8
+
9
+ export function curlRequest({ method = "GET", url, headers = {}, body }) {
10
+ const marker = `__FORGERELAY_TRAFFIC_${randomUUID()}__`;
11
+ const args = [
12
+ "--silent",
13
+ "--show-error",
14
+ "--max-time",
15
+ "20",
16
+ "--request",
17
+ method,
18
+ "--dump-header",
19
+ "-",
20
+ "--output",
21
+ "-",
22
+ "--write-out",
23
+ `\n${marker}%{http_code}|%{size_upload}|%{size_download}|%{size_request}|%{size_header}`,
24
+ ];
25
+ for (const [name, value] of Object.entries(headers)) args.push("--header", `${name}: ${value}`);
26
+ if (body !== undefined) args.push("--data-binary", "@-");
27
+ args.push(url);
28
+
29
+ const result = spawnSync("curl", args, {
30
+ cwd: repoRoot,
31
+ input: body,
32
+ encoding: "utf8",
33
+ maxBuffer: 40 * 1024 * 1024,
34
+ });
35
+ if (result.status !== 0) {
36
+ throw new Error(`curl ${method} ${url} failed: ${result.stderr.trim() || `exit ${result.status}`}`);
37
+ }
38
+ const statusMarker = `\n${marker}`;
39
+ const markerIndex = result.stdout.lastIndexOf(statusMarker);
40
+ assert.notEqual(markerIndex, -1, `curl response did not contain status marker for ${url}`);
41
+ const rawResponse = result.stdout.slice(0, markerIndex);
42
+ const stats = result.stdout.slice(markerIndex + statusMarker.length).trim().split("|");
43
+ const [status, sizeUpload, sizeDownload, sizeRequest, sizeHeader] = stats.map(Number);
44
+ const separator = rawResponse.indexOf("\r\n\r\n") >= 0 ? "\r\n\r\n" : "\n\n";
45
+ const headerEnd = rawResponse.indexOf(separator);
46
+ assert.notEqual(headerEnd, -1, `curl response did not contain headers for ${url}`);
47
+ const headerBlock = rawResponse.slice(0, headerEnd);
48
+ const responseBody = rawResponse.slice(headerEnd + separator.length);
49
+ const responseHeaders = new Map();
50
+ for (const line of headerBlock.split(/\r?\n/).slice(1)) {
51
+ const colon = line.indexOf(":");
52
+ if (colon < 0) continue;
53
+ responseHeaders.set(line.slice(0, colon).trim().toLowerCase(), line.slice(colon + 1).trim());
54
+ }
55
+ return {
56
+ status,
57
+ headers: responseHeaders,
58
+ body: responseBody,
59
+ sizeUpload,
60
+ sizeDownload,
61
+ sizeRequest,
62
+ sizeHeader,
63
+ };
64
+ }
65
+
66
+ export function jsonLogEntries(path) {
67
+ if (!existsSync(path)) return [];
68
+ return readFileSync(path, "utf8")
69
+ .split(/\r?\n/)
70
+ .filter(Boolean)
71
+ .flatMap((line) => {
72
+ try {
73
+ return [JSON.parse(line)];
74
+ } catch {
75
+ return [];
76
+ }
77
+ });
78
+ }
79
+
80
+ export function requestLogEntries(path) {
81
+ return jsonLogEntries(path).filter((entry) => entry.event === "http_request");
82
+ }
83
+
84
+ export function requestBodiesForRpc(entries, rpcTarget) {
85
+ const requestIds = new Set(entries
86
+ .filter((entry) => entry.event === "mcp_request" && entry.rpcTarget === rpcTarget)
87
+ .map((entry) => entry.requestId)
88
+ .filter(Boolean));
89
+ return entries
90
+ .filter((entry) => entry.event === "http_request" && requestIds.has(entry.requestId))
91
+ .map((entry) => Number(entry.contentLength ?? 0))
92
+ .filter((value) => Number.isFinite(value));
93
+ }
94
+
95
+ export function countBy(values, keyFor) {
96
+ const result = {};
97
+ for (const value of values) {
98
+ const key = keyFor(value);
99
+ result[key] = (result[key] ?? 0) + 1;
100
+ }
101
+ return result;
102
+ }
103
+
104
+ export async function waitForHealth(child, baseUrl) {
105
+ for (let attempt = 0; attempt < 80; attempt += 1) {
106
+ if (child.exitCode !== null) throw new Error(`debug server exited before health check: ${child.exitCode}`);
107
+ try {
108
+ const response = jsonRequest(`${baseUrl}/healthz`);
109
+ if (response.status === 200) return;
110
+ } catch {
111
+ // still starting
112
+ }
113
+ await delay(100);
114
+ }
115
+ throw new Error(`debug server did not become healthy on ${baseUrl}`);
116
+ }
117
+
118
+ export async function stopServer(child) {
119
+ if (child.exitCode !== null) return;
120
+ child.kill("SIGTERM");
121
+ const exited = once(child, "exit");
122
+ await Promise.race([
123
+ exited,
124
+ delay(3000).then(() => {
125
+ if (child.exitCode === null) child.kill("SIGKILL");
126
+ }),
127
+ ]);
128
+ }
129
+
130
+ export async function assertPortsFree(ports) {
131
+ const script = [
132
+ "const net=require('node:net');",
133
+ `const ports=${JSON.stringify(ports)};`,
134
+ "let pending=ports.length; let bad=[];",
135
+ "for(const port of ports){const s=net.connect({host:'127.0.0.1',port}); s.setTimeout(250);",
136
+ "s.once('connect',()=>{bad.push(port);s.destroy();done();});",
137
+ "s.once('error',()=>{s.destroy();done();}); s.once('timeout',()=>{s.destroy();done();});}",
138
+ "function done(){if(--pending===0){if(bad.length){console.error('busy:'+bad.join(','));process.exit(2)}}}",
139
+ ].join("");
140
+ const result = spawnSync(process.execPath, ["-e", script], { encoding: "utf8" });
141
+ if (result.status !== 0) throw new Error(`reserved debug port already in use: ${result.stderr.trim()}`);
142
+ }
143
+
144
+ export function jsonBytes(value) {
145
+ if (value === undefined) return 0;
146
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
147
+ }
148
+
149
+ export function formatBytes(value) {
150
+ const bytes = Number(value);
151
+ if (!Number.isFinite(bytes)) return String(value);
152
+ if (bytes < 1024) return `${bytes.toFixed(0)} B`;
153
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
154
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`;
155
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
156
+ }
157
+
158
+
159
+ export function jsonRequest(url, options = {}) {
160
+ const response = curlRequest({
161
+ method: options.method ?? "GET",
162
+ url,
163
+ headers: options.headers,
164
+ body: options.body,
165
+ });
166
+ return { ...response, json: JSON.parse(response.body) };
167
+ }