@akira-tl/forgerelay 0.8.8 → 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 +12 -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 +116 -4202
  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 -410
  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 +68 -995
  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-7AckaxIR.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-Bol1_mcs.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-BqJp0c5e.js +0 -1
  196. package/dist/ui/assets/scrollbar-CvE-I-jG.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-BLW7p6IZ.js +0 -114
  203. package/dist/ui/assets/workspace-app-BjNqR0en.js +0 -1
  204. package/dist/ui/assets/workspace-app-CmaYU4DW.js +0 -3
  205. package/dist/ui/assets/workspace-app-D2bJ5fjt.css +0 -1
  206. package/dist/ui/assets/workspace-lifecycle-app-BVbI2Ilf.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,13 +1,14 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- import { realpathSync } from "node:fs";
3
- import { mkdir, opendir, readFile, realpath, stat } from "node:fs/promises";
4
- import { tmpdir } from "node:os";
5
- import { basename, dirname, join, relative, resolve, sep } from "node:path";
6
- import { loadCapabilityGuides, markCapabilityGuideActivated, resolveCapabilityGuideReadPath, } from "./capabilities.js";
7
- import { HookRunner } from "./hooks.js";
8
- import { closeManagedWorktree, createManagedWorktree, discardFreshManagedWorktree, resolveManagedWorktreeBase, } from "./git-worktrees.js";
9
- import { AccessDeniedError, assertAllowedPath, isPathInsideRoot, resolveAllowedPath, } from "./roots.js";
10
- import { loadWorkspaceSkills, markSkillActivated, resolveSkillReadPath, } from "./skills.js";
1
+ import { randomBytes } from "node:crypto";
2
+ import { resolve } from "node:path";
3
+ import { loadCapabilityGuides, } from "./mcp/server/core/capabilities.js";
4
+ import { WorkspaceContextService, formatAgentsPath } from "./workspaces/context.js";
5
+ import { BOOTSTRAP_CONTEXT_COMPONENTS, bootstrapContextFingerprints, resolveBootstrapContextComponents, } from "./workspaces/bootstrap.js";
6
+ import { canonicalPath, ensureCheckoutWorkspaceRoot } from "./workspaces/paths.js";
7
+ import { WorkspaceInventoryService } from "./workspaces/inventory.js";
8
+ import { WorkspaceSessionService } from "./workspaces/sessions.js";
9
+ import { HookRunner } from "./mcp/hooks/hooks.js";
10
+ import { closeManagedWorktree, createManagedWorktree, resolveManagedWorktreeBase, } from "./workspaces/git/git-worktrees.js";
11
+ import { assertAllowedPath, } from "./mcp/filesystem/roots.js";
11
12
  import { loadSubagentProfiles, } from "./subagents/profiles.js";
12
13
  const WORKSPACE_STALE_REMINDER_MS = 2 * 24 * 60 * 60 * 1_000;
13
14
  const WORKSPACE_SESSION_IDLE_TTL_MS = 30 * 24 * 60 * 60 * 1_000;
@@ -17,21 +18,25 @@ export class WorkspaceRegistry {
17
18
  config;
18
19
  store;
19
20
  workspaces = new Map();
20
- pendingOpens = new Map();
21
21
  hooks;
22
- lastWorkspaceGcAt = 0;
22
+ context;
23
+ sessions;
24
+ inventory;
23
25
  constructor(config, store) {
24
26
  this.config = config;
25
27
  this.store = store;
26
28
  this.hooks = new HookRunner(config.hooks, config.logging);
27
- this.foldLegacyWorkspaceSessions();
28
- this.pruneIdleWorkspaceSessions(new Set(), true);
29
+ this.context = new WorkspaceContextService(config);
30
+ this.sessions = new WorkspaceSessionService(config, store, this.workspaces, this.context);
31
+ this.inventory = new WorkspaceInventoryService(config, store, this.workspaces, this.sessions);
32
+ this.sessions.foldLegacyWorkspaceSessions();
33
+ this.sessions.pruneIdleWorkspaceSessions(new Set(), true);
29
34
  }
30
35
  get cachedWorkspaceCount() {
31
36
  return this.workspaces.size;
32
37
  }
33
38
  async openWorkspace(input, openOptions = {}) {
34
- this.pruneIdleWorkspaceSessions(openOptions.protectedWorkspaceIds ?? new Set());
39
+ this.sessions.pruneIdleWorkspaceSessions(openOptions.protectedWorkspaceIds ?? new Set());
35
40
  const workspaceInput = typeof input === "string" ? { path: input } : input;
36
41
  const bootstrapContext = workspaceInput.context ?? "auto";
37
42
  if (workspaceInput.workspaceId) {
@@ -46,148 +51,17 @@ export class WorkspaceRegistry {
46
51
  }
47
52
  return this.openReusableCheckout(workspaceInput.path, openOptions.conversationScopeId, bootstrapContext);
48
53
  }
49
- async inspectWorkspace(workspaceId) {
50
- let session = this.store?.getSession(workspaceId);
51
- if (!session) {
52
- const workspace = this.workspaces.get(workspaceId);
53
- if (workspace) {
54
- session = {
55
- id: workspace.id,
56
- root: workspace.root,
57
- status: "active",
58
- mode: workspace.mode,
59
- sourceRoot: workspace.sourceRoot,
60
- baseRef: workspace.worktree?.baseRef,
61
- baseSha: workspace.worktree?.baseSha,
62
- branch: workspace.worktree?.branch,
63
- targetBranch: workspace.worktree?.targetBranch,
64
- managed: workspace.worktree?.managed ?? false,
65
- createdAt: "",
66
- lastUsedAt: "",
67
- };
68
- }
69
- }
70
- if (!session)
71
- throw new Error(`Unknown workspaceId: ${workspaceId}.`);
72
- const entry = await this.inventoryEntryForSession(session, Date.now(), false);
73
- const { current: _current, ...inspection } = entry;
74
- return {
75
- kind: "workspace",
76
- location: "local",
77
- ...inspection,
78
- };
54
+ inspectWorkspace(workspaceId) {
55
+ return this.inventory.inspectWorkspace(workspaceId);
79
56
  }
80
- async listWorkspaces(input = {}, openOptions = {}) {
81
- this.pruneIdleWorkspaceSessions(openOptions.protectedWorkspaceIds ?? new Set());
82
- const now = Date.now();
83
- const sessions = this.store
84
- ? this.store.listSessions()
85
- : [...this.workspaces.values()].map((workspace) => ({
86
- id: workspace.id,
87
- root: workspace.root,
88
- status: "active",
89
- mode: workspace.mode,
90
- sourceRoot: workspace.sourceRoot,
91
- baseRef: workspace.worktree?.baseRef,
92
- baseSha: workspace.worktree?.baseSha,
93
- branch: workspace.worktree?.branch,
94
- targetBranch: workspace.worktree?.targetBranch,
95
- managed: workspace.worktree?.managed ?? false,
96
- createdAt: "",
97
- lastUsedAt: "",
98
- }));
99
- const currentWorkspaceIds = new Set(openOptions.conversationScopeId && this.store
100
- ? this.store
101
- .listConversationBindings()
102
- .filter((binding) => binding.conversationScopeId === openOptions.conversationScopeId)
103
- .map((binding) => binding.workspaceSessionId)
104
- : []);
105
- const workspaceIdFilter = input.workspaceId
106
- ? this.store?.getSession(input.workspaceId)?.id ?? input.workspaceId
107
- : undefined;
108
- const rootKey = input.root
109
- ? await canonicalPath(assertAllowedPath(input.root, [...this.config.allowedRoots, this.config.worktreeRoot]))
110
- : undefined;
111
- const entries = await Promise.all(sessions.map((session) => this.inventoryEntryForSession(session, now, currentWorkspaceIds.has(session.id))));
112
- const filtered = [];
113
- for (let index = 0; index < sessions.length; index += 1) {
114
- const session = sessions[index];
115
- const entry = entries[index];
116
- if (!session || !entry)
117
- continue;
118
- if (workspaceIdFilter && entry.workspaceId !== workspaceIdFilter)
119
- continue;
120
- if (input.status && entry.status !== input.status)
121
- continue;
122
- if (input.state && entry.state !== input.state)
123
- continue;
124
- if (input.mode && entry.mode !== input.mode)
125
- continue;
126
- if (input.staleOnly && entry.state !== "stale")
127
- continue;
128
- if (rootKey) {
129
- const sessionRootKey = await canonicalPath(session.root);
130
- const sourceRootKey = session.sourceRoot ? await canonicalPath(session.sourceRoot) : undefined;
131
- if (sessionRootKey !== rootKey && sourceRootKey !== rootKey)
132
- continue;
133
- }
134
- filtered.push(entry);
135
- }
136
- const summary = filtered.reduce((counts, entry) => {
137
- counts[entry.state] += 1;
138
- return counts;
139
- }, { active: 0, stale: 0, invalid: 0, closed: 0 });
140
- const offset = Math.max(0, input.offset ?? 0);
141
- const limit = Math.min(100, Math.max(1, input.limit ?? 50));
142
- return {
143
- workspaces: filtered.slice(offset, offset + limit),
144
- summary: {
145
- total: entries.length,
146
- matching: filtered.length,
147
- ...summary,
148
- },
149
- page: {
150
- offset,
151
- limit,
152
- hasMore: offset + limit < filtered.length,
153
- },
154
- };
155
- }
156
- async inventoryEntryForSession(session, now, current) {
157
- const rootValid = await this.validSessionRoot(session) !== undefined;
158
- const lastUsedAt = Date.parse(session.lastUsedAt);
159
- const idleMs = Number.isFinite(lastUsedAt) ? Math.max(0, now - lastUsedAt) : 0;
160
- const state = session.status !== "active"
161
- ? "closed"
162
- : !rootValid
163
- ? "invalid"
164
- : idleMs >= WORKSPACE_STALE_REMINDER_MS
165
- ? "stale"
166
- : "active";
167
- const projectRoot = session.sourceRoot ?? session.root;
168
- return {
169
- label: `${basename(resolve(projectRoot)) || "workspace"}/${session.id}`,
170
- workspaceId: session.id,
171
- root: session.root,
172
- status: session.status,
173
- state,
174
- mode: session.mode,
175
- sourceRoot: session.sourceRoot,
176
- branch: session.branch,
177
- targetBranch: session.targetBranch,
178
- managed: session.managed,
179
- createdAt: session.createdAt,
180
- lastUsedAt: session.lastUsedAt,
181
- idleMs,
182
- rootValid,
183
- current,
184
- };
57
+ listWorkspaces(input = {}, openOptions = {}) {
58
+ return this.inventory.listWorkspaces(input, openOptions);
185
59
  }
186
60
  async resumeWorkspace(workspaceId, conversationScopeId, bootstrapContext = "auto") {
187
61
  const session = this.store?.getSession(workspaceId);
188
62
  const context = session?.status === "closed" && session.mode === "worktree"
189
- ? await this.reopenClosedManagedWorktreeContext(session)
190
- : await this.reusedWorkspaceContext(await this.workspaceForOpen(workspaceId));
63
+ ? await this.sessions.reopenClosedManagedWorktreeContext(session)
64
+ : await this.sessions.reusedWorkspaceContext(await this.sessions.workspaceForOpen(workspaceId));
191
65
  const workspace = context.workspace;
192
66
  if (!conversationScopeId || !this.store) {
193
67
  const bootstrapContextComponents = resolveBootstrapContextComponents(bootstrapContext, context.bootstrapComponentFingerprints, []);
@@ -197,7 +71,7 @@ export class WorkspaceRegistry {
197
71
  includeBootstrapContext: bootstrapContextComponents.length > 0,
198
72
  };
199
73
  }
200
- const targetKeys = await this.workspaceTargetKeys(workspace);
74
+ const targetKeys = await this.sessions.workspaceTargetKeys(workspace);
201
75
  const deliveries = targetKeys
202
76
  .map((targetKey) => this.store?.getContextDelivery(conversationScopeId, targetKey))
203
77
  .filter((delivery) => delivery !== undefined);
@@ -232,7 +106,7 @@ export class WorkspaceRegistry {
232
106
  const lastUsedAt = Date.parse(session.lastUsedAt);
233
107
  if (!Number.isFinite(lastUsedAt) || now - lastUsedAt < WORKSPACE_STALE_REMINDER_MS)
234
108
  continue;
235
- const root = await this.validSessionRoot(session);
109
+ const root = await this.sessions.validSessionRoot(session);
236
110
  if (!root || await canonicalPath(root) !== workspaceRootKey)
237
111
  continue;
238
112
  stale.push({
@@ -255,7 +129,7 @@ export class WorkspaceRegistry {
255
129
  throw new Error(`Workspace ${canonicalWorkspaceId} is backed by a managed worktree. Use close_workspace with its managed-worktree finalize lifecycle.`);
256
130
  }
257
131
  if (this.store) {
258
- this.deleteConversationBindingsForWorkspace(canonicalWorkspaceId);
132
+ this.sessions.deleteConversationBindingsForWorkspace(canonicalWorkspaceId);
259
133
  this.store.setSessionStatus(canonicalWorkspaceId, "closed");
260
134
  }
261
135
  this.workspaces.delete(canonicalWorkspaceId);
@@ -265,13 +139,13 @@ export class WorkspaceRegistry {
265
139
  if (session.mode === "worktree" && session.status === "active") {
266
140
  throw new Error(`Workspace ${session.id} is an active managed-worktree Workspace. Finalize it safely before deleting its persistent identity.`);
267
141
  }
268
- this.deleteConversationBindingsForWorkspace(session.id);
142
+ this.sessions.deleteConversationBindingsForWorkspace(session.id);
269
143
  this.store?.deleteSession(session.id);
270
144
  this.workspaces.delete(session.id);
271
145
  }
272
146
  workspaceIdsForPhysicalWorkspace(workspace) {
273
147
  const root = resolve(workspace.root);
274
- return this.activeSessions(workspace.mode)
148
+ return this.sessions.activeSessions(workspace.mode)
275
149
  .filter((session) => resolve(session.root) === root)
276
150
  .map((session) => session.id);
277
151
  }
@@ -284,12 +158,12 @@ export class WorkspaceRegistry {
284
158
  ? await canonicalPath(workspace.root)
285
159
  : undefined;
286
160
  const resultsByRoot = new Map();
287
- for (const session of this.activeSessions("worktree")) {
161
+ for (const session of this.sessions.activeSessions("worktree")) {
288
162
  if (!session.sourceRoot)
289
163
  continue;
290
164
  if (await canonicalPath(session.sourceRoot) !== sourceKey)
291
165
  continue;
292
- const root = await this.validSessionRoot(session);
166
+ const root = await this.sessions.validSessionRoot(session);
293
167
  if (!root)
294
168
  continue;
295
169
  const rootKey = await canonicalPath(root);
@@ -342,7 +216,7 @@ export class WorkspaceRegistry {
342
216
  targetBranch: managedWorktree.targetBranch,
343
217
  },
344
218
  });
345
- const aliasedWorkspaceIds = this.activeSessions("worktree")
219
+ const aliasedWorkspaceIds = this.sessions.activeSessions("worktree")
346
220
  .filter((session) => resolve(session.root) === resolve(workspace.root))
347
221
  .map((session) => session.id);
348
222
  const result = await closeManagedWorktree({
@@ -367,7 +241,7 @@ export class WorkspaceRegistry {
367
241
  },
368
242
  }));
369
243
  for (const aliasedWorkspaceId of aliasedWorkspaceIds) {
370
- this.deleteConversationBindingsForWorkspace(aliasedWorkspaceId);
244
+ this.sessions.deleteConversationBindingsForWorkspace(aliasedWorkspaceId);
371
245
  this.store?.setSessionStatus(aliasedWorkspaceId, "closed");
372
246
  this.workspaces.delete(aliasedWorkspaceId);
373
247
  }
@@ -377,37 +251,37 @@ export class WorkspaceRegistry {
377
251
  const allowedPath = assertAllowedPath(path, this.config.allowedRoots);
378
252
  const projectKey = await canonicalPath(allowedPath);
379
253
  const targetKey = JSON.stringify(["checkout", projectKey, null]);
380
- const boundContext = await this.boundConversationContext(conversationScopeId, targetKey, "checkout", async (session, root) => session.mode === "checkout" && await canonicalPath(root) === projectKey, bootstrapContext);
254
+ const boundContext = await this.sessions.boundConversationContext(conversationScopeId, targetKey, "checkout", async (session, root) => session.mode === "checkout" && await canonicalPath(root) === projectKey, bootstrapContext);
381
255
  if (boundContext)
382
256
  return boundContext;
383
- const context = await this.openOnce(targetKey, async () => {
384
- const reusableWorkspace = await this.findReusableWorkspaceByDirectory(projectKey, "checkout", true);
257
+ const context = await this.sessions.openOnce(targetKey, async () => {
258
+ const reusableWorkspace = await this.sessions.findReusableWorkspaceByDirectory(projectKey, "checkout", true);
385
259
  if (!reusableWorkspace)
386
260
  return this.openCheckoutWorkspace(path);
387
- return this.reusedWorkspaceContext(reusableWorkspace);
261
+ return this.sessions.reusedWorkspaceContext(reusableWorkspace);
388
262
  });
389
- return this.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
263
+ return this.sessions.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
390
264
  }
391
265
  async openReusableWorktree(input, conversationScopeId) {
392
266
  const path = input.path;
393
267
  if (!path)
394
268
  throw new Error("Worktree mode requires path.");
395
269
  const bootstrapContext = input.context ?? "auto";
396
- const managedPath = this.tryManagedWorktreePath(path);
270
+ const managedPath = this.sessions.tryManagedWorktreePath(path);
397
271
  if (managedPath) {
398
272
  const worktreeKey = await canonicalPath(managedPath);
399
273
  const targetKey = JSON.stringify(["worktree-path", worktreeKey]);
400
- const boundContext = await this.boundConversationContext(conversationScopeId, targetKey, "worktree", async (session, root) => session.mode === "worktree" && await canonicalPath(root) === worktreeKey, bootstrapContext);
274
+ const boundContext = await this.sessions.boundConversationContext(conversationScopeId, targetKey, "worktree", async (session, root) => session.mode === "worktree" && await canonicalPath(root) === worktreeKey, bootstrapContext);
401
275
  if (boundContext)
402
276
  return boundContext;
403
- const context = await this.openOnce(targetKey, async () => {
404
- const reusableWorkspace = await this.findReusableWorkspaceByDirectory(worktreeKey, "worktree");
277
+ const context = await this.sessions.openOnce(targetKey, async () => {
278
+ const reusableWorkspace = await this.sessions.findReusableWorkspaceByDirectory(worktreeKey, "worktree");
405
279
  if (!reusableWorkspace) {
406
280
  throw new Error(`Managed worktree is not registered as an active ForgeRelay workspace: ${managedPath}. Open the source project in worktree mode to create or recover a managed worktree first.`);
407
281
  }
408
- return this.reusedWorkspaceContext(reusableWorkspace);
282
+ return this.sessions.reusedWorkspaceContext(reusableWorkspace);
409
283
  });
410
- return this.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
284
+ return this.sessions.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
411
285
  }
412
286
  const resolvedBase = await resolveManagedWorktreeBase({
413
287
  sourcePath: path,
@@ -418,533 +292,43 @@ export class WorkspaceRegistry {
418
292
  const targetKey = JSON.stringify(["worktree", sourceKey, resolvedBase.targetBranch]);
419
293
  if (input.newWorktree) {
420
294
  const context = await this.openWorktreeWorkspace(path, input.baseRef);
421
- return this.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
295
+ return this.sessions.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
422
296
  }
423
- const boundContext = await this.boundConversationContext(conversationScopeId, targetKey, "worktree", async (session) => session.mode === "worktree" &&
297
+ const boundContext = await this.sessions.boundConversationContext(conversationScopeId, targetKey, "worktree", async (session) => session.mode === "worktree" &&
424
298
  session.sourceRoot !== undefined &&
425
299
  await canonicalPath(session.sourceRoot) === sourceKey &&
426
300
  session.targetBranch === resolvedBase.targetBranch, bootstrapContext);
427
301
  if (boundContext)
428
302
  return boundContext;
429
- const context = await this.openOnce(targetKey, async () => {
430
- const reusableContext = await this.findReusableWorktreeContextBySource(sourceKey, resolvedBase.targetBranch);
303
+ const context = await this.sessions.openOnce(targetKey, async () => {
304
+ const reusableContext = await this.sessions.findReusableWorktreeContextBySource(sourceKey, resolvedBase.targetBranch);
431
305
  return reusableContext ?? this.openWorktreeWorkspace(path, input.baseRef);
432
306
  });
433
- return this.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
434
- }
435
- async openOnce(operationKey, open) {
436
- const pending = this.pendingOpens.get(operationKey);
437
- if (pending) {
438
- const context = await pending;
439
- return { ...context, workspaceReused: true };
440
- }
441
- const operation = open();
442
- this.pendingOpens.set(operationKey, operation);
443
- try {
444
- return await operation;
445
- }
446
- finally {
447
- if (this.pendingOpens.get(operationKey) === operation) {
448
- this.pendingOpens.delete(operationKey);
449
- }
450
- }
451
- }
452
- async workspaceTargetKeys(workspace) {
453
- if (workspace.mode === "checkout") {
454
- return [JSON.stringify(["checkout", await canonicalPath(workspace.root), null])];
455
- }
456
- const keys = [JSON.stringify(["worktree-path", await canonicalPath(workspace.root)])];
457
- if (workspace.sourceRoot && workspace.worktree?.targetBranch) {
458
- keys.push(JSON.stringify([
459
- "worktree",
460
- await canonicalPath(workspace.sourceRoot),
461
- workspace.worktree.targetBranch,
462
- ]));
463
- }
464
- return keys;
465
- }
466
- async boundConversationContext(conversationScopeId, targetKey, mode, matches, bootstrapContext) {
467
- if (!conversationScopeId || !this.store)
468
- return undefined;
469
- const binding = this.store.getConversationBinding(conversationScopeId, targetKey);
470
- if (!binding)
471
- return undefined;
472
- const session = this.store.getSession(binding.workspaceSessionId);
473
- if (!session || session.status !== "active" || session.mode !== mode) {
474
- this.store.deleteConversationBinding(conversationScopeId, targetKey);
475
- return undefined;
476
- }
477
- const root = await this.validSessionRoot(session);
478
- if (!root) {
479
- this.store.deleteConversationBinding(conversationScopeId, targetKey);
480
- return undefined;
481
- }
482
- if (!await matches(session, root)) {
483
- this.store.deleteConversationBinding(conversationScopeId, targetKey);
484
- return undefined;
485
- }
486
- const context = await this.reusedWorkspaceContext(this.getWorkspace(session.id));
487
- return this.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
488
- }
489
- withConversationContext(context, conversationScopeId, targetKey, bootstrapContext) {
490
- if (!conversationScopeId || !this.store) {
491
- const bootstrapContextComponents = resolveBootstrapContextComponents(bootstrapContext, context.bootstrapComponentFingerprints, []);
492
- return {
493
- ...context,
494
- bootstrapContextComponents,
495
- includeBootstrapContext: bootstrapContextComponents.length > 0,
496
- };
497
- }
498
- const delivery = this.store.getContextDelivery(conversationScopeId, targetKey);
499
- const bootstrapContextComponents = resolveBootstrapContextComponents(bootstrapContext, context.bootstrapComponentFingerprints, delivery ? [delivery] : [], context.contextFingerprint);
500
- const includeBootstrapContext = bootstrapContextComponents.length > 0;
501
- this.store.setConversationBinding({
502
- conversationScopeId,
503
- targetKey,
504
- workspaceSessionId: context.workspace.id,
505
- });
506
- if (bootstrapContext !== "none" &&
507
- (includeBootstrapContext ||
508
- (delivery?.contextFingerprint === context.contextFingerprint && !delivery.componentFingerprints))) {
509
- this.store.setContextDelivery({
510
- conversationScopeId,
511
- targetKey,
512
- contextFingerprint: context.contextFingerprint,
513
- componentFingerprints: context.bootstrapComponentFingerprints,
514
- });
515
- }
516
- return { ...context, bootstrapContextComponents, includeBootstrapContext };
517
- }
518
- pruneIdleWorkspaceSessions(protectedWorkspaceIds, force = false) {
519
- if (!this.store)
520
- return;
521
- const now = Date.now();
522
- if (!force && now - this.lastWorkspaceGcAt < WORKSPACE_GC_INTERVAL_MS)
523
- return;
524
- this.lastWorkspaceGcAt = now;
525
- const activeSessions = this.store.listSessions({ status: "active" });
526
- const sessionsById = new Map(activeSessions.map((session) => [session.id, session]));
527
- const isIdle = (session) => {
528
- const lastUsedAt = Date.parse(session.lastUsedAt);
529
- return Number.isFinite(lastUsedAt) && now - lastUsedAt >= WORKSPACE_SESSION_IDLE_TTL_MS;
530
- };
531
- for (const binding of this.store.listConversationBindings()) {
532
- const session = sessionsById.get(binding.workspaceSessionId);
533
- if (!session) {
534
- this.store.deleteConversationBinding(binding.conversationScopeId, binding.targetKey);
535
- }
536
- }
537
- for (const delivery of this.store.listContextDeliveries()) {
538
- const deliveredAt = Date.parse(delivery.deliveredAt);
539
- if (Number.isFinite(deliveredAt) &&
540
- now - deliveredAt >= WORKSPACE_SESSION_IDLE_TTL_MS) {
541
- this.store.deleteContextDelivery(delivery.conversationScopeId, delivery.targetKey);
542
- }
543
- }
544
- const boundWorkspaceIds = new Set(this.store.listConversationBindings().map((binding) => binding.workspaceSessionId));
545
- const worktreeAnchors = new Map();
546
- for (const session of activeSessions) {
547
- if (session.mode !== "worktree")
548
- continue;
549
- const key = resolve(session.root);
550
- const current = worktreeAnchors.get(key);
551
- if (!current || current.lastUsedAt < session.lastUsedAt) {
552
- worktreeAnchors.set(key, session);
553
- }
554
- }
555
- for (const session of activeSessions) {
556
- if (!isIdle(session))
557
- continue;
558
- if (protectedWorkspaceIds.has(session.id) || boundWorkspaceIds.has(session.id))
559
- continue;
560
- if (session.mode === "worktree" &&
561
- worktreeAnchors.get(resolve(session.root))?.id === session.id) {
562
- continue;
563
- }
564
- this.workspaces.delete(session.id);
565
- }
566
- }
567
- foldLegacyWorkspaceSessions() {
568
- if (!this.store)
569
- return;
570
- const groups = new Map();
571
- for (const session of this.store.listSessions()) {
572
- const targetPath = canonicalPersistedWorkspacePath(session.root);
573
- const key = JSON.stringify([session.mode, targetPath]);
574
- const group = groups.get(key) ?? [];
575
- group.push(session);
576
- groups.set(key, group);
577
- }
578
- for (const sessions of groups.values()) {
579
- if (sessions.length < 2)
580
- continue;
581
- const ordered = [...sessions].sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
582
- const canonical = ordered[0];
583
- if (!canonical)
584
- continue;
585
- const createdAt = ordered.reduce((earliest, session) => session.createdAt < earliest ? session.createdAt : earliest, canonical.createdAt);
586
- const lastUsedAt = ordered.reduce((latest, session) => session.lastUsedAt > latest ? session.lastUsedAt : latest, canonical.lastUsedAt);
587
- const status = ordered.some((session) => session.status === "active")
588
- ? "active"
589
- : canonical.status;
590
- this.store.foldSessions({
591
- canonicalId: canonical.id,
592
- aliasIds: ordered.slice(1).map((session) => session.id),
593
- createdAt,
594
- lastUsedAt,
595
- status,
596
- });
597
- }
598
- }
599
- async findReusableWorkspaceByDirectory(directoryKey, mode, reopenClosedCheckout = false) {
600
- const sessions = this.store
601
- ? this.store.listSessions({ mode })
602
- : this.activeSessions(mode);
603
- for (const session of sessions) {
604
- const reusable = session.status === "active" ||
605
- (reopenClosedCheckout && mode === "checkout" && session.status === "closed");
606
- if (!reusable)
607
- continue;
608
- const root = await this.validSessionRoot(session);
609
- if (!root)
610
- continue;
611
- if (await canonicalPath(root) !== directoryKey)
612
- continue;
613
- if (session.status === "closed") {
614
- this.store?.setSessionStatus(session.id, "active");
615
- const reopened = this.store?.getSession(session.id);
616
- if (!reopened)
617
- continue;
618
- return this.workspaceFromSession(reopened, false);
619
- }
620
- return this.workspaceFromSession(session, false);
621
- }
622
- return undefined;
623
- }
624
- async findReusableWorktreeContextBySource(sourceKey, targetBranch) {
625
- const sessions = this.store
626
- ? this.store.listSessions({ mode: "worktree" })
627
- : this.activeSessions("worktree");
628
- const closedMatches = [];
629
- for (const session of sessions) {
630
- if (!session.sourceRoot || session.targetBranch !== targetBranch)
631
- continue;
632
- if (await canonicalPath(session.sourceRoot) !== sourceKey)
633
- continue;
634
- if (session.status === "closed") {
635
- if (session.managed)
636
- closedMatches.push(session);
637
- continue;
638
- }
639
- if (session.status !== "active")
640
- continue;
641
- const root = await this.validSessionRoot(session);
642
- if (!root)
643
- continue;
644
- return this.reusedWorkspaceContext(this.workspaceFromSession(session, false));
645
- }
646
- if (closedMatches.length === 1) {
647
- return this.reopenClosedManagedWorktreeContext(closedMatches[0]);
648
- }
649
- return undefined;
650
- }
651
- activeSessions(mode) {
652
- if (this.store) {
653
- return this.store.listSessions({ status: "active", mode });
654
- }
655
- return [...this.workspaces.values()]
656
- .filter((workspace) => workspace.mode === mode)
657
- .map((workspace) => ({
658
- id: workspace.id,
659
- root: workspace.root,
660
- status: "active",
661
- mode: workspace.mode,
662
- sourceRoot: workspace.sourceRoot,
663
- baseRef: workspace.worktree?.baseRef,
664
- baseSha: workspace.worktree?.baseSha,
665
- branch: workspace.worktree?.branch,
666
- targetBranch: workspace.worktree?.targetBranch,
667
- managed: workspace.worktree?.managed ?? false,
668
- createdAt: "",
669
- lastUsedAt: "",
670
- }));
671
- }
672
- async validSessionRoot(session) {
673
- try {
674
- const root = this.assertWorkspaceRootAllowed(session.root, session.mode, session.sourceRoot);
675
- const rootStats = await stat(root);
676
- return rootStats.isDirectory() ? root : undefined;
677
- }
678
- catch (error) {
679
- if (error instanceof AccessDeniedError ||
680
- (isErrnoException(error) && (error.code === "ENOENT" || error.code === "ENOTDIR"))) {
681
- return undefined;
682
- }
683
- throw error;
684
- }
685
- }
686
- tryManagedWorktreePath(path) {
687
- try {
688
- return assertAllowedPath(path, [this.config.worktreeRoot]);
689
- }
690
- catch (error) {
691
- if (error instanceof AccessDeniedError)
692
- return undefined;
693
- throw error;
694
- }
695
- }
696
- async reusedWorkspaceContext(workspace) {
697
- Object.assign(workspace, this.loadSkillsForWorkspace(workspace.root));
698
- workspace.capabilityGuides = loadCapabilityGuides(this.config);
699
- workspace.agentProfiles = await loadSubagentProfiles(this.config, workspace.root);
700
- workspace.scannedInstructionDirs.clear();
701
- workspace.knownInstructionPathsByDir.clear();
702
- workspace.loadedInstructionRealPaths.clear();
703
- workspace.loadedInstructionPaths.clear();
704
- const agentsFiles = await this.loadInitialAgentsFiles(workspace);
705
- const availableAgentsFiles = await this.findAvailableAgentsFiles(workspace, agentsFiles);
706
- const { contextFingerprint, componentFingerprints: bootstrapComponentFingerprints, } = bootstrapContextFingerprints(workspace, agentsFiles, availableAgentsFiles);
707
- return {
708
- workspace,
709
- agentsFiles,
710
- availableAgentsFiles,
711
- contextFingerprint,
712
- bootstrapComponentFingerprints,
713
- bootstrapContextComponents: [...BOOTSTRAP_CONTEXT_COMPONENTS],
714
- hookReports: [],
715
- workspaceReused: true,
716
- includeBootstrapContext: true,
717
- };
718
- }
719
- async workspaceForOpen(workspaceId) {
720
- const session = this.store?.getSession(workspaceId);
721
- if (session?.status === "closed" && session.mode === "checkout") {
722
- this.store?.setSessionStatus(session.id, "active");
723
- const reopened = this.store?.getSession(session.id);
724
- if (!reopened) {
725
- throw new Error(`Unknown workspaceId: ${workspaceId}. Call open_workspace first.`);
726
- }
727
- return this.workspaceFromSession(reopened, true);
728
- }
729
- return this.getWorkspace(workspaceId);
730
- }
731
- async reopenClosedManagedWorktreeContext(session) {
732
- const operationKey = JSON.stringify(["worktree-reopen", session.id]);
733
- return this.openOnce(operationKey, async () => {
734
- const current = this.store?.getSession(session.id);
735
- if (!current) {
736
- throw new Error(`Unknown workspaceId: ${session.id}. Call open_workspace first.`);
737
- }
738
- if (current.status === "active") {
739
- return this.reusedWorkspaceContext(this.getWorkspace(current.id));
740
- }
741
- if (current.status !== "closed" || current.mode !== "worktree") {
742
- throw new Error(`Workspace ${current.id} is not a closed managed-worktree Workspace.`);
743
- }
744
- return this.reopenClosedManagedWorktreeContextUnlocked(current);
745
- });
746
- }
747
- async reopenClosedManagedWorktreeContextUnlocked(session) {
748
- if (!this.store) {
749
- throw new Error(`Workspace ${session.id} cannot be reopened without persistent Workspace state.`);
750
- }
751
- if (!session.managed || !session.sourceRoot || !session.targetBranch) {
752
- throw new Error(`Workspace ${session.id} does not have enough managed-worktree metadata to recreate its execution backing.`);
753
- }
754
- const worktree = await createManagedWorktree({
755
- sourcePath: session.sourceRoot,
756
- baseRef: session.targetBranch,
757
- config: this.config,
758
- });
759
- const candidateSession = {
760
- ...session,
761
- root: worktree.path,
762
- status: "active",
763
- sourceRoot: worktree.sourceRoot,
764
- baseRef: worktree.baseRef,
765
- baseSha: worktree.baseSha,
766
- branch: worktree.branch,
767
- targetBranch: worktree.targetBranch,
768
- managed: true,
769
- };
770
- const workspace = this.workspaceFromSession(candidateSession, false);
771
- try {
772
- const context = await this.reusedWorkspaceContext(workspace);
773
- this.store.replaceWorktreeBacking({
774
- id: session.id,
775
- root: worktree.path,
776
- sourceRoot: worktree.sourceRoot,
777
- baseRef: worktree.baseRef,
778
- baseSha: worktree.baseSha,
779
- branch: worktree.branch,
780
- targetBranch: worktree.targetBranch,
781
- });
782
- return context;
783
- }
784
- catch (error) {
785
- this.workspaces.delete(session.id);
786
- try {
787
- await discardFreshManagedWorktree({ worktree, config: this.config });
788
- }
789
- catch (cleanupError) {
790
- const original = error instanceof Error ? error.message : String(error);
791
- const cleanup = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
792
- throw new Error(`${original} Reopen rollback also failed: ${cleanup}`);
793
- }
794
- throw error;
795
- }
307
+ return this.sessions.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
796
308
  }
797
309
  getWorkspaceSession(workspaceId) {
798
- const session = this.store?.getSession(workspaceId);
799
- if (session)
800
- return session;
801
- const workspace = this.workspaces.get(workspaceId);
802
- if (!workspace) {
803
- throw new Error(`Unknown workspaceId: ${workspaceId}. Call open_workspace first.`);
804
- }
805
- return {
806
- id: workspace.id,
807
- root: workspace.root,
808
- status: "active",
809
- mode: workspace.mode,
810
- sourceRoot: workspace.sourceRoot,
811
- baseRef: workspace.worktree?.baseRef,
812
- baseSha: workspace.worktree?.baseSha,
813
- branch: workspace.worktree?.branch,
814
- targetBranch: workspace.worktree?.targetBranch,
815
- managed: workspace.worktree?.managed ?? false,
816
- createdAt: "",
817
- lastUsedAt: "",
818
- };
819
- }
820
- deleteConversationBindingsForWorkspace(workspaceId) {
821
- if (!this.store)
822
- return;
823
- for (const binding of this.store.listConversationBindings()) {
824
- if (binding.workspaceSessionId === workspaceId) {
825
- this.store.deleteConversationBinding(binding.conversationScopeId, binding.targetKey);
826
- }
827
- }
310
+ return this.sessions.getWorkspaceSession(workspaceId);
828
311
  }
829
312
  getWorkspace(workspaceId) {
830
- const workspace = this.workspaces.get(workspaceId);
831
- if (workspace) {
832
- this.store?.touchSession(workspaceId);
833
- return workspace;
834
- }
835
- const session = this.store?.getSession(workspaceId);
836
- if (!session || session.status !== "active") {
837
- throw new Error(`Unknown workspaceId: ${workspaceId}. Call open_workspace first.`);
838
- }
839
- return this.workspaceFromSession(session, true);
840
- }
841
- workspaceFromSession(session, touch) {
842
- const existing = this.workspaces.get(session.id);
843
- if (existing) {
844
- if (touch)
845
- this.store?.touchSession(session.id);
846
- return existing;
847
- }
848
- const root = this.assertWorkspaceRootAllowed(session.root, session.mode, session.sourceRoot);
849
- const restoredWorkspace = {
850
- id: session.id,
851
- root,
852
- mode: session.mode,
853
- sourceRoot: session.sourceRoot,
854
- worktree: session.mode === "worktree"
855
- ? {
856
- path: root,
857
- baseRef: session.baseRef ?? "HEAD",
858
- baseSha: session.baseSha ?? "",
859
- branch: session.branch,
860
- targetBranch: session.targetBranch,
861
- dirtySource: false,
862
- detached: !session.branch,
863
- managed: session.managed,
864
- }
865
- : undefined,
866
- ...this.loadSkillsForWorkspace(root),
867
- capabilityGuides: loadCapabilityGuides(this.config),
868
- agentProfiles: [],
869
- activatedSkillDirs: new Set(),
870
- activatedCapabilityGuideDirs: new Set(),
871
- scannedInstructionDirs: new Set(),
872
- knownInstructionPathsByDir: new Map(),
873
- loadedInstructionRealPaths: new Set(),
874
- loadedInstructionPaths: new Set(),
875
- };
876
- if (touch)
877
- this.store?.touchSession(session.id);
878
- this.workspaces.set(restoredWorkspace.id, restoredWorkspace);
879
- return restoredWorkspace;
313
+ return this.sessions.getWorkspace(workspaceId);
880
314
  }
881
315
  fileToolRoots(workspace) {
882
- return [workspace.root, tmpdir()];
316
+ return this.context.fileToolRoots(workspace);
883
317
  }
884
318
  resolvePath(workspace, inputPath) {
885
- const absolutePath = resolveAllowedPath(inputPath, workspace.root, [workspace.root]);
886
- if (!isPathInsideRoot(absolutePath, workspace.root)) {
887
- throw new Error(`Path is outside workspace root: ${inputPath}`);
888
- }
889
- return absolutePath;
319
+ return this.context.resolvePath(workspace, inputPath);
890
320
  }
891
321
  resolveReadPath(workspace, inputPath) {
892
- if (inputPath.startsWith("skills://")) {
893
- const skillRead = resolveSkillReadPath(workspace.skills, workspace.activatedSkillDirs, inputPath);
894
- if (!skillRead) {
895
- throw new Error(`Skill resource is not readable before its skill is loaded: ${inputPath}. Read skills://<name> first.`);
896
- }
897
- return {
898
- absolutePath: skillRead.absolutePath,
899
- readRoots: [workspace.root, skillRead.skill.baseDir],
900
- skillRead,
901
- };
902
- }
903
- try {
904
- return {
905
- absolutePath: this.resolvePath(workspace, inputPath),
906
- readRoots: [workspace.root],
907
- };
908
- }
909
- catch (workspaceError) {
910
- const skillRead = resolveSkillReadPath(workspace.skills, workspace.activatedSkillDirs, inputPath);
911
- if (skillRead) {
912
- return {
913
- absolutePath: skillRead.absolutePath,
914
- readRoots: [workspace.root, skillRead.skill.baseDir],
915
- skillRead,
916
- };
917
- }
918
- const capabilityGuideRead = resolveCapabilityGuideReadPath(workspace.capabilityGuides, workspace.activatedCapabilityGuideDirs, inputPath);
919
- if (capabilityGuideRead) {
920
- return {
921
- absolutePath: capabilityGuideRead.absolutePath,
922
- readRoots: [workspace.root, capabilityGuideRead.guide.baseDir],
923
- capabilityGuideRead,
924
- };
925
- }
926
- try {
927
- return {
928
- absolutePath: resolveAllowedPath(inputPath, workspace.root, [tmpdir()]),
929
- readRoots: this.fileToolRoots(workspace),
930
- };
931
- }
932
- catch {
933
- throw workspaceError;
934
- }
935
- }
322
+ return this.context.resolveReadPath(workspace, inputPath);
936
323
  }
937
324
  markReadPathLoaded(workspace, readPath) {
938
- if (readPath.skillRead?.isSkillFile) {
939
- markSkillActivated(workspace.activatedSkillDirs, readPath.skillRead.skill);
940
- }
941
- if (readPath.capabilityGuideRead?.isGuideFile) {
942
- markCapabilityGuideActivated(workspace.activatedCapabilityGuideDirs, readPath.capabilityGuideRead.guide);
943
- }
325
+ this.context.markReadPathLoaded(workspace, readPath);
326
+ }
327
+ readAdvertisedInstruction(workspace, inputPath) {
328
+ return this.context.readAdvertisedInstruction(workspace, inputPath);
944
329
  }
945
330
  resolveWorkingDirectory(workspace, workingDirectory) {
946
- const directory = workingDirectory ? this.resolvePath(workspace, workingDirectory) : workspace.root;
947
- return assertAllowedPath(directory, [workspace.root]);
331
+ return this.context.resolveWorkingDirectory(workspace, workingDirectory);
948
332
  }
949
333
  async openCheckoutWorkspace(path) {
950
334
  const root = assertAllowedPath(path, this.config.allowedRoots);
@@ -974,7 +358,7 @@ export class WorkspaceRegistry {
974
358
  mode: input.mode,
975
359
  sourceRoot: input.sourceRoot,
976
360
  worktree: input.worktree,
977
- ...this.loadSkillsForWorkspace(input.root),
361
+ ...this.context.loadSkillsForWorkspace(input.root),
978
362
  capabilityGuides: loadCapabilityGuides(this.config),
979
363
  agentProfiles: await loadSubagentProfiles(this.config, input.root),
980
364
  activatedSkillDirs: new Set(),
@@ -1008,8 +392,8 @@ export class WorkspaceRegistry {
1008
392
  targetBranch: workspace.worktree?.targetBranch,
1009
393
  },
1010
394
  });
1011
- const agentsFiles = await this.loadInitialAgentsFiles(workspace);
1012
- const availableAgentsFiles = await this.findAvailableAgentsFiles(workspace, agentsFiles);
395
+ const agentsFiles = await this.context.loadInitialAgentsFiles(workspace);
396
+ const availableAgentsFiles = await this.context.findAvailableAgentsFiles(workspace, agentsFiles);
1013
397
  const { contextFingerprint, componentFingerprints: bootstrapComponentFingerprints, } = bootstrapContextFingerprints(workspace, agentsFiles, availableAgentsFiles);
1014
398
  return {
1015
399
  workspace,
@@ -1023,320 +407,9 @@ export class WorkspaceRegistry {
1023
407
  includeBootstrapContext: true,
1024
408
  };
1025
409
  }
1026
- loadSkillsForWorkspace(root) {
1027
- const result = loadWorkspaceSkills(this.config, root);
1028
- return {
1029
- skills: result.skills,
1030
- skillDiagnostics: result.diagnostics,
1031
- };
410
+ discoverPathInstructions(workspace, inputPath) {
411
+ return this.context.discoverPathInstructions(workspace, inputPath);
1032
412
  }
1033
- assertWorkspaceRootAllowed(root, mode, sourceRoot) {
1034
- if (mode === "worktree") {
1035
- if (!sourceRoot) {
1036
- throw new Error(`Stored worktree workspace is missing sourceRoot: ${root}`);
1037
- }
1038
- assertAllowedPath(sourceRoot, this.config.allowedRoots);
1039
- return assertAllowedPath(root, [this.config.worktreeRoot]);
1040
- }
1041
- return assertAllowedPath(root, this.config.allowedRoots);
1042
- }
1043
- async loadInitialAgentsFiles(workspace) {
1044
- const systemInstructionsPath = resolve(this.config.systemInstructionsPath);
1045
- const loadedFiles = [];
1046
- const systemInstructions = await readSystemInstructions(systemInstructionsPath);
1047
- const systemInstructionsRealPath = await tryRealpath(systemInstructionsPath);
1048
- if (systemInstructions !== undefined) {
1049
- loadedFiles.push({
1050
- path: systemInstructionsPath,
1051
- content: systemInstructions,
1052
- });
1053
- workspace.loadedInstructionPaths.add(systemInstructionsPath);
1054
- if (systemInstructionsRealPath) {
1055
- workspace.loadedInstructionRealPaths.add(systemInstructionsRealPath);
1056
- }
1057
- }
1058
- await this.discoverInstructionTree(workspace, workspace.root, INITIAL_INSTRUCTION_DISCOVERY_DEPTH);
1059
- loadedFiles.push(...await this.loadKnownInstructionsInDirectory(workspace, workspace.root));
1060
- return loadedFiles;
1061
- }
1062
- async findAvailableAgentsFiles(workspace, loadedFiles) {
1063
- const loadedPaths = new Set(loadedFiles.map((file) => resolve(file.path)));
1064
- const discovered = [];
1065
- for (const paths of workspace.knownInstructionPathsByDir.values()) {
1066
- for (const path of paths) {
1067
- if (loadedPaths.has(path))
1068
- continue;
1069
- const realPath = await tryRealpath(path);
1070
- if (realPath && workspace.loadedInstructionRealPaths.has(realPath))
1071
- continue;
1072
- discovered.push({ path });
1073
- }
1074
- }
1075
- return discovered.sort((a, b) => a.path.localeCompare(b.path));
1076
- }
1077
- async discoverPathInstructions(workspace, inputPath) {
1078
- const absolutePath = resolve(inputPath);
1079
- if (!isPathInsideRoot(absolutePath, workspace.root))
1080
- return [];
1081
- const targetDirectory = dirname(absolutePath);
1082
- const relationship = relative(workspace.root, targetDirectory);
1083
- if (relationship === ".." ||
1084
- relationship.startsWith(`..${sep}`) ||
1085
- resolve(targetDirectory) === resolve(this.config.agentDir) ||
1086
- isPathInsideRoot(targetDirectory, resolve(this.config.agentDir))) {
1087
- return [];
1088
- }
1089
- const directories = [resolve(workspace.root)];
1090
- if (relationship) {
1091
- let current = resolve(workspace.root);
1092
- for (const segment of relationship.split(sep).filter(Boolean)) {
1093
- if (SKIPPED_CONTEXT_DIRS.has(segment))
1094
- break;
1095
- current = join(current, segment);
1096
- directories.push(current);
1097
- }
1098
- }
1099
- const loaded = [];
1100
- for (const directory of directories) {
1101
- await this.discoverInstructionTree(workspace, directory, 0);
1102
- loaded.push(...await this.loadKnownInstructionsInDirectory(workspace, directory));
1103
- }
1104
- return loaded;
1105
- }
1106
- async discoverInstructionTree(workspace, directory, remainingDepth) {
1107
- const resolvedDirectory = resolve(directory);
1108
- if (workspace.scannedInstructionDirs.has(resolvedDirectory))
1109
- return;
1110
- workspace.scannedInstructionDirs.add(resolvedDirectory);
1111
- if (resolvedDirectory !== resolve(workspace.root) &&
1112
- isPathInsideRoot(resolvedDirectory, resolve(this.config.agentDir))) {
1113
- return;
1114
- }
1115
- let entries;
1116
- try {
1117
- entries = await opendir(resolvedDirectory);
1118
- }
1119
- catch {
1120
- return;
1121
- }
1122
- const instructionPaths = [];
1123
- const childDirectories = [];
1124
- for await (const entry of entries) {
1125
- const path = join(resolvedDirectory, entry.name);
1126
- if (entry.isFile() && CONTEXT_FILE_NAMES.has(entry.name)) {
1127
- instructionPaths.push(path);
1128
- continue;
1129
- }
1130
- if (remainingDepth > 0 &&
1131
- entry.isDirectory() &&
1132
- !SKIPPED_CONTEXT_DIRS.has(entry.name)) {
1133
- childDirectories.push(path);
1134
- }
1135
- }
1136
- workspace.knownInstructionPathsByDir.set(resolvedDirectory, instructionPaths.sort((left, right) => left.localeCompare(right)));
1137
- if (remainingDepth <= 0)
1138
- return;
1139
- for (const childDirectory of childDirectories) {
1140
- await this.discoverInstructionTree(workspace, childDirectory, remainingDepth - 1);
1141
- }
1142
- }
1143
- async loadKnownInstructionsInDirectory(workspace, directory) {
1144
- const resolvedDirectory = resolve(directory);
1145
- const paths = workspace.knownInstructionPathsByDir.get(resolvedDirectory) ?? [];
1146
- const loaded = [];
1147
- const resolvedRoot = (await tryRealpath(workspace.root)) ?? resolve(workspace.root);
1148
- const realDirectory = (await tryRealpath(resolvedDirectory)) ?? resolvedDirectory;
1149
- for (const path of paths) {
1150
- const realPath = await tryRealpath(path);
1151
- if (!realPath)
1152
- continue;
1153
- if (!isPathInsideRoot(realPath, resolvedRoot))
1154
- continue;
1155
- if (dirname(realPath) !== realDirectory)
1156
- continue;
1157
- if (workspace.loadedInstructionRealPaths.has(realPath))
1158
- continue;
1159
- let content;
1160
- try {
1161
- content = await readFile(realPath, "utf8");
1162
- }
1163
- catch (error) {
1164
- if (isErrnoException(error) && (error.code === "ENOENT" || error.code === "ENOTDIR"))
1165
- continue;
1166
- throw error;
1167
- }
1168
- workspace.loadedInstructionRealPaths.add(realPath);
1169
- workspace.loadedInstructionPaths.add(path);
1170
- loaded.push({ path, content });
1171
- }
1172
- return loaded;
1173
- }
1174
- }
1175
- const BOOTSTRAP_CONTEXT_COMPONENTS = [
1176
- "agentsFiles",
1177
- "availableAgentsFiles",
1178
- "skills",
1179
- "skillDiagnostics",
1180
- "capabilityGuides",
1181
- "agentProfiles",
1182
- ];
1183
- function resolveBootstrapContextComponents(mode, currentFingerprints, deliveries, contextFingerprint) {
1184
- if (mode === "none")
1185
- return [];
1186
- if (mode === "full")
1187
- return [...BOOTSTRAP_CONTEXT_COMPONENTS];
1188
- if (deliveries.length === 0)
1189
- return [...BOOTSTRAP_CONTEXT_COMPONENTS];
1190
- if (contextFingerprint &&
1191
- deliveries.some((delivery) => !delivery.componentFingerprints && delivery.contextFingerprint === contextFingerprint)) {
1192
- return [];
1193
- }
1194
- return BOOTSTRAP_CONTEXT_COMPONENTS.filter((component) => !deliveries.some((delivery) => delivery.componentFingerprints?.[component] === currentFingerprints[component]));
1195
- }
1196
- function bootstrapContextFingerprints(workspace, agentsFiles, availableAgentsFiles) {
1197
- const payload = {
1198
- agentsFiles: agentsFiles
1199
- .map((file) => ({ path: resolve(file.path), content: file.content }))
1200
- .sort((left, right) => left.path.localeCompare(right.path)),
1201
- availableAgentsFiles: availableAgentsFiles
1202
- .map((file) => resolve(file.path))
1203
- .sort((left, right) => left.localeCompare(right)),
1204
- skills: workspace.skills
1205
- .map((skill) => ({
1206
- name: skill.name,
1207
- description: skill.description,
1208
- filePath: resolve(skill.filePath),
1209
- disableModelInvocation: skill.disableModelInvocation ?? false,
1210
- }))
1211
- .sort((left, right) => left.name.localeCompare(right.name) || left.filePath.localeCompare(right.filePath)),
1212
- skillDiagnostics: workspace.skillDiagnostics,
1213
- capabilityGuides: workspace.capabilityGuides
1214
- .map((guide) => ({
1215
- name: guide.name,
1216
- description: guide.description,
1217
- whenToRead: guide.whenToRead,
1218
- filePath: resolve(guide.filePath),
1219
- }))
1220
- .sort((left, right) => left.name.localeCompare(right.name)),
1221
- agentProfiles: workspace.agentProfiles
1222
- .map((profile) => ({
1223
- name: profile.name,
1224
- description: profile.description,
1225
- provider: profile.provider,
1226
- model: profile.model,
1227
- thinking: profile.thinking,
1228
- filePath: resolve(profile.filePath),
1229
- }))
1230
- .sort((left, right) => left.name.localeCompare(right.name)),
1231
- };
1232
- const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
1233
- return {
1234
- contextFingerprint: hash(payload),
1235
- componentFingerprints: {
1236
- agentsFiles: hash(payload.agentsFiles),
1237
- availableAgentsFiles: hash(payload.availableAgentsFiles),
1238
- skills: hash(payload.skills),
1239
- skillDiagnostics: hash(payload.skillDiagnostics),
1240
- capabilityGuides: hash(payload.capabilityGuides),
1241
- agentProfiles: hash(payload.agentProfiles),
1242
- },
1243
- };
1244
- }
1245
- function canonicalPersistedWorkspacePath(path) {
1246
- const missingSegments = [];
1247
- let candidate = path;
1248
- while (true) {
1249
- try {
1250
- return resolve(realpathSync(candidate), ...missingSegments.slice().reverse());
1251
- }
1252
- catch (error) {
1253
- if (!isErrnoException(error) || (error.code !== "ENOENT" && error.code !== "ENOTDIR")) {
1254
- return resolve(path);
1255
- }
1256
- const parent = dirname(candidate);
1257
- if (parent === candidate)
1258
- return resolve(path);
1259
- missingSegments.push(basename(candidate));
1260
- candidate = parent;
1261
- }
1262
- }
1263
- }
1264
- async function canonicalPath(path) {
1265
- const missingSegments = [];
1266
- let candidate = path;
1267
- while (true) {
1268
- try {
1269
- return resolve(await realpath(candidate), ...missingSegments.slice().reverse());
1270
- }
1271
- catch (error) {
1272
- if (!isErrnoException(error) || (error.code !== "ENOENT" && error.code !== "ENOTDIR")) {
1273
- throw error;
1274
- }
1275
- const parent = dirname(candidate);
1276
- if (parent === candidate)
1277
- return path;
1278
- missingSegments.push(basename(candidate));
1279
- candidate = parent;
1280
- }
1281
- }
1282
- }
1283
- export async function ensureCheckoutWorkspaceRoot(path, ops = { stat, mkdir }) {
1284
- try {
1285
- return await ops.stat(path);
1286
- }
1287
- catch (error) {
1288
- if (!isErrnoException(error) || error.code !== "ENOENT") {
1289
- throw error;
1290
- }
1291
- }
1292
- await ops.mkdir(path, { recursive: true });
1293
- return await ops.stat(path);
1294
- }
1295
- const CONTEXT_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
1296
- const SKIPPED_CONTEXT_DIRS = new Set([
1297
- ".git",
1298
- ".hg",
1299
- ".svn",
1300
- ".forgerelay",
1301
- ".devspace",
1302
- "node_modules",
1303
- "dist",
1304
- "build",
1305
- ".next",
1306
- ".turbo",
1307
- ".cache",
1308
- ]);
1309
- export function formatAgentsPath(path, workspaceRoot) {
1310
- if (!workspaceRoot)
1311
- return path.split(sep).join("/");
1312
- const relationship = relative(workspaceRoot, path);
1313
- if (relationship === "" ||
1314
- relationship.startsWith("..") ||
1315
- relationship === ".." ||
1316
- relationship.includes(`..${sep}`)) {
1317
- return path.split(sep).join("/");
1318
- }
1319
- return relationship.split(sep).join("/");
1320
- }
1321
- async function readSystemInstructions(path) {
1322
- try {
1323
- return await readFile(path, "utf8");
1324
- }
1325
- catch (error) {
1326
- if (isErrnoException(error) && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1327
- return undefined;
1328
- }
1329
- throw error;
1330
- }
1331
- }
1332
- async function tryRealpath(path) {
1333
- try {
1334
- return await realpath(path);
1335
- }
1336
- catch {
1337
- return undefined;
1338
- }
1339
- }
1340
- function isErrnoException(error) {
1341
- return error instanceof Error && "code" in error;
1342
413
  }
414
+ export { formatAgentsPath };
415
+ export { ensureCheckoutWorkspaceRoot } from "./workspaces/paths.js";