@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,432 @@
1
+ import assert from "node:assert/strict";
2
+ import { 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 { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { setTimeout as delay } from "node:timers/promises";
9
+ import { debugBaseUrl, debugMcpUrl, repoRoot } from "../runtime.mjs";
10
+
11
+ export function authorizeDebugClient(metadata, ownerToken) {
12
+ const redirectUri = `${debugBaseUrl}/debug/callback`;
13
+ const registration = jsonRequest(metadata.registration_endpoint, {
14
+ method: "POST",
15
+ body: JSON.stringify({
16
+ client_name: "ForgeRelay 7677 acceptance",
17
+ redirect_uris: [redirectUri],
18
+ token_endpoint_auth_method: "none",
19
+ grant_types: ["authorization_code", "refresh_token"],
20
+ response_types: ["code"],
21
+ }),
22
+ headers: { "content-type": "application/json" },
23
+ });
24
+ assert.equal(registration.status, 201);
25
+
26
+ const verifier = randomBytes(32).toString("base64url");
27
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
28
+ const authorizationBody = new URLSearchParams({
29
+ response_type: "code",
30
+ client_id: registration.json.client_id,
31
+ redirect_uri: redirectUri,
32
+ code_challenge: challenge,
33
+ code_challenge_method: "S256",
34
+ scope: "forgerelay",
35
+ resource: debugMcpUrl,
36
+ state: "forgerelay-debug-acceptance",
37
+ owner_token: ownerToken,
38
+ }).toString();
39
+ const authorization = curlRequest({
40
+ method: "POST",
41
+ url: metadata.authorization_endpoint,
42
+ headers: { "content-type": "application/x-www-form-urlencoded" },
43
+ body: authorizationBody,
44
+ });
45
+ assert.equal(authorization.status, 302);
46
+ const redirect = new URL(authorization.headers.get("location"));
47
+ assert.equal(redirect.origin + redirect.pathname, redirectUri);
48
+ assert.equal(redirect.searchParams.get("state"), "forgerelay-debug-acceptance");
49
+ const code = redirect.searchParams.get("code");
50
+ assert.ok(code);
51
+
52
+ const token = jsonRequest(metadata.token_endpoint, {
53
+ method: "POST",
54
+ headers: { "content-type": "application/x-www-form-urlencoded" },
55
+ body: new URLSearchParams({
56
+ grant_type: "authorization_code",
57
+ client_id: registration.json.client_id,
58
+ code,
59
+ redirect_uri: redirectUri,
60
+ code_verifier: verifier,
61
+ resource: debugMcpUrl,
62
+ }).toString(),
63
+ });
64
+ assert.equal(token.status, 200);
65
+ assert.equal(token.json.token_type, "bearer");
66
+ assert.equal(token.json.scope, "forgerelay");
67
+ assert.ok(token.json.access_token);
68
+ return { accessToken: token.json.access_token };
69
+ }
70
+
71
+ export function initializeRequest(id) {
72
+ return {
73
+ jsonrpc: "2.0",
74
+ id,
75
+ method: "initialize",
76
+ params: {
77
+ protocolVersion: "2025-06-18",
78
+ capabilities: {
79
+ extensions: {
80
+ "io.modelcontextprotocol/ui": {
81
+ mimeTypes: ["text/html;profile=mcp-app"],
82
+ },
83
+ },
84
+ },
85
+ clientInfo: { name: "forgerelay-debug-acceptance", version: "1.0.0" },
86
+ },
87
+ };
88
+ }
89
+
90
+ export function mcpRequest(accessToken, sessionId, request) {
91
+ const response = curlRequest({
92
+ method: "POST",
93
+ url: debugMcpUrl,
94
+ headers: mcpHeaders(accessToken, sessionId),
95
+ body: JSON.stringify(request),
96
+ });
97
+ assert.equal(response.status, 200, response.body);
98
+ return { response, message: parseMcpMessage(response.body, request.id) };
99
+ }
100
+
101
+ export function callTool(accessToken, sessionId, id, name, args, meta) {
102
+ const message = mcpRequest(accessToken, sessionId, {
103
+ jsonrpc: "2.0",
104
+ id,
105
+ method: "tools/call",
106
+ params: {
107
+ name,
108
+ arguments: args,
109
+ ...(meta ? { _meta: meta } : {}),
110
+ },
111
+ }).message;
112
+ assert.equal(message.id, id);
113
+ assert.ok(message.result, `tool ${name} did not return a result`);
114
+ return message.result;
115
+ }
116
+
117
+ export function mcpHeaders(accessToken, sessionId) {
118
+ return {
119
+ ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
120
+ ...(sessionId ? { "mcp-session-id": sessionId } : {}),
121
+ "content-type": "application/json",
122
+ accept: "application/json, text/event-stream",
123
+ };
124
+ }
125
+
126
+ export function parseMcpMessage(body, expectedId) {
127
+ const dataLines = body
128
+ .split(/\r?\n/)
129
+ .filter((line) => line.startsWith("data:"))
130
+ .map((line) => line.slice(5).trim())
131
+ .filter(Boolean);
132
+ const messages = dataLines.length > 0
133
+ ? dataLines.map((line) => JSON.parse(line))
134
+ : [JSON.parse(body)];
135
+ return messages.find((message) => message.id === expectedId) ?? messages[0];
136
+ }
137
+
138
+ export function jsonRequest(url, options = {}) {
139
+ const response = curlRequest({
140
+ method: options.method ?? "GET",
141
+ url,
142
+ headers: options.headers,
143
+ body: options.body,
144
+ });
145
+ return { ...response, json: JSON.parse(response.body) };
146
+ }
147
+
148
+ export function curlRequest({ method = "GET", url, headers = {}, body }) {
149
+ const marker = `__FORGERELAY_DEBUG_STATUS_${randomUUID()}__`;
150
+ const args = [
151
+ "--silent",
152
+ "--show-error",
153
+ "--max-time",
154
+ "15",
155
+ "--request",
156
+ method,
157
+ "--dump-header",
158
+ "-",
159
+ "--output",
160
+ "-",
161
+ "--write-out",
162
+ `\n${marker}%{http_code}`,
163
+ ];
164
+ for (const [name, value] of Object.entries(headers)) {
165
+ args.push("--header", `${name}: ${value}`);
166
+ }
167
+ if (body !== undefined) {
168
+ args.push("--data-binary", "@-");
169
+ }
170
+ args.push(url);
171
+
172
+ const result = spawnSync("curl", args, {
173
+ cwd: repoRoot,
174
+ input: body,
175
+ encoding: "utf8",
176
+ maxBuffer: 20 * 1024 * 1024,
177
+ });
178
+ if (result.status !== 0) {
179
+ throw new Error(`curl ${method} ${url} failed: ${result.stderr.trim() || `exit ${result.status}`}`);
180
+ }
181
+
182
+ const statusMarker = `\n${marker}`;
183
+ const markerIndex = result.stdout.lastIndexOf(statusMarker);
184
+ assert.notEqual(markerIndex, -1, `curl response did not contain status marker for ${url}`);
185
+ const rawResponse = result.stdout.slice(0, markerIndex);
186
+ const status = Number(result.stdout.slice(markerIndex + statusMarker.length).trim());
187
+ const separator = rawResponse.indexOf("\r\n\r\n") >= 0 ? "\r\n\r\n" : "\n\n";
188
+ const headerEnd = rawResponse.indexOf(separator);
189
+ assert.notEqual(headerEnd, -1, `curl response did not contain headers for ${url}`);
190
+ const headerBlock = rawResponse.slice(0, headerEnd);
191
+ const responseBody = rawResponse.slice(headerEnd + separator.length);
192
+ const responseHeaders = new Map();
193
+ for (const line of headerBlock.split(/\r?\n/).slice(1)) {
194
+ const colon = line.indexOf(":");
195
+ if (colon < 0) continue;
196
+ responseHeaders.set(line.slice(0, colon).trim().toLowerCase(), line.slice(colon + 1).trim());
197
+ }
198
+ return { status, headers: responseHeaders, body: responseBody };
199
+ }
200
+
201
+ export async function waitForHealth(child) {
202
+ for (let attempt = 0; attempt < 60; attempt += 1) {
203
+ if (child.exitCode !== null) {
204
+ throw new Error(`debug server exited before health check: ${child.exitCode}`);
205
+ }
206
+ try {
207
+ const response = jsonRequest(`${debugBaseUrl}/healthz`);
208
+ if (response.status === 200) return response.json;
209
+ } catch {
210
+ // Server is still starting.
211
+ }
212
+ await delay(100);
213
+ }
214
+ throw new Error("debug server did not become healthy on 127.0.0.1:7677");
215
+ }
216
+
217
+ export async function stopServer(child) {
218
+ if (child.exitCode !== null) return;
219
+ child.kill("SIGTERM");
220
+ const exited = once(child, "exit");
221
+ await Promise.race([
222
+ exited,
223
+ delay(3000).then(() => {
224
+ if (child.exitCode === null) child.kill("SIGKILL");
225
+ }),
226
+ ]);
227
+ }
228
+
229
+ export function setupGitProject(root) {
230
+ mkdirSync(root, { recursive: true });
231
+ writeFileSync(join(root, "README.md"), "debug git project\n", "utf8");
232
+ runGit(root, ["init"]);
233
+ runGit(root, ["config", "user.email", "forgerelay-debug@example.com"]);
234
+ runGit(root, ["config", "user.name", "ForgeRelay Debug"]);
235
+ runGit(root, ["add", "."]);
236
+ runGit(root, ["commit", "-m", "Initial debug commit"]);
237
+ }
238
+
239
+ export function runGit(cwd, args) {
240
+ const result = spawnSync("git", args, { cwd, encoding: "utf8" });
241
+ if (result.status !== 0) {
242
+ throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`);
243
+ }
244
+ }
245
+
246
+ export function gitOutput(cwd, args, options = {}) {
247
+ const gitArgs = options.gitDir ? ["--git-dir", cwd, ...args] : args;
248
+ const result = spawnSync("git", gitArgs, {
249
+ ...(options.gitDir ? {} : { cwd }),
250
+ encoding: "utf8",
251
+ });
252
+ if (result.status !== 0) {
253
+ throw new Error(`git ${gitArgs.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`);
254
+ }
255
+ return result.stdout.trim();
256
+ }
257
+
258
+ export function exerciseReleaseTagHooks(accessToken, sessionId, { acceptanceRoot, releaseProject, releaseRemote }) {
259
+ setupGitProject(releaseProject);
260
+ runGit(acceptanceRoot, ["init", "--bare", releaseRemote]);
261
+ runGit(releaseProject, ["remote", "add", "origin", releaseRemote]);
262
+ mkdirSync(join(releaseProject, ".forgerelay", "hooks"), { recursive: true });
263
+ writeFileSync(
264
+ join(releaseProject, ".forgerelay", "release-check.mjs"),
265
+ [
266
+ 'import { writeFileSync } from "node:fs";',
267
+ 'const payload = process.env.FORGERELAY_HOOK_PAYLOAD ?? "{}";',
268
+ 'const parsed = JSON.parse(payload);',
269
+ 'writeFileSync("release-ci-ran.txt", payload);',
270
+ 'if (parsed.command === "git push origin v0.2.1") process.exit(17);',
271
+ "",
272
+ ].join("\n"),
273
+ );
274
+ writeFileSync(
275
+ join(releaseProject, ".forgerelay", "hooks", "release-tag-local-ci.json"),
276
+ JSON.stringify({
277
+ event: "BeforeTool",
278
+ matcher: { tool: "bash", commandRegex: "git push origin v0\\.2\\.[01]" },
279
+ command: "node .forgerelay/release-check.mjs",
280
+ timeoutSeconds: 30,
281
+ report: true,
282
+ }, null, 2) + "\n",
283
+ );
284
+ runGit(releaseProject, ["add", ".forgerelay"]);
285
+ runGit(releaseProject, ["commit", "-m", "Add release hook fixture"]);
286
+ runGit(releaseProject, ["tag", "v0.2.0"]);
287
+ runGit(releaseProject, ["tag", "v0.2.1"]);
288
+
289
+ const opened = callTool(accessToken, sessionId, 11, "open_workspace", {
290
+ path: releaseProject,
291
+ });
292
+ const releaseWorkspaceId = opened.structuredContent.workspaceId;
293
+
294
+ const pushed = callTool(accessToken, sessionId, 12, "bash", {
295
+ workspaceId: releaseWorkspaceId,
296
+ command: "git status --short && git push origin v0.2.0 && echo release-pushed",
297
+ });
298
+ assert.equal(pushed.isError, undefined);
299
+ assert.match(toolText(pushed), /release-tag-local-ci \(BeforeTool, project\) passed/);
300
+ assert.ok(existsSync(join(releaseProject, "release-ci-ran.txt")));
301
+ assert.deepEqual(JSON.parse(readFileSync(join(releaseProject, "release-ci-ran.txt"), "utf8")), {
302
+ tool: "bash",
303
+ action: "run",
304
+ command: "git push origin v0.2.0",
305
+ workingDirectory: ".",
306
+ originalCommand: "git status --short && git push origin v0.2.0 && echo release-pushed",
307
+ });
308
+ assert.equal(
309
+ gitOutput(releaseRemote, ["rev-parse", "refs/tags/v0.2.0"], { gitDir: true }),
310
+ gitOutput(releaseProject, ["rev-parse", "v0.2.0"]),
311
+ );
312
+
313
+ const blocked = callTool(accessToken, sessionId, 13, "bash", {
314
+ workspaceId: releaseWorkspaceId,
315
+ command: "git status --short && git push origin v0.2.1 && echo should-not-run",
316
+ });
317
+ assert.equal(blocked.isError, true);
318
+ assert.match(toolText(blocked), /release-tag-local-ci.*failed/);
319
+ const missingTag = spawnSync("git", ["--git-dir", releaseRemote, "show-ref", "--verify", "--quiet", "refs/tags/v0.2.1"]);
320
+ assert.notEqual(missingTag.status, 0, "blocked release tag unexpectedly reached the remote");
321
+ pass("release tag hook gate", "local Hook passed before v0.2.0 push and blocked v0.2.1 before remote mutation");
322
+ }
323
+
324
+ export function exerciseSubagentHooks(runtimeEnv, debugStateDir, workspaceId, workspaceRoot, acceptanceRoot) {
325
+ const seedScript = [
326
+ 'import { SubagentSessionStore } from "./src/subagents/sessions/store.js";',
327
+ 'const store = new SubagentSessionStore(process.env.FORGERELAY_STATE_DIR);',
328
+ 'const record = store.create({',
329
+ ` workspaceId: ${JSON.stringify(workspaceId)},`,
330
+ ` workspaceRoot: ${JSON.stringify(workspaceRoot)},`,
331
+ ' profileName: "debug-missing-profile",',
332
+ ' provider: "codex",',
333
+ ' activeRun: { id: "run_debug_acceptance", startedAt: new Date().toISOString() },',
334
+ '});',
335
+ 'store.close();',
336
+ 'process.stdout.write(record.id);',
337
+ ].join("\n");
338
+ const seeded = spawnSync(process.execPath, ["--import", "tsx", "--input-type=module", "-e", seedScript], {
339
+ cwd: repoRoot,
340
+ env: { ...runtimeEnv, FORGERELAY_STATE_DIR: debugStateDir },
341
+ encoding: "utf8",
342
+ });
343
+ if (seeded.status !== 0) {
344
+ throw new Error(`unable to seed debug subagent: ${seeded.stderr.trim()}`);
345
+ }
346
+ const agentId = seeded.stdout.trim();
347
+ assert.match(agentId, /^agt_/);
348
+
349
+ const promptFile = join(acceptanceRoot, "subagent-prompt.txt");
350
+ writeFileSync(promptFile, "debug acceptance prompt that must not reach a provider\n", "utf8");
351
+ const worker = spawnSync(
352
+ process.execPath,
353
+ ["--import", "tsx", "src/cli.ts", "agents", "__worker", agentId, "--prompt-file", promptFile],
354
+ { cwd: repoRoot, env: runtimeEnv, encoding: "utf8", maxBuffer: 2 * 1024 * 1024 },
355
+ );
356
+ if (worker.status !== 0) {
357
+ throw new Error(`debug subagent worker failed to execute: ${worker.stderr.trim()}`);
358
+ }
359
+
360
+ const inspectScript = [
361
+ 'import { SubagentSessionStore } from "./src/subagents/sessions/store.js";',
362
+ 'import { SubagentDeliveryMailbox } from "./src/subagents/sessions/delivery-mailbox.js";',
363
+ 'const store = new SubagentSessionStore(process.env.FORGERELAY_STATE_DIR);',
364
+ `const record = store.get(${JSON.stringify(agentId)});`,
365
+ `const deliveries = new SubagentDeliveryMailbox(process.env.FORGERELAY_STATE_DIR).claimSession(${JSON.stringify(workspaceId)}, ${JSON.stringify(agentId)});`,
366
+ 'store.close();',
367
+ 'process.stdout.write(JSON.stringify({ status: record?.status, latestRun: record?.latestRun, delivery: deliveries[0] }));',
368
+ ].join("\n");
369
+ const inspected = spawnSync(process.execPath, ["--import", "tsx", "--input-type=module", "-e", inspectScript], {
370
+ cwd: repoRoot,
371
+ env: runtimeEnv,
372
+ encoding: "utf8",
373
+ });
374
+ if (inspected.status !== 0) {
375
+ throw new Error(`unable to inspect debug subagent: ${inspected.stderr.trim()}`);
376
+ }
377
+ const record = JSON.parse(inspected.stdout);
378
+ assert.equal(record.status, "idle");
379
+ assert.equal(record.latestRun?.status, "failed");
380
+ assert.equal(record.delivery?.outcome, "failed");
381
+ assert.match(record.delivery?.error ?? "", /Subagent profile not found/);
382
+ pass("subagent hook path", `${agentId} stopped in deterministic failed Run without calling a provider`);
383
+ }
384
+
385
+ export function toolText(result) {
386
+ return (result.content ?? [])
387
+ .filter((entry) => entry?.type === "text" && typeof entry.text === "string")
388
+ .map((entry) => entry.text)
389
+ .join("\n");
390
+ }
391
+
392
+ export function readHookEntries(path) {
393
+ return readFileSync(path, "utf8")
394
+ .trim()
395
+ .split(/\r?\n/)
396
+ .filter(Boolean)
397
+ .map((line) => JSON.parse(line));
398
+ }
399
+
400
+ export async function assertDebugPortFree() {
401
+ await new Promise((resolvePromise, rejectPromise) => {
402
+ const socket = connect({ host: "127.0.0.1", port: 7677 });
403
+ socket.setTimeout(500);
404
+ socket.once("connect", () => {
405
+ socket.destroy();
406
+ rejectPromise(new Error("debug port 7677 is already in use; stop the existing debug server first"));
407
+ });
408
+ socket.once("error", (error) => {
409
+ socket.destroy();
410
+ if (error.code === "ECONNREFUSED") {
411
+ resolvePromise();
412
+ return;
413
+ }
414
+ rejectPromise(error);
415
+ });
416
+ socket.once("timeout", () => {
417
+ socket.destroy();
418
+ resolvePromise();
419
+ });
420
+ });
421
+ }
422
+
423
+ export function assertCurlAvailable() {
424
+ const result = spawnSync("curl", ["--version"], { encoding: "utf8" });
425
+ if (result.status !== 0) {
426
+ throw new Error("npm run debug:accept requires curl on PATH.");
427
+ }
428
+ }
429
+
430
+ export function pass(label, detail) {
431
+ console.log(`✓ ${label}: ${detail}`);
432
+ }