@akira-tl/forgerelay 0.8.7 → 0.8.9

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 (246) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +10 -18
  3. package/capabilities/host-integration/GUIDE.md +2 -2
  4. package/dist/activity/{audit-store.js → history/audit-store.js} +129 -21
  5. package/dist/activity/history/bash-output-store.js +250 -0
  6. package/dist/activity/{host-turn-store.js → history/host-turn-store.js} +4 -1
  7. package/dist/activity/{query-service.js → history/query-service.js} +13 -7
  8. package/dist/activity/{lifecycle.js → runtime/lifecycle.js} +1 -1
  9. package/dist/activity/{mcp-query-tools.js → runtime/mcp-query-tools.js} +3 -3
  10. package/dist/activity/storage/history.js +12 -0
  11. package/dist/activity/storage/paths.js +56 -0
  12. package/dist/activity/storage/segmented-log.js +149 -0
  13. package/dist/cli/setup-support.js +116 -0
  14. package/dist/cli.js +18 -126
  15. package/dist/lsp/code-intelligence.js +1 -1
  16. package/dist/lsp/test-support/server-fixture.js +9 -9
  17. package/dist/mcp/filesystem/filesystem-tools.js +245 -0
  18. package/dist/mcp/hooks/command-runner.js +79 -0
  19. package/dist/{hook-cli.js → mcp/hooks/hook-cli.js} +1 -1
  20. package/dist/{hooks.js → mcp/hooks/hooks.js} +3 -80
  21. package/dist/{oauth-provider.js → mcp/oauth/oauth-provider.js} +1 -1
  22. package/dist/{oauth-store.js → mcp/oauth/oauth-store.js} +5 -4
  23. package/dist/{operations → mcp/operations}/native-bulk-mutations.js +2 -2
  24. package/dist/mcp/panel/app.js +143 -0
  25. package/dist/{mcp-app-template.js → mcp/panel/mcp-app-template.js} +2 -38
  26. package/dist/mcp/process/runtime.js +257 -0
  27. package/dist/mcp/process/tools.js +346 -0
  28. package/dist/mcp/server/core/activity-support.js +70 -0
  29. package/dist/{capabilities.js → mcp/server/core/capabilities.js} +5 -3
  30. package/dist/{capability-registry.js → mcp/server/core/capability-registry.js} +2 -2
  31. package/dist/mcp/server/core/capability-support.js +90 -0
  32. package/dist/mcp/server/core/schemas.js +216 -0
  33. package/dist/mcp/server/core/tool-support.js +207 -0
  34. package/dist/mcp/server/core/version.js +9 -0
  35. package/dist/mcp/server/operations/runtime/filesystem-tools.js +400 -0
  36. package/dist/mcp/server/operations/runtime/operation-runtime.js +523 -0
  37. package/dist/mcp/server/transport/http-server.js +301 -0
  38. package/dist/mcp/server/transport/http-support.js +43 -0
  39. package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +222 -0
  40. package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
  41. package/dist/mcp/server/workspace/runtime/workspace-open.js +552 -0
  42. package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
  43. package/dist/{config.js → runtime/config/config.js} +3 -3
  44. package/dist/{user-config.js → runtime/config/user-config.js} +14 -54
  45. package/dist/{db → runtime/state/db}/client.js +1 -1
  46. package/dist/{db → runtime/state/db}/migrations.js +34 -3
  47. package/dist/runtime/state/lock/file-lock.js +130 -0
  48. package/dist/runtime/testing/server-fixture.js +190 -0
  49. package/dist/server.js +139 -4192
  50. package/dist/subagents/providers/adapters/claude.js +86 -52
  51. package/dist/subagents/providers/adapters/codex.js +103 -39
  52. package/dist/subagents/providers/adapters/opencode.js +114 -84
  53. package/dist/subagents/providers/adapters/pi.js +59 -8
  54. package/dist/subagents/providers/availability.js +15 -30
  55. package/dist/subagents/providers/runtime/external-command.js +56 -0
  56. package/dist/subagents/sessions/capability.js +1 -1
  57. package/dist/subagents/sessions/execution.js +1 -1
  58. package/dist/subagents/sessions/mcp/activity-audit-test-support.js +30 -0
  59. package/dist/subagents/sessions/store.js +1 -1
  60. package/dist/ui/.vite/manifest.json +342 -409
  61. package/dist/ui/activity-panel-app.html +2 -4
  62. package/dist/ui/assets/activity-panel-app-7Xe0alhm.css +1 -0
  63. package/dist/ui/assets/activity-panel-app-Sl1Jro8v.js +115 -0
  64. package/dist/ui/assets/angular-html-B_yn91wW.js +1 -0
  65. package/dist/ui/assets/{angular-ts-Bxh4-O1-.js → angular-ts-DSgtloKz.js} +1 -1
  66. package/dist/ui/assets/{apl-BmYHKQ6m.js → apl-NatWtF8X.js} +1 -1
  67. package/dist/ui/assets/{astro-C51vqDKq.js → astro-BYFEVBuN.js} +1 -1
  68. package/dist/ui/assets/{blade-CdhVsmwq.js → blade-DoO7yvMR.js} +1 -1
  69. package/dist/ui/assets/c-Rl2hMCcg.js +1 -0
  70. package/dist/ui/assets/{cobol-Cf6azoN_.js → cobol-ByC5mWiB.js} +1 -1
  71. package/dist/ui/assets/{coffee-vVdGzlmt.js → coffee-Dez77Qov.js} +1 -1
  72. package/dist/ui/assets/cpp-DtaAQT9h.js +1 -0
  73. package/dist/ui/assets/{crystal-5UZy6KIX.js → crystal-Bgmbf8ec.js} +1 -1
  74. package/dist/ui/assets/css-C1tjBtVN.js +1 -0
  75. package/dist/ui/assets/{edge-CESgo4ia.js → edge-DeH6u8Wg.js} +1 -1
  76. package/dist/ui/assets/{elixir-9FKfw4cJ.js → elixir-_H3KnmWT.js} +1 -1
  77. package/dist/ui/assets/{elm-Dv7A0aLa.js → elm-CsCe3U28.js} +1 -1
  78. package/dist/ui/assets/{erb-DHFmDvM0.js → erb-CbM-L2g8.js} +1 -1
  79. package/dist/ui/assets/{git-rebase-utbU6CzV.js → git-rebase-D0s5pDh9.js} +1 -1
  80. package/dist/ui/assets/{glimmer-js-t2kiCkyB.js → glimmer-js-BfOToq_A.js} +1 -1
  81. package/dist/ui/assets/{glimmer-ts-F0Jv5okV.js → glimmer-ts-CCUOkL2K.js} +1 -1
  82. package/dist/ui/assets/glsl-DH4ec9dE.js +1 -0
  83. package/dist/ui/assets/graphql-DilAMBoI.js +1 -0
  84. package/dist/ui/assets/{hack-8yjdbaSA.js → hack-BDkNDZlF.js} +1 -1
  85. package/dist/ui/assets/haml-D_QBp6K7.js +1 -0
  86. package/dist/ui/assets/{handlebars-BBFEg4Lo.js → handlebars-DePUrTpC.js} +1 -1
  87. package/dist/ui/assets/heavy-payload-DOwZpO6n.js +290 -0
  88. package/dist/ui/assets/html-By4dNnM4.js +1 -0
  89. package/dist/ui/assets/{html-derivative-DJAQcZyc.js → html-derivative-DDw9Ef4Q.js} +1 -1
  90. package/dist/ui/assets/{http-Dg5LSYRK.js → http-B0QxWc9M.js} +1 -1
  91. package/dist/ui/assets/{hurl-Y5uYzMZR.js → hurl-vMQ4lda1.js} +1 -1
  92. package/dist/ui/assets/java-DYRe2lWG.js +1 -0
  93. package/dist/ui/assets/javascript-CxyGqwcL.js +1 -0
  94. package/dist/ui/assets/{jinja-Bt30h8Ik.js → jinja-JUwV-fZt.js} +1 -1
  95. package/dist/ui/assets/{jison-D5uZ1Nyo.js → jison-7ZqCEvS8.js} +1 -1
  96. package/dist/ui/assets/json-dbltgiiA.js +1 -0
  97. package/dist/ui/assets/jsx-oDopT7YQ.js +1 -0
  98. package/dist/ui/assets/{julia-76hRaaGz.js → julia-CkKiNSav.js} +1 -1
  99. package/dist/ui/assets/{just-CDE7W0vW.js → just-DuqCAKJ3.js} +1 -1
  100. package/dist/ui/assets/{latex-CVO2Byab.js → latex-sudcRDSn.js} +1 -1
  101. package/dist/ui/assets/{liquid-CA-AS3EX.js → liquid-Di-08Js7.js} +1 -1
  102. package/dist/ui/assets/lua-YlqdMoV0.js +1 -0
  103. package/dist/ui/assets/{marko-_BWrPfny.js → marko-CJRNyJDb.js} +1 -1
  104. package/dist/ui/assets/{mdc-PyJv85eS.js → mdc-t12CzZHs.js} +1 -1
  105. package/dist/ui/assets/{nginx-DAWVw8yu.js → nginx-57-b4XwT.js} +1 -1
  106. package/dist/ui/assets/{nim-CKE1dzpU.js → nim-BzrswYQa.js} +1 -1
  107. package/dist/ui/assets/{perl-BBhWva19.js → perl-D9JR_iIh.js} +1 -1
  108. package/dist/ui/assets/{php-ChIeaAoF.js → php-Py2gFHil.js} +1 -1
  109. package/dist/ui/assets/{pug-CMgI5afi.js → pug-PzLLy0h1.js} +1 -1
  110. package/dist/ui/assets/{qml-CjxTkcFH.js → qml-By8fwgV4.js} +1 -1
  111. package/dist/ui/assets/r-CcezTbLq.js +1 -0
  112. package/dist/ui/assets/{razor-i4DbHr8p.js → razor-dG-vdhL_.js} +1 -1
  113. package/dist/ui/assets/regexp-BlsQ-N_8.js +1 -0
  114. package/dist/ui/assets/{rst-oy_97Kl4.js → rst-C2awDCat.js} +1 -1
  115. package/dist/ui/assets/{ruby-C5zYZIyl.js → ruby-zA2W_49d.js} +1 -1
  116. package/dist/ui/assets/{sas-BnMEh2S3.js → sas-D49bhyaN.js} +1 -1
  117. package/dist/ui/assets/scss-BQb-cp01.js +1 -0
  118. package/dist/ui/assets/shellscript-Dga4H_Ni.js +1 -0
  119. package/dist/ui/assets/{shellsession-BpE5o7_3.js → shellsession-n3-5CqcF.js} +1 -1
  120. package/dist/ui/assets/{soy-COesmhTY.js → soy-XYBOf_BA.js} +1 -1
  121. package/dist/ui/assets/sql-ET4u5Cpl.js +1 -0
  122. package/dist/ui/assets/{stata-CGQEuSZq.js → stata-CLHcJHO_.js} +1 -1
  123. package/dist/ui/assets/{surrealql-Bz4JbU-9.js → surrealql-BdjQ60T4.js} +1 -1
  124. package/dist/ui/assets/{svelte-1jM2ejXj.js → svelte-B2RPtKJe.js} +1 -1
  125. package/dist/ui/assets/{templ-C-VwELcp.js → templ-CrByPW_W.js} +1 -1
  126. package/dist/ui/assets/{tex-t1rUiQCm.js → tex-DZBkg363.js} +1 -1
  127. package/dist/ui/assets/{ts-tags-Bewoa6pS.js → ts-tags-DJR3Gphb.js} +1 -1
  128. package/dist/ui/assets/tsx-CajzROyy.js +1 -0
  129. package/dist/ui/assets/{twig-DVg-vELl.js → twig-BZjk5Zda.js} +1 -1
  130. package/dist/ui/assets/typescript-B-EWToLj.js +1 -0
  131. package/dist/ui/assets/{vue-wPQ7UlMy.js → vue-D_rHCCr8.js} +1 -1
  132. package/dist/ui/assets/{vue-html-BHtJ8geX.js → vue-html-CC4WlvVt.js} +1 -1
  133. package/dist/ui/assets/{vue-vine-CNSGAWwc.js → vue-vine-BgNMVgpk.js} +1 -1
  134. package/dist/ui/assets/xml-BI86-WbH.js +1 -0
  135. package/dist/ui/assets/{xsl-3_vJ27aE.js → xsl-DXeLZV4J.js} +1 -1
  136. package/dist/ui/assets/yaml-60_PFlWa.js +1 -0
  137. package/dist/workspaces/bootstrap.js +72 -0
  138. package/dist/workspaces/context.js +302 -0
  139. package/dist/workspaces/conversation-test-support.js +86 -0
  140. package/dist/{git-worktrees.js → workspaces/git/git-worktrees.js} +1 -1
  141. package/dist/workspaces/inventory.js +131 -0
  142. package/dist/workspaces/paths.js +55 -0
  143. package/dist/{remote-auth.js → workspaces/relay/auth/remote-auth.js} +2 -2
  144. package/dist/workspaces/relay/result-support.js +111 -0
  145. package/dist/workspaces/relay/tests/test-support.js +203 -0
  146. package/dist/{remote-mcp-connection-pool.js → workspaces/relay/transport/remote-mcp-connection-pool.js} +1 -1
  147. package/dist/workspaces/relay/types.js +1 -0
  148. package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
  149. package/dist/workspaces/resources/skills.js +285 -0
  150. package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
  151. package/dist/workspaces/sessions.js +457 -0
  152. package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
  153. package/dist/workspaces.js +69 -991
  154. package/docs/chatgpt-coding-workflow.md +3 -11
  155. package/docs/configuration.md +12 -25
  156. package/docs/gotchas.md +0 -30
  157. package/docs/roadmap.md +6 -10
  158. package/docs/security.md +1 -5
  159. package/docs/setup.md +0 -11
  160. package/package.json +6 -8
  161. package/scripts/ci/architecture.mjs +109 -0
  162. package/scripts/ci/verify.mjs +5 -0
  163. package/scripts/debug/accept/bootstrap.mjs +326 -0
  164. package/scripts/debug/accept/harness.mjs +76 -0
  165. package/scripts/debug/accept/support.mjs +432 -0
  166. package/scripts/debug/accept.mjs +43 -806
  167. package/scripts/debug/relay-accept/support.mjs +135 -0
  168. package/scripts/debug/relay-accept.mjs +26 -115
  169. package/scripts/debug/traffic/support.mjs +167 -0
  170. package/scripts/debug/traffic/traffic-audit.mjs +4 -161
  171. package/scripts/{lsp-interop-support.test.mjs → lsp/interop-support.test.mjs} +1 -1
  172. package/scripts/{lsp-interop.mjs → lsp/interop.mjs} +3 -3
  173. package/scripts/wiki/sync.mjs +21 -10
  174. package/dist/activity/bash-output-store.js +0 -127
  175. package/dist/pi-tools.js +0 -110
  176. package/dist/skills.js +0 -127
  177. package/dist/ui/assets/activity-panel-app-CUAN6zyW.js +0 -2
  178. package/dist/ui/assets/angular-html-BkgDQl-G.js +0 -1
  179. package/dist/ui/assets/c-IBzBpxAO.js +0 -1
  180. package/dist/ui/assets/chunk-EyZ2wyi3.js +0 -1
  181. package/dist/ui/assets/cpp-CVHeO-Jk.js +0 -1
  182. package/dist/ui/assets/css-DEfus9J5.js +0 -1
  183. package/dist/ui/assets/glsl-_ooCjaeB.js +0 -1
  184. package/dist/ui/assets/graphql-B9LqP-cY.js +0 -1
  185. package/dist/ui/assets/haml-BU5EoZBn.js +0 -1
  186. package/dist/ui/assets/heavy-payload-CgzrutLm.js +0 -4
  187. package/dist/ui/assets/html-D-C6YZk4.js +0 -1
  188. package/dist/ui/assets/java-ppHvycj5.js +0 -1
  189. package/dist/ui/assets/javascript-DDTjKLlQ.js +0 -1
  190. package/dist/ui/assets/json-Cw91r8h1.js +0 -1
  191. package/dist/ui/assets/jsx-04X2FT_1.js +0 -1
  192. package/dist/ui/assets/lua-CiyBp52Y.js +0 -1
  193. package/dist/ui/assets/r-DfFga4si.js +0 -1
  194. package/dist/ui/assets/regexp-iC9_nbSI.js +0 -1
  195. package/dist/ui/assets/review-payload-BrLbezbq.js +0 -1
  196. package/dist/ui/assets/scrollbar-CbhpdW05.js +0 -287
  197. package/dist/ui/assets/scss-BfZXsAWQ.js +0 -1
  198. package/dist/ui/assets/shellscript-BC2srNaB.js +0 -1
  199. package/dist/ui/assets/sql-Ccv_yeUm.js +0 -1
  200. package/dist/ui/assets/tsx-CTXzrBkN.js +0 -1
  201. package/dist/ui/assets/typescript--ElHoFkw.js +0 -1
  202. package/dist/ui/assets/workspace-app-Bhj96tsR.js +0 -1
  203. package/dist/ui/assets/workspace-app-CxwJuZyS.js +0 -111
  204. package/dist/ui/assets/workspace-app-D6UR0AFl.js +0 -5
  205. package/dist/ui/assets/workspace-app-ldjBmCJR.css +0 -1
  206. package/dist/ui/assets/workspace-lifecycle-app-Cqfhx9pV.js +0 -1
  207. package/dist/ui/assets/xml-JnmX6vyS.js +0 -1
  208. package/dist/ui/assets/yaml-OAcZkP-w.js +0 -1
  209. package/dist/ui/workspace-app.html +0 -18
  210. package/dist/ui/workspace-lifecycle-app.html +0 -18
  211. package/docs/agent-profile-schema.md +0 -114
  212. package/docs/artifact-exchange.md +0 -65
  213. package/docs/versioning.md +0 -205
  214. /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
  215. /package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +0 -0
  216. /package/dist/activity/{process-output-audit.js → runtime/process-output-audit.js} +0 -0
  217. /package/dist/{artifact-error.js → mcp/artifacts/artifact-error.js} +0 -0
  218. /package/dist/{artifact-tools.js → mcp/artifacts/artifact-tools.js} +0 -0
  219. /package/dist/{incoming-artifacts.js → mcp/artifacts/incoming-artifacts.js} +0 -0
  220. /package/dist/{advertised-files.js → mcp/filesystem/advertised-files.js} +0 -0
  221. /package/dist/{apply-patch.js → mcp/filesystem/apply-patch.js} +0 -0
  222. /package/dist/{file-mutations.js → mcp/filesystem/file-mutations.js} +0 -0
  223. /package/dist/{roots.js → mcp/filesystem/roots.js} +0 -0
  224. /package/dist/{oauth → mcp/oauth}/public-url.js +0 -0
  225. /package/dist/{oauth → mcp/oauth}/router.js +0 -0
  226. /package/dist/{operations → mcp/operations}/batch/executor.js +0 -0
  227. /package/dist/{operations → mcp/operations}/batch/scheduler.js +0 -0
  228. /package/dist/{operations → mcp/operations}/batch/types.js +0 -0
  229. /package/dist/{operations → mcp/operations}/bulk-mutation.js +0 -0
  230. /package/dist/{operations → mcp/operations}/bulk-read.js +0 -0
  231. /package/dist/{operations → mcp/operations}/core-operation-executor.js +0 -0
  232. /package/dist/{process-platform.js → mcp/process/process-platform.js} +0 -0
  233. /package/dist/{process-sessions.js → mcp/process/process-sessions.js} +0 -0
  234. /package/dist/{request-meta.js → mcp/request-meta.js} +0 -0
  235. /package/dist/{mcp-sessions.js → mcp/server/transport/mcp-sessions.js} +0 -0
  236. /package/dist/{server-shutdown.js → mcp/server/transport/server-shutdown.js} +0 -0
  237. /package/dist/{logger.js → runtime/logging/logger.js} +0 -0
  238. /package/dist/{db → runtime/state/db}/schema.js +0 -0
  239. /package/dist/{composite-activity.js → workspaces/composite/composite-activity.js} +0 -0
  240. /package/dist/{composite-workspaces.js → workspaces/composite/composite-workspaces.js} +0 -0
  241. /package/dist/{git.js → workspaces/git/git.js} +0 -0
  242. /package/dist/{workspace-presentation.js → workspaces/presentation/workspace-presentation.js} +0 -0
  243. /package/dist/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +0 -0
  244. /package/dist/{workspace-task-reminders.js → workspaces/tasks/workspace-task-reminders.js} +0 -0
  245. /package/dist/{workspace-tasks.js → workspaces/tasks/workspace-tasks.js} +0 -0
  246. /package/scripts/{lsp-interop-support.mjs → lsp/interop-support.mjs} +0 -0
@@ -1,15 +1,13 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
5
- import { isRemoteMcpUnauthorized, refreshRemoteAuthentication, } from "./remote-auth.js";
6
- import { RemoteMcpConnectionPool } from "./remote-mcp-connection-pool.js";
7
- import { withRemoteServiceEndpoint } from "./remote-transport.js";
8
- import { loadForgeRelayFiles, writeForgeRelayRemote, } from "./user-config.js";
9
- const ROUTE_LOCK_RETRY_MS = 10;
10
- const ROUTE_LOCK_TIMEOUT_MS = 5_000;
11
- const ROUTE_LOCK_STALE_MS = 30_000;
12
- const ROUTE_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
5
+ import { isRemoteMcpUnauthorized, refreshRemoteAuthentication, } from "./auth/remote-auth.js";
6
+ import { RemoteMcpConnectionPool } from "./transport/remote-mcp-connection-pool.js";
7
+ import { withFileLock } from "../../runtime/state/lock/file-lock.js";
8
+ import { withRemoteServiceEndpoint } from "./transport/remote-transport.js";
9
+ import { loadForgeRelayFiles, writeForgeRelayRemote, } from "../../runtime/config/user-config.js";
10
+ import { assertRemoteToolSucceeded, stringField, copyStringField, copyBooleanField, copyNumberField, safeTaskSummary, toolResultText, remapToolResultWorkspaceId, replaceExactWorkspaceId, sanitizedRemoteError, errorMessage } from "./result-support.js";
13
11
  export class RemoteWorkspaceRelay {
14
12
  routes = new Map();
15
13
  turnRoutes = new Map();
@@ -109,7 +107,7 @@ export class RemoteWorkspaceRelay {
109
107
  throw new Error("Remote open_workspace response did not include a valid workspace mode.");
110
108
  }
111
109
  const sourceRoot = typeof structured?.sourceRoot === "string" ? structured.sourceRoot : undefined;
112
- const route = this.findOrCreateRoute({
110
+ const route = await this.findOrCreateRoute({
113
111
  remoteInstanceId: resolved.remote.instanceId,
114
112
  remoteWorkspaceId,
115
113
  root,
@@ -167,7 +165,7 @@ export class RemoteWorkspaceRelay {
167
165
  ? structured.mode
168
166
  : route.mode;
169
167
  const sourceRoot = typeof structured?.sourceRoot === "string" ? structured.sourceRoot : route.sourceRoot;
170
- this.findOrCreateRoute({
168
+ await this.findOrCreateRoute({
171
169
  remoteInstanceId: route.remoteInstanceId,
172
170
  remoteWorkspaceId: route.remoteWorkspaceId,
173
171
  root,
@@ -194,6 +192,9 @@ export class RemoteWorkspaceRelay {
194
192
  async bash(gatewayWorkspaceId, input, conversationScopeId) {
195
193
  return this.callWorkspaceTool(gatewayWorkspaceId, "bash", input, conversationScopeId);
196
194
  }
195
+ async workspaceInstruction(gatewayWorkspaceId, path, conversationScopeId) {
196
+ return this.callWorkspaceTool(gatewayWorkspaceId, "workspace_instruction", { path }, conversationScopeId);
197
+ }
197
198
  async execCommand(gatewayWorkspaceId, input, conversationScopeId) {
198
199
  return this.callWorkspaceTool(gatewayWorkspaceId, "exec_command", input, conversationScopeId);
199
200
  }
@@ -296,7 +297,7 @@ export class RemoteWorkspaceRelay {
296
297
  const remoteStructured = result.structuredContent;
297
298
  if (action === "delete") {
298
299
  this.routes.delete(gatewayWorkspaceId);
299
- this.deletePersistedRoute(gatewayWorkspaceId);
300
+ await this.deletePersistedRoute(gatewayWorkspaceId);
300
301
  }
301
302
  for (const [turnId, routedWorkspaceId] of this.turnRoutes) {
302
303
  if (routedWorkspaceId === gatewayWorkspaceId)
@@ -356,35 +357,33 @@ export class RemoteWorkspaceRelay {
356
357
  this.routes.set(workspaceId, route);
357
358
  }
358
359
  }
359
- findOrCreateRoute(input) {
360
- let selected;
360
+ async findOrCreateRoute(input) {
361
361
  mkdirSync(this.routeStateDir, { recursive: true });
362
- this.withRouteFileLock(() => {
362
+ const selected = await withFileLock(`${this.routeStatePath}.lock`, () => {
363
363
  const routes = this.readRoutesFromDisk();
364
364
  const existing = [...routes.values()]
365
365
  .filter((route) => route.remoteInstanceId === input.remoteInstanceId &&
366
366
  route.remoteWorkspaceId === input.remoteWorkspaceId)
367
367
  .sort((left, right) => left.gatewayWorkspaceId.localeCompare(right.gatewayWorkspaceId))[0];
368
- selected = {
368
+ const route = {
369
369
  gatewayWorkspaceId: existing?.gatewayWorkspaceId ?? this.allocateGatewayWorkspaceId(routes),
370
370
  ...input,
371
371
  };
372
- routes.set(selected.gatewayWorkspaceId, selected);
372
+ routes.set(route.gatewayWorkspaceId, route);
373
373
  this.writeRoutesToDisk(routes);
374
+ return route;
374
375
  });
375
- if (!selected)
376
- throw new Error("Failed to persist relayed Workspace route.");
377
376
  this.routes.set(selected.gatewayWorkspaceId, selected);
378
377
  return selected;
379
378
  }
380
- deletePersistedRoute(workspaceId) {
381
- this.updatePersistedRoutes((routes) => {
379
+ async deletePersistedRoute(workspaceId) {
380
+ await this.updatePersistedRoutes((routes) => {
382
381
  routes.delete(workspaceId);
383
382
  });
384
383
  }
385
- updatePersistedRoutes(update) {
384
+ async updatePersistedRoutes(update) {
386
385
  mkdirSync(this.routeStateDir, { recursive: true });
387
- this.withRouteFileLock(() => {
386
+ await withFileLock(`${this.routeStatePath}.lock`, () => {
388
387
  const routes = this.readRoutesFromDisk();
389
388
  update(routes);
390
389
  this.writeRoutesToDisk(routes);
@@ -418,43 +417,6 @@ export class RemoteWorkspaceRelay {
418
417
  rmSync(tempPath, { force: true });
419
418
  }
420
419
  }
421
- withRouteFileLock(operation) {
422
- const lockPath = `${this.routeStatePath}.lock`;
423
- const deadline = Date.now() + ROUTE_LOCK_TIMEOUT_MS;
424
- for (;;) {
425
- try {
426
- const fd = openSync(lockPath, "wx", 0o600);
427
- closeSync(fd);
428
- break;
429
- }
430
- catch (error) {
431
- const code = error.code;
432
- if (code !== "EEXIST")
433
- throw error;
434
- try {
435
- if (Date.now() - statSync(lockPath).mtimeMs > ROUTE_LOCK_STALE_MS) {
436
- rmSync(lockPath, { force: true });
437
- continue;
438
- }
439
- }
440
- catch (statError) {
441
- if (statError.code === "ENOENT")
442
- continue;
443
- throw statError;
444
- }
445
- if (Date.now() >= deadline) {
446
- throw new Error(`Timed out waiting for relayed workspace route lock: ${lockPath}`);
447
- }
448
- Atomics.wait(ROUTE_LOCK_SLEEP, 0, 0, ROUTE_LOCK_RETRY_MS);
449
- }
450
- }
451
- try {
452
- return operation();
453
- }
454
- finally {
455
- rmSync(lockPath, { force: true });
456
- }
457
- }
458
420
  remoteByAlias(aliasInput) {
459
421
  const alias = aliasInput.trim();
460
422
  if (!alias)
@@ -522,7 +484,7 @@ export class RemoteWorkspaceRelay {
522
484
  }
523
485
  async refreshRemote(alias, remote, endpoint) {
524
486
  const refreshed = await refreshRemoteAuthentication(remote, endpoint);
525
- writeForgeRelayRemote(alias, refreshed, this.authEnv);
487
+ await writeForgeRelayRemote(alias, refreshed, this.authEnv);
526
488
  return refreshed;
527
489
  }
528
490
  allocateGatewayWorkspaceId(routes = this.routes) {
@@ -533,114 +495,3 @@ export class RemoteWorkspaceRelay {
533
495
  return workspaceId;
534
496
  }
535
497
  }
536
- function assertRemoteToolSucceeded(alias, tool, result) {
537
- if (result.isError !== true)
538
- return;
539
- throw new Error(`Remote ForgeRelay ${alias} ${tool} failed: ${toolResultText(result)}`);
540
- }
541
- function stringField(structured, field, label) {
542
- const value = structured?.[field];
543
- if (typeof value !== "string" || !value) {
544
- throw new Error(`${label} did not include ${field}.`);
545
- }
546
- return value;
547
- }
548
- function copyStringField(source, target, field) {
549
- const value = source[field];
550
- if (typeof value === "string")
551
- target[field] = value;
552
- }
553
- function copyBooleanField(source, target, field) {
554
- const value = source[field];
555
- if (typeof value === "boolean")
556
- target[field] = value;
557
- }
558
- function copyNumberField(source, target, field) {
559
- const value = source[field];
560
- if (typeof value === "number" && Number.isFinite(value) && value >= 0)
561
- target[field] = value;
562
- }
563
- function safeTaskSummary(value) {
564
- if (!value || typeof value !== "object")
565
- return undefined;
566
- const summary = value;
567
- if (summary.level !== "summary" ||
568
- summary.version !== 1 ||
569
- typeof summary.revision !== "number" ||
570
- !Number.isInteger(summary.revision) ||
571
- summary.revision < 0 ||
572
- !Array.isArray(summary.lists)) {
573
- return undefined;
574
- }
575
- const lists = [];
576
- for (const value of summary.lists) {
577
- if (!value || typeof value !== "object")
578
- return undefined;
579
- const list = value;
580
- if (typeof list.id !== "string" ||
581
- typeof list.name !== "string" ||
582
- (list.state !== "active" && list.state !== "archived") ||
583
- typeof list.revision !== "number" || !Number.isInteger(list.revision) || list.revision <= 0 ||
584
- typeof list.taskCount !== "number" || !Number.isInteger(list.taskCount) || list.taskCount < 0 ||
585
- typeof list.unfinishedTaskCount !== "number" || !Number.isInteger(list.unfinishedTaskCount) || list.unfinishedTaskCount < 0) {
586
- return undefined;
587
- }
588
- lists.push({
589
- id: list.id,
590
- name: list.name,
591
- state: list.state,
592
- revision: list.revision,
593
- taskCount: list.taskCount,
594
- unfinishedTaskCount: list.unfinishedTaskCount,
595
- });
596
- }
597
- return {
598
- level: "summary",
599
- version: 1,
600
- revision: summary.revision,
601
- lists,
602
- };
603
- }
604
- function toolResultText(result) {
605
- return (result.content ?? [])
606
- .filter((entry) => entry.type === "text")
607
- .map((entry) => entry.text)
608
- .join("\n") || "remote tool returned an error";
609
- }
610
- function remapToolResultWorkspaceId(result, remoteWorkspaceId, gatewayWorkspaceId) {
611
- return {
612
- ...result,
613
- content: (result.content ?? []).map((entry) => entry.type === "text"
614
- ? { ...entry, text: entry.text.split(remoteWorkspaceId).join(gatewayWorkspaceId) }
615
- : entry),
616
- ...(result._meta
617
- ? { _meta: replaceExactWorkspaceId(result._meta, remoteWorkspaceId, gatewayWorkspaceId) }
618
- : {}),
619
- ...(result.structuredContent
620
- ? {
621
- structuredContent: replaceExactWorkspaceId(result.structuredContent, remoteWorkspaceId, gatewayWorkspaceId),
622
- }
623
- : {}),
624
- };
625
- }
626
- function replaceExactWorkspaceId(value, from, to) {
627
- if (typeof value === "string")
628
- return value.split(from).join(to);
629
- if (Array.isArray(value))
630
- return value.map((entry) => replaceExactWorkspaceId(entry, from, to));
631
- if (!value || typeof value !== "object")
632
- return value;
633
- return Object.fromEntries(Object.entries(value)
634
- .map(([key, entry]) => [key, replaceExactWorkspaceId(entry, from, to)]));
635
- }
636
- function sanitizedRemoteError(error, remoteWorkspaceId, gatewayWorkspaceId) {
637
- let message = errorMessage(error);
638
- if (remoteWorkspaceId && gatewayWorkspaceId) {
639
- message = message.split(remoteWorkspaceId).join(gatewayWorkspaceId);
640
- }
641
- message = message.replace(/(^|[^A-Za-z0-9_])ws_[0-9a-f]{10}(?=$|[^A-Za-z0-9_])/g, "$1[remote-workspace]");
642
- return new Error(message, { cause: error });
643
- }
644
- function errorMessage(error) {
645
- return error instanceof Error ? error.message : String(error);
646
- }
@@ -0,0 +1,285 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync, } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, extname, join, resolve, sep } from "node:path";
4
+ import { parse as parseYaml } from "yaml";
5
+ import { markAdvertisedFileSourceActivated, resolveAdvertisedFileReadPath, } from "../../mcp/filesystem/advertised-files.js";
6
+ import { expandHomePath, isPathInsideRoot } from "../../mcp/filesystem/roots.js";
7
+ const SUBAGENT_DELEGATION_NAME = "subagent-delegation";
8
+ const FRONTMATTER_DELIMITER = "---";
9
+ export function effectiveSkillPaths(config, cwd) {
10
+ const defaultPathCandidates = [
11
+ join(homedir(), ".agents", "skills"),
12
+ resolve(cwd, ".agents", "skills"),
13
+ config.configSkillsDir,
14
+ join(config.agentDir, "skills"),
15
+ ];
16
+ const defaultPaths = defaultPathCandidates.filter((path) => path !== undefined && existsSync(path));
17
+ const seen = new Set();
18
+ return [...defaultPaths, ...config.skillPaths]
19
+ .map((path) => resolveSkillPath(path, cwd))
20
+ .filter((path) => {
21
+ if (seen.has(path))
22
+ return false;
23
+ seen.add(path);
24
+ return true;
25
+ });
26
+ }
27
+ function resolveSkillPath(path, cwd) {
28
+ return resolve(cwd, expandHomePath(path));
29
+ }
30
+ export function loadWorkspaceSkills(config, cwd) {
31
+ if (!config.skillsEnabled)
32
+ return { skills: [], diagnostics: [] };
33
+ const skills = [];
34
+ const diagnostics = [];
35
+ const winners = new Map();
36
+ for (const sourcePath of effectiveSkillPaths(config, cwd)) {
37
+ const candidates = discoverSkills(sourcePath, diagnostics);
38
+ for (const skill of candidates) {
39
+ const existing = winners.get(skill.name);
40
+ if (existing) {
41
+ diagnostics.push({
42
+ type: "collision",
43
+ message: `Skill '${skill.name}' from ${skill.filePath} was ignored because ${existing.skill.filePath} was loaded first.`,
44
+ collision: {
45
+ resourceType: "skill",
46
+ name: skill.name,
47
+ winnerPath: existing.skill.filePath,
48
+ loserPath: skill.filePath,
49
+ winnerSource: existing.source,
50
+ loserSource: sourcePath,
51
+ },
52
+ });
53
+ continue;
54
+ }
55
+ winners.set(skill.name, { skill, source: sourcePath });
56
+ skills.push(skill);
57
+ }
58
+ }
59
+ if (config.subagents)
60
+ return { skills, diagnostics };
61
+ return {
62
+ skills: skills.filter((skill) => skill.name !== SUBAGENT_DELEGATION_NAME),
63
+ diagnostics: diagnostics.filter((diagnostic) => {
64
+ const collision = diagnostic.collision;
65
+ return !(collision?.resourceType === "skill" && collision.name === SUBAGENT_DELEGATION_NAME);
66
+ }),
67
+ };
68
+ }
69
+ function discoverSkills(sourcePath, diagnostics) {
70
+ if (!existsSync(sourcePath))
71
+ return [];
72
+ let stats;
73
+ try {
74
+ stats = statSync(sourcePath);
75
+ }
76
+ catch (error) {
77
+ diagnostics.push({
78
+ type: "warning",
79
+ message: `Unable to inspect skill path ${sourcePath}: ${errorMessage(error)}`,
80
+ path: sourcePath,
81
+ });
82
+ return [];
83
+ }
84
+ if (stats.isFile()) {
85
+ const skill = loadSkillFile(sourcePath, diagnostics);
86
+ return skill ? [skill] : [];
87
+ }
88
+ if (!stats.isDirectory())
89
+ return [];
90
+ const entry = join(sourcePath, "SKILL.md");
91
+ if (existsSync(entry)) {
92
+ const skill = loadSkillFile(entry, diagnostics);
93
+ return skill ? [skill] : [];
94
+ }
95
+ const skills = [];
96
+ for (const entryName of sortedDirectoryEntries(sourcePath, diagnostics)) {
97
+ const child = join(sourcePath, entryName);
98
+ let childStats;
99
+ try {
100
+ childStats = statSync(child);
101
+ }
102
+ catch (error) {
103
+ diagnostics.push({
104
+ type: "warning",
105
+ message: `Unable to inspect skill candidate ${child}: ${errorMessage(error)}`,
106
+ path: child,
107
+ });
108
+ continue;
109
+ }
110
+ if (childStats.isDirectory()) {
111
+ skills.push(...discoverSkills(child, diagnostics));
112
+ continue;
113
+ }
114
+ if (childStats.isFile() && extname(entryName).toLowerCase() === ".md") {
115
+ const skill = loadSkillFile(child, diagnostics);
116
+ if (skill)
117
+ skills.push(skill);
118
+ }
119
+ }
120
+ return skills;
121
+ }
122
+ function sortedDirectoryEntries(path, diagnostics) {
123
+ try {
124
+ return readdirSync(path).sort((left, right) => left.localeCompare(right));
125
+ }
126
+ catch (error) {
127
+ diagnostics.push({
128
+ type: "warning",
129
+ message: `Unable to read skill directory ${path}: ${errorMessage(error)}`,
130
+ path,
131
+ });
132
+ return [];
133
+ }
134
+ }
135
+ function loadSkillFile(filePath, diagnostics) {
136
+ let content;
137
+ try {
138
+ content = readFileSync(filePath, "utf8");
139
+ }
140
+ catch (error) {
141
+ diagnostics.push({
142
+ type: "warning",
143
+ message: `Unable to read skill file ${filePath}: ${errorMessage(error)}`,
144
+ path: filePath,
145
+ });
146
+ return undefined;
147
+ }
148
+ let frontmatter = {};
149
+ try {
150
+ frontmatter = parseSkillFrontmatter(content);
151
+ }
152
+ catch (error) {
153
+ diagnostics.push({
154
+ type: "error",
155
+ message: `Invalid skill frontmatter in ${filePath}: ${errorMessage(error)}`,
156
+ path: filePath,
157
+ });
158
+ return undefined;
159
+ }
160
+ const baseDir = dirname(filePath);
161
+ const name = normalizedSkillName(frontmatter.name, filePath);
162
+ if (!name) {
163
+ diagnostics.push({
164
+ type: "error",
165
+ message: `Skill ${filePath} has an invalid or empty name.`,
166
+ path: filePath,
167
+ });
168
+ return undefined;
169
+ }
170
+ const description = typeof frontmatter.description === "string"
171
+ ? frontmatter.description.trim()
172
+ : "";
173
+ return {
174
+ name,
175
+ description,
176
+ filePath: resolve(filePath),
177
+ baseDir: resolve(baseDir),
178
+ disableModelInvocation: frontmatter["disable-model-invocation"] === true,
179
+ };
180
+ }
181
+ function parseSkillFrontmatter(content) {
182
+ const lines = content.replace(/^\uFEFF/, "").split(/\r?\n/);
183
+ if (lines[0]?.trim() !== FRONTMATTER_DELIMITER)
184
+ return {};
185
+ const end = lines.findIndex((line, index) => index > 0 && line.trim() === FRONTMATTER_DELIMITER);
186
+ if (end < 0)
187
+ throw new Error("opening '---' is missing a closing delimiter");
188
+ const parsed = parseYaml(lines.slice(1, end).join("\n"));
189
+ if (parsed === null || parsed === undefined)
190
+ return {};
191
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
192
+ throw new Error("frontmatter must be a YAML mapping");
193
+ }
194
+ return parsed;
195
+ }
196
+ function normalizedSkillName(value, filePath) {
197
+ const explicit = typeof value === "string" ? value.trim() : "";
198
+ if (explicit)
199
+ return explicit;
200
+ if (basename(filePath).toLowerCase() === "skill.md")
201
+ return basename(dirname(filePath)).trim();
202
+ return basename(filePath, extname(filePath)).trim();
203
+ }
204
+ function errorMessage(error) {
205
+ return error instanceof Error ? error.message : String(error);
206
+ }
207
+ export function resolveSkillReadPath(skills, activatedSkillDirs, inputPath) {
208
+ const virtualRead = resolveVirtualSkillReadPath(skills, activatedSkillDirs, inputPath);
209
+ if (virtualRead)
210
+ return virtualRead;
211
+ const resolution = resolveAdvertisedFileReadPath(skills, activatedSkillDirs, inputPath);
212
+ if (!resolution)
213
+ return undefined;
214
+ return {
215
+ absolutePath: resolution.absolutePath,
216
+ skill: resolution.source,
217
+ isSkillFile: resolution.isEntryFile,
218
+ };
219
+ }
220
+ function resolveVirtualSkillReadPath(skills, activatedSkillDirs, inputPath) {
221
+ const prefix = "skills://";
222
+ if (!inputPath.startsWith(prefix))
223
+ return undefined;
224
+ const requested = inputPath.slice(prefix.length);
225
+ const slashIndex = requested.indexOf("/");
226
+ const encodedName = slashIndex === -1 ? requested : requested.slice(0, slashIndex);
227
+ if (!encodedName)
228
+ throw new Error(`Invalid skill URI: ${inputPath}`);
229
+ let name;
230
+ try {
231
+ name = decodeURIComponent(encodedName);
232
+ }
233
+ catch {
234
+ throw new Error(`Invalid skill URI: ${inputPath}`);
235
+ }
236
+ const skill = skills.find((candidate) => candidate.name === name);
237
+ if (!skill)
238
+ throw new Error(`Unknown advertised skill: ${name}`);
239
+ if (slashIndex === -1 || slashIndex === requested.length - 1) {
240
+ return {
241
+ absolutePath: resolve(skill.filePath),
242
+ skill,
243
+ isSkillFile: true,
244
+ };
245
+ }
246
+ const relativePart = requested.slice(slashIndex + 1);
247
+ const segments = relativePart.split("/").map((segment) => {
248
+ let decoded;
249
+ try {
250
+ decoded = decodeURIComponent(segment);
251
+ }
252
+ catch {
253
+ throw new Error(`Invalid skill URI: ${inputPath}`);
254
+ }
255
+ if (!decoded || decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\")) {
256
+ throw new Error(`Invalid skill URI: ${inputPath}`);
257
+ }
258
+ return decoded;
259
+ });
260
+ const baseDir = resolve(skill.baseDir);
261
+ if (!activatedSkillDirs.has(baseDir))
262
+ return undefined;
263
+ const absolutePath = resolve(baseDir, ...segments);
264
+ if (!isPathInsideRoot(absolutePath, baseDir)) {
265
+ throw new Error(`Skill resource is outside its skill directory: ${inputPath}`);
266
+ }
267
+ return {
268
+ absolutePath,
269
+ skill,
270
+ isSkillFile: false,
271
+ };
272
+ }
273
+ export function markSkillActivated(activatedSkillDirs, skill) {
274
+ markAdvertisedFileSourceActivated(activatedSkillDirs, skill);
275
+ }
276
+ export function formatPathForPrompt(path) {
277
+ const home = resolve(homedir());
278
+ const resolvedPath = resolve(path);
279
+ if (resolvedPath === home)
280
+ return "~";
281
+ if (resolvedPath.startsWith(`${home}${sep}`)) {
282
+ return `~/${resolvedPath.slice(home.length + 1).split(sep).join("/")}`;
283
+ }
284
+ return resolvedPath.split(sep).join("/");
285
+ }
@@ -1,8 +1,8 @@
1
1
  import { mkdtemp, rm } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { git, getGitEligibility, safeWorkspaceRefSegment } from "./git.js";
5
- const REVIEW_REF_PREFIX = "refs/devspace/review";
4
+ import { git, getGitEligibility, safeWorkspaceRefSegment } from "../git/git.js";
5
+ const REVIEW_REF_PREFIX = "refs/forgerelay/review";
6
6
  const DEFAULT_MAX_REVIEW_WORKSPACE_STATES = 128;
7
7
  export function createReviewCheckpointManager(options = {}) {
8
8
  const maxWorkspaceStates = options.maxWorkspaceStates ?? DEFAULT_MAX_REVIEW_WORKSPACE_STATES;