@akira-tl/forgerelay 0.8.8 → 0.8.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +10 -18
- package/capabilities/code-intelligence/GUIDE.md +7 -3
- package/capabilities/host-integration/GUIDE.md +2 -2
- package/capabilities/subagents/GUIDE.md +9 -0
- package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +6 -0
- package/dist/activity/{audit-store.js → history/audit-store.js} +129 -21
- package/dist/activity/history/bash-output-store.js +250 -0
- package/dist/activity/{host-turn-store.js → history/host-turn-store.js} +4 -1
- package/dist/activity/{query-service.js → history/query-service.js} +13 -7
- package/dist/activity/{lifecycle.js → runtime/lifecycle.js} +1 -1
- package/dist/activity/{mcp-query-tools.js → runtime/mcp-query-tools.js} +3 -3
- package/dist/activity/storage/history.js +12 -0
- package/dist/activity/storage/paths.js +56 -0
- package/dist/activity/storage/segmented-log.js +149 -0
- package/dist/cli/init.js +180 -0
- package/dist/cli/setup-support.js +181 -0
- package/dist/cli.js +23 -216
- package/dist/lsp/code-intelligence.js +18 -4
- package/dist/lsp/runtime/diagnostic-snapshots.js +52 -1
- package/dist/lsp/runtime/managed-language-servers.js +172 -0
- package/dist/lsp/runtime/manager.js +37 -2
- package/dist/lsp/runtime/process-launch.js +3 -0
- package/dist/lsp/test-support/server-fixture.js +14 -11
- package/dist/mcp/filesystem/filesystem-tools.js +245 -0
- package/dist/mcp/hooks/command-runner.js +79 -0
- package/dist/{hook-cli.js → mcp/hooks/hook-cli.js} +1 -1
- package/dist/{hooks.js → mcp/hooks/hooks.js} +3 -80
- package/dist/{oauth-provider.js → mcp/oauth/oauth-provider.js} +1 -1
- package/dist/{oauth-store.js → mcp/oauth/oauth-store.js} +5 -4
- package/dist/{operations → mcp/operations}/native-bulk-mutations.js +2 -2
- package/dist/mcp/panel/app.js +143 -0
- package/dist/{mcp-app-template.js → mcp/panel/mcp-app-template.js} +2 -38
- package/dist/{process-platform.js → mcp/process/process-platform.js} +21 -9
- package/dist/{process-sessions.js → mcp/process/process-sessions.js} +3 -3
- package/dist/mcp/process/runtime.js +257 -0
- package/dist/mcp/process/tools.js +346 -0
- package/dist/mcp/server/core/activity-support.js +70 -0
- package/dist/{capabilities.js → mcp/server/core/capabilities.js} +5 -3
- package/dist/{capability-registry.js → mcp/server/core/capability-registry.js} +9 -4
- package/dist/mcp/server/core/capability-support.js +90 -0
- package/dist/mcp/server/core/schemas.js +216 -0
- package/dist/mcp/server/core/tool-support.js +210 -0
- package/dist/mcp/server/core/version.js +9 -0
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +455 -0
- package/dist/mcp/server/operations/runtime/mutation-diagnostics.js +54 -0
- package/dist/mcp/server/operations/runtime/operation-runtime.js +524 -0
- package/dist/mcp/server/transport/http-server.js +301 -0
- package/dist/mcp/server/transport/http-support.js +43 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +242 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
- package/dist/mcp/server/workspace/runtime/workspace-open.js +555 -0
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/{config.js → runtime/config/config.js} +4 -3
- package/dist/{user-config.js → runtime/config/user-config.js} +14 -63
- package/dist/{db → runtime/state/db}/client.js +1 -1
- package/dist/{db → runtime/state/db}/migrations.js +34 -3
- package/dist/runtime/state/lock/file-lock.js +130 -0
- package/dist/runtime/testing/server-fixture.js +190 -0
- package/dist/server.js +133 -4189
- package/dist/subagents/providers/adapters/claude.js +86 -52
- package/dist/subagents/providers/adapters/codex.js +103 -39
- package/dist/subagents/providers/adapters/opencode.js +114 -84
- package/dist/subagents/providers/adapters/pi.js +59 -8
- package/dist/subagents/providers/availability.js +15 -30
- package/dist/subagents/providers/runtime/external-command.js +56 -0
- package/dist/subagents/sessions/capability.js +1 -1
- package/dist/subagents/sessions/execution.js +1 -1
- package/dist/subagents/sessions/mcp/activity-audit-test-support.js +30 -0
- package/dist/subagents/sessions/store.js +1 -1
- package/dist/ui/.vite/manifest.json +342 -410
- package/dist/ui/activity-panel-app.html +2 -4
- package/dist/ui/assets/activity-panel-app-7Xe0alhm.css +1 -0
- package/dist/ui/assets/activity-panel-app-Sl1Jro8v.js +115 -0
- package/dist/ui/assets/angular-html-B_yn91wW.js +1 -0
- package/dist/ui/assets/{angular-ts-Bxh4-O1-.js → angular-ts-DSgtloKz.js} +1 -1
- package/dist/ui/assets/{apl-BmYHKQ6m.js → apl-NatWtF8X.js} +1 -1
- package/dist/ui/assets/{astro-C51vqDKq.js → astro-BYFEVBuN.js} +1 -1
- package/dist/ui/assets/{blade-CdhVsmwq.js → blade-DoO7yvMR.js} +1 -1
- package/dist/ui/assets/c-Rl2hMCcg.js +1 -0
- package/dist/ui/assets/{cobol-Cf6azoN_.js → cobol-ByC5mWiB.js} +1 -1
- package/dist/ui/assets/{coffee-vVdGzlmt.js → coffee-Dez77Qov.js} +1 -1
- package/dist/ui/assets/cpp-DtaAQT9h.js +1 -0
- package/dist/ui/assets/{crystal-5UZy6KIX.js → crystal-Bgmbf8ec.js} +1 -1
- package/dist/ui/assets/css-C1tjBtVN.js +1 -0
- package/dist/ui/assets/{edge-CESgo4ia.js → edge-DeH6u8Wg.js} +1 -1
- package/dist/ui/assets/{elixir-9FKfw4cJ.js → elixir-_H3KnmWT.js} +1 -1
- package/dist/ui/assets/{elm-Dv7A0aLa.js → elm-CsCe3U28.js} +1 -1
- package/dist/ui/assets/{erb-DHFmDvM0.js → erb-CbM-L2g8.js} +1 -1
- package/dist/ui/assets/{git-rebase-utbU6CzV.js → git-rebase-D0s5pDh9.js} +1 -1
- package/dist/ui/assets/{glimmer-js-t2kiCkyB.js → glimmer-js-BfOToq_A.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-F0Jv5okV.js → glimmer-ts-CCUOkL2K.js} +1 -1
- package/dist/ui/assets/glsl-DH4ec9dE.js +1 -0
- package/dist/ui/assets/graphql-DilAMBoI.js +1 -0
- package/dist/ui/assets/{hack-8yjdbaSA.js → hack-BDkNDZlF.js} +1 -1
- package/dist/ui/assets/haml-D_QBp6K7.js +1 -0
- package/dist/ui/assets/{handlebars-BBFEg4Lo.js → handlebars-DePUrTpC.js} +1 -1
- package/dist/ui/assets/heavy-payload-DOwZpO6n.js +290 -0
- package/dist/ui/assets/html-By4dNnM4.js +1 -0
- package/dist/ui/assets/{html-derivative-DJAQcZyc.js → html-derivative-DDw9Ef4Q.js} +1 -1
- package/dist/ui/assets/{http-Dg5LSYRK.js → http-B0QxWc9M.js} +1 -1
- package/dist/ui/assets/{hurl-Y5uYzMZR.js → hurl-vMQ4lda1.js} +1 -1
- package/dist/ui/assets/java-DYRe2lWG.js +1 -0
- package/dist/ui/assets/javascript-CxyGqwcL.js +1 -0
- package/dist/ui/assets/{jinja-Bt30h8Ik.js → jinja-JUwV-fZt.js} +1 -1
- package/dist/ui/assets/{jison-D5uZ1Nyo.js → jison-7ZqCEvS8.js} +1 -1
- package/dist/ui/assets/json-dbltgiiA.js +1 -0
- package/dist/ui/assets/jsx-oDopT7YQ.js +1 -0
- package/dist/ui/assets/{julia-76hRaaGz.js → julia-CkKiNSav.js} +1 -1
- package/dist/ui/assets/{just-CDE7W0vW.js → just-DuqCAKJ3.js} +1 -1
- package/dist/ui/assets/{latex-CVO2Byab.js → latex-sudcRDSn.js} +1 -1
- package/dist/ui/assets/{liquid-CA-AS3EX.js → liquid-Di-08Js7.js} +1 -1
- package/dist/ui/assets/lua-YlqdMoV0.js +1 -0
- package/dist/ui/assets/{marko-_BWrPfny.js → marko-CJRNyJDb.js} +1 -1
- package/dist/ui/assets/{mdc-PyJv85eS.js → mdc-t12CzZHs.js} +1 -1
- package/dist/ui/assets/{nginx-DAWVw8yu.js → nginx-57-b4XwT.js} +1 -1
- package/dist/ui/assets/{nim-CKE1dzpU.js → nim-BzrswYQa.js} +1 -1
- package/dist/ui/assets/{perl-BBhWva19.js → perl-D9JR_iIh.js} +1 -1
- package/dist/ui/assets/{php-ChIeaAoF.js → php-Py2gFHil.js} +1 -1
- package/dist/ui/assets/{pug-CMgI5afi.js → pug-PzLLy0h1.js} +1 -1
- package/dist/ui/assets/{qml-CjxTkcFH.js → qml-By8fwgV4.js} +1 -1
- package/dist/ui/assets/r-CcezTbLq.js +1 -0
- package/dist/ui/assets/{razor-i4DbHr8p.js → razor-dG-vdhL_.js} +1 -1
- package/dist/ui/assets/regexp-BlsQ-N_8.js +1 -0
- package/dist/ui/assets/{rst-oy_97Kl4.js → rst-C2awDCat.js} +1 -1
- package/dist/ui/assets/{ruby-C5zYZIyl.js → ruby-zA2W_49d.js} +1 -1
- package/dist/ui/assets/{sas-BnMEh2S3.js → sas-D49bhyaN.js} +1 -1
- package/dist/ui/assets/scss-BQb-cp01.js +1 -0
- package/dist/ui/assets/shellscript-Dga4H_Ni.js +1 -0
- package/dist/ui/assets/{shellsession-BpE5o7_3.js → shellsession-n3-5CqcF.js} +1 -1
- package/dist/ui/assets/{soy-COesmhTY.js → soy-XYBOf_BA.js} +1 -1
- package/dist/ui/assets/sql-ET4u5Cpl.js +1 -0
- package/dist/ui/assets/{stata-CGQEuSZq.js → stata-CLHcJHO_.js} +1 -1
- package/dist/ui/assets/{surrealql-Bz4JbU-9.js → surrealql-BdjQ60T4.js} +1 -1
- package/dist/ui/assets/{svelte-1jM2ejXj.js → svelte-B2RPtKJe.js} +1 -1
- package/dist/ui/assets/{templ-C-VwELcp.js → templ-CrByPW_W.js} +1 -1
- package/dist/ui/assets/{tex-t1rUiQCm.js → tex-DZBkg363.js} +1 -1
- package/dist/ui/assets/{ts-tags-Bewoa6pS.js → ts-tags-DJR3Gphb.js} +1 -1
- package/dist/ui/assets/tsx-CajzROyy.js +1 -0
- package/dist/ui/assets/{twig-DVg-vELl.js → twig-BZjk5Zda.js} +1 -1
- package/dist/ui/assets/typescript-B-EWToLj.js +1 -0
- package/dist/ui/assets/{vue-wPQ7UlMy.js → vue-D_rHCCr8.js} +1 -1
- package/dist/ui/assets/{vue-html-BHtJ8geX.js → vue-html-CC4WlvVt.js} +1 -1
- package/dist/ui/assets/{vue-vine-CNSGAWwc.js → vue-vine-BgNMVgpk.js} +1 -1
- package/dist/ui/assets/xml-BI86-WbH.js +1 -0
- package/dist/ui/assets/{xsl-3_vJ27aE.js → xsl-DXeLZV4J.js} +1 -1
- package/dist/ui/assets/yaml-60_PFlWa.js +1 -0
- package/dist/workspaces/bootstrap.js +72 -0
- package/dist/workspaces/context.js +337 -0
- package/dist/workspaces/conversation-test-support.js +86 -0
- package/dist/{git-worktrees.js → workspaces/git/git-worktrees.js} +1 -1
- package/dist/workspaces/inventory.js +131 -0
- package/dist/workspaces/paths.js +55 -0
- package/dist/{remote-auth.js → workspaces/relay/auth/remote-auth.js} +2 -2
- package/dist/workspaces/relay/result-support.js +111 -0
- package/dist/workspaces/relay/tests/test-support.js +203 -0
- package/dist/{remote-mcp-connection-pool.js → workspaces/relay/transport/remote-mcp-connection-pool.js} +1 -1
- package/dist/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +30 -3
- package/dist/workspaces/relay/types.js +1 -0
- package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
- package/dist/workspaces/resources/resource-monitor.js +377 -0
- package/dist/workspaces/resources/skills.js +283 -0
- package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
- package/dist/workspaces/sessions.js +460 -0
- package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
- package/dist/workspaces.js +77 -993
- package/docs/chatgpt-coding-workflow.md +7 -14
- package/docs/configuration.md +26 -34
- package/docs/gotchas.md +0 -30
- package/docs/roadmap.md +6 -10
- package/docs/security.md +1 -5
- package/docs/setup.md +0 -11
- package/package.json +6 -9
- package/scripts/ci/architecture.mjs +109 -0
- package/scripts/ci/verify.mjs +5 -0
- package/scripts/debug/accept/bootstrap.mjs +326 -0
- package/scripts/debug/accept/harness.mjs +76 -0
- package/scripts/debug/accept/support.mjs +432 -0
- package/scripts/debug/accept.mjs +43 -806
- package/scripts/debug/relay-accept/support.mjs +135 -0
- package/scripts/debug/relay-accept.mjs +26 -115
- package/scripts/debug/traffic/support.mjs +167 -0
- package/scripts/debug/traffic/traffic-audit.mjs +4 -161
- package/scripts/{lsp-interop-support.test.mjs → lsp/interop-support.test.mjs} +1 -1
- package/scripts/{lsp-interop.mjs → lsp/interop.mjs} +3 -3
- package/scripts/release/publish.mjs +12 -9
- package/scripts/release/release-gate.test.mjs +3 -1
- package/scripts/release/release-version.test.mjs +13 -1
- package/scripts/release-parity.mjs +1 -1
- package/scripts/release-proof.mjs +15 -0
- package/scripts/release-proof.test.mjs +27 -1
- package/scripts/release-version.mjs +16 -1
- package/scripts/wiki/sync.mjs +21 -10
- package/dist/activity/bash-output-store.js +0 -127
- package/dist/pi-tools.js +0 -110
- package/dist/skills.js +0 -127
- package/dist/ui/assets/activity-panel-app-7AckaxIR.js +0 -2
- package/dist/ui/assets/angular-html-BkgDQl-G.js +0 -1
- package/dist/ui/assets/c-IBzBpxAO.js +0 -1
- package/dist/ui/assets/chunk-EyZ2wyi3.js +0 -1
- package/dist/ui/assets/cpp-CVHeO-Jk.js +0 -1
- package/dist/ui/assets/css-DEfus9J5.js +0 -1
- package/dist/ui/assets/glsl-_ooCjaeB.js +0 -1
- package/dist/ui/assets/graphql-B9LqP-cY.js +0 -1
- package/dist/ui/assets/haml-BU5EoZBn.js +0 -1
- package/dist/ui/assets/heavy-payload-Bol1_mcs.js +0 -4
- package/dist/ui/assets/html-D-C6YZk4.js +0 -1
- package/dist/ui/assets/java-ppHvycj5.js +0 -1
- package/dist/ui/assets/javascript-DDTjKLlQ.js +0 -1
- package/dist/ui/assets/json-Cw91r8h1.js +0 -1
- package/dist/ui/assets/jsx-04X2FT_1.js +0 -1
- package/dist/ui/assets/lua-CiyBp52Y.js +0 -1
- package/dist/ui/assets/r-DfFga4si.js +0 -1
- package/dist/ui/assets/regexp-iC9_nbSI.js +0 -1
- package/dist/ui/assets/review-payload-BqJp0c5e.js +0 -1
- package/dist/ui/assets/scrollbar-CvE-I-jG.js +0 -287
- package/dist/ui/assets/scss-BfZXsAWQ.js +0 -1
- package/dist/ui/assets/shellscript-BC2srNaB.js +0 -1
- package/dist/ui/assets/sql-Ccv_yeUm.js +0 -1
- package/dist/ui/assets/tsx-CTXzrBkN.js +0 -1
- package/dist/ui/assets/typescript--ElHoFkw.js +0 -1
- package/dist/ui/assets/workspace-app-BLW7p6IZ.js +0 -114
- package/dist/ui/assets/workspace-app-BjNqR0en.js +0 -1
- package/dist/ui/assets/workspace-app-CmaYU4DW.js +0 -3
- package/dist/ui/assets/workspace-app-D2bJ5fjt.css +0 -1
- package/dist/ui/assets/workspace-lifecycle-app-BVbI2Ilf.js +0 -1
- package/dist/ui/assets/xml-JnmX6vyS.js +0 -1
- package/dist/ui/assets/yaml-OAcZkP-w.js +0 -1
- package/dist/ui/workspace-app.html +0 -18
- package/dist/ui/workspace-lifecycle-app.html +0 -18
- package/docs/agent-profile-schema.md +0 -114
- package/docs/artifact-exchange.md +0 -65
- package/docs/versioning.md +0 -205
- package/skills/subagent-delegation/SKILL.md +0 -132
- /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
- /package/dist/activity/{process-output-audit.js → runtime/process-output-audit.js} +0 -0
- /package/dist/{artifact-error.js → mcp/artifacts/artifact-error.js} +0 -0
- /package/dist/{artifact-tools.js → mcp/artifacts/artifact-tools.js} +0 -0
- /package/dist/{incoming-artifacts.js → mcp/artifacts/incoming-artifacts.js} +0 -0
- /package/dist/{advertised-files.js → mcp/filesystem/advertised-files.js} +0 -0
- /package/dist/{apply-patch.js → mcp/filesystem/apply-patch.js} +0 -0
- /package/dist/{file-mutations.js → mcp/filesystem/file-mutations.js} +0 -0
- /package/dist/{roots.js → mcp/filesystem/roots.js} +0 -0
- /package/dist/{oauth → mcp/oauth}/public-url.js +0 -0
- /package/dist/{oauth → mcp/oauth}/router.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/executor.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/scheduler.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/types.js +0 -0
- /package/dist/{operations → mcp/operations}/bulk-mutation.js +0 -0
- /package/dist/{operations → mcp/operations}/bulk-read.js +0 -0
- /package/dist/{operations → mcp/operations}/core-operation-executor.js +0 -0
- /package/dist/{request-meta.js → mcp/request-meta.js} +0 -0
- /package/dist/{mcp-sessions.js → mcp/server/transport/mcp-sessions.js} +0 -0
- /package/dist/{server-shutdown.js → mcp/server/transport/server-shutdown.js} +0 -0
- /package/dist/{logger.js → runtime/logging/logger.js} +0 -0
- /package/dist/{db → runtime/state/db}/schema.js +0 -0
- /package/dist/{composite-activity.js → workspaces/composite/composite-activity.js} +0 -0
- /package/dist/{composite-workspaces.js → workspaces/composite/composite-workspaces.js} +0 -0
- /package/dist/{git.js → workspaces/git/git.js} +0 -0
- /package/dist/{workspace-presentation.js → workspaces/presentation/workspace-presentation.js} +0 -0
- /package/dist/{workspace-task-reminders.js → workspaces/tasks/workspace-task-reminders.js} +0 -0
- /package/dist/{workspace-tasks.js → workspaces/tasks/workspace-tasks.js} +0 -0
- /package/scripts/{lsp-interop-support.mjs → lsp/interop-support.mjs} +0 -0
package/dist/workspaces.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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,26 @@ export class WorkspaceRegistry {
|
|
|
17
18
|
config;
|
|
18
19
|
store;
|
|
19
20
|
workspaces = new Map();
|
|
20
|
-
pendingOpens = new Map();
|
|
21
21
|
hooks;
|
|
22
|
-
|
|
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.
|
|
28
|
-
this.
|
|
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());
|
|
40
|
+
this.context.pruneWorkspaceResources(this.workspaces.keys());
|
|
35
41
|
const workspaceInput = typeof input === "string" ? { path: input } : input;
|
|
36
42
|
const bootstrapContext = workspaceInput.context ?? "auto";
|
|
37
43
|
if (workspaceInput.workspaceId) {
|
|
@@ -46,148 +52,17 @@ export class WorkspaceRegistry {
|
|
|
46
52
|
}
|
|
47
53
|
return this.openReusableCheckout(workspaceInput.path, openOptions.conversationScopeId, bootstrapContext);
|
|
48
54
|
}
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
};
|
|
55
|
+
inspectWorkspace(workspaceId) {
|
|
56
|
+
return this.inventory.inspectWorkspace(workspaceId);
|
|
79
57
|
}
|
|
80
|
-
|
|
81
|
-
this.
|
|
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
|
-
};
|
|
58
|
+
listWorkspaces(input = {}, openOptions = {}) {
|
|
59
|
+
return this.inventory.listWorkspaces(input, openOptions);
|
|
185
60
|
}
|
|
186
61
|
async resumeWorkspace(workspaceId, conversationScopeId, bootstrapContext = "auto") {
|
|
187
62
|
const session = this.store?.getSession(workspaceId);
|
|
188
63
|
const context = session?.status === "closed" && session.mode === "worktree"
|
|
189
|
-
? await this.reopenClosedManagedWorktreeContext(session)
|
|
190
|
-
: await this.reusedWorkspaceContext(await this.workspaceForOpen(workspaceId));
|
|
64
|
+
? await this.sessions.reopenClosedManagedWorktreeContext(session)
|
|
65
|
+
: await this.sessions.reusedWorkspaceContext(await this.sessions.workspaceForOpen(workspaceId));
|
|
191
66
|
const workspace = context.workspace;
|
|
192
67
|
if (!conversationScopeId || !this.store) {
|
|
193
68
|
const bootstrapContextComponents = resolveBootstrapContextComponents(bootstrapContext, context.bootstrapComponentFingerprints, []);
|
|
@@ -197,7 +72,7 @@ export class WorkspaceRegistry {
|
|
|
197
72
|
includeBootstrapContext: bootstrapContextComponents.length > 0,
|
|
198
73
|
};
|
|
199
74
|
}
|
|
200
|
-
const targetKeys = await this.workspaceTargetKeys(workspace);
|
|
75
|
+
const targetKeys = await this.sessions.workspaceTargetKeys(workspace);
|
|
201
76
|
const deliveries = targetKeys
|
|
202
77
|
.map((targetKey) => this.store?.getContextDelivery(conversationScopeId, targetKey))
|
|
203
78
|
.filter((delivery) => delivery !== undefined);
|
|
@@ -232,7 +107,7 @@ export class WorkspaceRegistry {
|
|
|
232
107
|
const lastUsedAt = Date.parse(session.lastUsedAt);
|
|
233
108
|
if (!Number.isFinite(lastUsedAt) || now - lastUsedAt < WORKSPACE_STALE_REMINDER_MS)
|
|
234
109
|
continue;
|
|
235
|
-
const root = await this.validSessionRoot(session);
|
|
110
|
+
const root = await this.sessions.validSessionRoot(session);
|
|
236
111
|
if (!root || await canonicalPath(root) !== workspaceRootKey)
|
|
237
112
|
continue;
|
|
238
113
|
stale.push({
|
|
@@ -255,9 +130,10 @@ export class WorkspaceRegistry {
|
|
|
255
130
|
throw new Error(`Workspace ${canonicalWorkspaceId} is backed by a managed worktree. Use close_workspace with its managed-worktree finalize lifecycle.`);
|
|
256
131
|
}
|
|
257
132
|
if (this.store) {
|
|
258
|
-
this.deleteConversationBindingsForWorkspace(canonicalWorkspaceId);
|
|
133
|
+
this.sessions.deleteConversationBindingsForWorkspace(canonicalWorkspaceId);
|
|
259
134
|
this.store.setSessionStatus(canonicalWorkspaceId, "closed");
|
|
260
135
|
}
|
|
136
|
+
this.context.forgetWorkspaceResources(canonicalWorkspaceId);
|
|
261
137
|
this.workspaces.delete(canonicalWorkspaceId);
|
|
262
138
|
}
|
|
263
139
|
deleteWorkspace(workspaceId) {
|
|
@@ -265,13 +141,14 @@ export class WorkspaceRegistry {
|
|
|
265
141
|
if (session.mode === "worktree" && session.status === "active") {
|
|
266
142
|
throw new Error(`Workspace ${session.id} is an active managed-worktree Workspace. Finalize it safely before deleting its persistent identity.`);
|
|
267
143
|
}
|
|
268
|
-
this.deleteConversationBindingsForWorkspace(session.id);
|
|
144
|
+
this.sessions.deleteConversationBindingsForWorkspace(session.id);
|
|
269
145
|
this.store?.deleteSession(session.id);
|
|
146
|
+
this.context.forgetWorkspaceResources(session.id);
|
|
270
147
|
this.workspaces.delete(session.id);
|
|
271
148
|
}
|
|
272
149
|
workspaceIdsForPhysicalWorkspace(workspace) {
|
|
273
150
|
const root = resolve(workspace.root);
|
|
274
|
-
return this.activeSessions(workspace.mode)
|
|
151
|
+
return this.sessions.activeSessions(workspace.mode)
|
|
275
152
|
.filter((session) => resolve(session.root) === root)
|
|
276
153
|
.map((session) => session.id);
|
|
277
154
|
}
|
|
@@ -284,12 +161,12 @@ export class WorkspaceRegistry {
|
|
|
284
161
|
? await canonicalPath(workspace.root)
|
|
285
162
|
: undefined;
|
|
286
163
|
const resultsByRoot = new Map();
|
|
287
|
-
for (const session of this.activeSessions("worktree")) {
|
|
164
|
+
for (const session of this.sessions.activeSessions("worktree")) {
|
|
288
165
|
if (!session.sourceRoot)
|
|
289
166
|
continue;
|
|
290
167
|
if (await canonicalPath(session.sourceRoot) !== sourceKey)
|
|
291
168
|
continue;
|
|
292
|
-
const root = await this.validSessionRoot(session);
|
|
169
|
+
const root = await this.sessions.validSessionRoot(session);
|
|
293
170
|
if (!root)
|
|
294
171
|
continue;
|
|
295
172
|
const rootKey = await canonicalPath(root);
|
|
@@ -342,7 +219,7 @@ export class WorkspaceRegistry {
|
|
|
342
219
|
targetBranch: managedWorktree.targetBranch,
|
|
343
220
|
},
|
|
344
221
|
});
|
|
345
|
-
const aliasedWorkspaceIds = this.activeSessions("worktree")
|
|
222
|
+
const aliasedWorkspaceIds = this.sessions.activeSessions("worktree")
|
|
346
223
|
.filter((session) => resolve(session.root) === resolve(workspace.root))
|
|
347
224
|
.map((session) => session.id);
|
|
348
225
|
const result = await closeManagedWorktree({
|
|
@@ -367,8 +244,9 @@ export class WorkspaceRegistry {
|
|
|
367
244
|
},
|
|
368
245
|
}));
|
|
369
246
|
for (const aliasedWorkspaceId of aliasedWorkspaceIds) {
|
|
370
|
-
this.deleteConversationBindingsForWorkspace(aliasedWorkspaceId);
|
|
247
|
+
this.sessions.deleteConversationBindingsForWorkspace(aliasedWorkspaceId);
|
|
371
248
|
this.store?.setSessionStatus(aliasedWorkspaceId, "closed");
|
|
249
|
+
this.context.forgetWorkspaceResources(aliasedWorkspaceId);
|
|
372
250
|
this.workspaces.delete(aliasedWorkspaceId);
|
|
373
251
|
}
|
|
374
252
|
return { ...result, hookReports };
|
|
@@ -377,37 +255,37 @@ export class WorkspaceRegistry {
|
|
|
377
255
|
const allowedPath = assertAllowedPath(path, this.config.allowedRoots);
|
|
378
256
|
const projectKey = await canonicalPath(allowedPath);
|
|
379
257
|
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);
|
|
258
|
+
const boundContext = await this.sessions.boundConversationContext(conversationScopeId, targetKey, "checkout", async (session, root) => session.mode === "checkout" && await canonicalPath(root) === projectKey, bootstrapContext);
|
|
381
259
|
if (boundContext)
|
|
382
260
|
return boundContext;
|
|
383
|
-
const context = await this.openOnce(targetKey, async () => {
|
|
384
|
-
const reusableWorkspace = await this.findReusableWorkspaceByDirectory(projectKey, "checkout", true);
|
|
261
|
+
const context = await this.sessions.openOnce(targetKey, async () => {
|
|
262
|
+
const reusableWorkspace = await this.sessions.findReusableWorkspaceByDirectory(projectKey, "checkout", true);
|
|
385
263
|
if (!reusableWorkspace)
|
|
386
264
|
return this.openCheckoutWorkspace(path);
|
|
387
|
-
return this.reusedWorkspaceContext(reusableWorkspace);
|
|
265
|
+
return this.sessions.reusedWorkspaceContext(reusableWorkspace);
|
|
388
266
|
});
|
|
389
|
-
return this.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
|
|
267
|
+
return this.sessions.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
|
|
390
268
|
}
|
|
391
269
|
async openReusableWorktree(input, conversationScopeId) {
|
|
392
270
|
const path = input.path;
|
|
393
271
|
if (!path)
|
|
394
272
|
throw new Error("Worktree mode requires path.");
|
|
395
273
|
const bootstrapContext = input.context ?? "auto";
|
|
396
|
-
const managedPath = this.tryManagedWorktreePath(path);
|
|
274
|
+
const managedPath = this.sessions.tryManagedWorktreePath(path);
|
|
397
275
|
if (managedPath) {
|
|
398
276
|
const worktreeKey = await canonicalPath(managedPath);
|
|
399
277
|
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);
|
|
278
|
+
const boundContext = await this.sessions.boundConversationContext(conversationScopeId, targetKey, "worktree", async (session, root) => session.mode === "worktree" && await canonicalPath(root) === worktreeKey, bootstrapContext);
|
|
401
279
|
if (boundContext)
|
|
402
280
|
return boundContext;
|
|
403
|
-
const context = await this.openOnce(targetKey, async () => {
|
|
404
|
-
const reusableWorkspace = await this.findReusableWorkspaceByDirectory(worktreeKey, "worktree");
|
|
281
|
+
const context = await this.sessions.openOnce(targetKey, async () => {
|
|
282
|
+
const reusableWorkspace = await this.sessions.findReusableWorkspaceByDirectory(worktreeKey, "worktree");
|
|
405
283
|
if (!reusableWorkspace) {
|
|
406
284
|
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
285
|
}
|
|
408
|
-
return this.reusedWorkspaceContext(reusableWorkspace);
|
|
286
|
+
return this.sessions.reusedWorkspaceContext(reusableWorkspace);
|
|
409
287
|
});
|
|
410
|
-
return this.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
|
|
288
|
+
return this.sessions.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
|
|
411
289
|
}
|
|
412
290
|
const resolvedBase = await resolveManagedWorktreeBase({
|
|
413
291
|
sourcePath: path,
|
|
@@ -418,533 +296,43 @@ export class WorkspaceRegistry {
|
|
|
418
296
|
const targetKey = JSON.stringify(["worktree", sourceKey, resolvedBase.targetBranch]);
|
|
419
297
|
if (input.newWorktree) {
|
|
420
298
|
const context = await this.openWorktreeWorkspace(path, input.baseRef);
|
|
421
|
-
return this.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
|
|
299
|
+
return this.sessions.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
|
|
422
300
|
}
|
|
423
|
-
const boundContext = await this.boundConversationContext(conversationScopeId, targetKey, "worktree", async (session) => session.mode === "worktree" &&
|
|
301
|
+
const boundContext = await this.sessions.boundConversationContext(conversationScopeId, targetKey, "worktree", async (session) => session.mode === "worktree" &&
|
|
424
302
|
session.sourceRoot !== undefined &&
|
|
425
303
|
await canonicalPath(session.sourceRoot) === sourceKey &&
|
|
426
304
|
session.targetBranch === resolvedBase.targetBranch, bootstrapContext);
|
|
427
305
|
if (boundContext)
|
|
428
306
|
return boundContext;
|
|
429
|
-
const context = await this.openOnce(targetKey, async () => {
|
|
430
|
-
const reusableContext = await this.findReusableWorktreeContextBySource(sourceKey, resolvedBase.targetBranch);
|
|
307
|
+
const context = await this.sessions.openOnce(targetKey, async () => {
|
|
308
|
+
const reusableContext = await this.sessions.findReusableWorktreeContextBySource(sourceKey, resolvedBase.targetBranch);
|
|
431
309
|
return reusableContext ?? this.openWorktreeWorkspace(path, input.baseRef);
|
|
432
310
|
});
|
|
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
|
-
}
|
|
311
|
+
return this.sessions.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
|
|
796
312
|
}
|
|
797
313
|
getWorkspaceSession(workspaceId) {
|
|
798
|
-
|
|
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
|
-
}
|
|
314
|
+
return this.sessions.getWorkspaceSession(workspaceId);
|
|
828
315
|
}
|
|
829
316
|
getWorkspace(workspaceId) {
|
|
830
|
-
|
|
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;
|
|
317
|
+
return this.sessions.getWorkspace(workspaceId);
|
|
880
318
|
}
|
|
881
319
|
fileToolRoots(workspace) {
|
|
882
|
-
return
|
|
320
|
+
return this.context.fileToolRoots(workspace);
|
|
883
321
|
}
|
|
884
322
|
resolvePath(workspace, inputPath) {
|
|
885
|
-
|
|
886
|
-
if (!isPathInsideRoot(absolutePath, workspace.root)) {
|
|
887
|
-
throw new Error(`Path is outside workspace root: ${inputPath}`);
|
|
888
|
-
}
|
|
889
|
-
return absolutePath;
|
|
323
|
+
return this.context.resolvePath(workspace, inputPath);
|
|
890
324
|
}
|
|
891
325
|
resolveReadPath(workspace, inputPath) {
|
|
892
|
-
|
|
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
|
-
}
|
|
326
|
+
return this.context.resolveReadPath(workspace, inputPath);
|
|
936
327
|
}
|
|
937
328
|
markReadPathLoaded(workspace, readPath) {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
markCapabilityGuideActivated(workspace.activatedCapabilityGuideDirs, readPath.capabilityGuideRead.guide);
|
|
943
|
-
}
|
|
329
|
+
this.context.markReadPathLoaded(workspace, readPath);
|
|
330
|
+
}
|
|
331
|
+
readAdvertisedInstruction(workspace, inputPath) {
|
|
332
|
+
return this.context.readAdvertisedInstruction(workspace, inputPath);
|
|
944
333
|
}
|
|
945
334
|
resolveWorkingDirectory(workspace, workingDirectory) {
|
|
946
|
-
|
|
947
|
-
return assertAllowedPath(directory, [workspace.root]);
|
|
335
|
+
return this.context.resolveWorkingDirectory(workspace, workingDirectory);
|
|
948
336
|
}
|
|
949
337
|
async openCheckoutWorkspace(path) {
|
|
950
338
|
const root = assertAllowedPath(path, this.config.allowedRoots);
|
|
@@ -974,7 +362,7 @@ export class WorkspaceRegistry {
|
|
|
974
362
|
mode: input.mode,
|
|
975
363
|
sourceRoot: input.sourceRoot,
|
|
976
364
|
worktree: input.worktree,
|
|
977
|
-
...this.loadSkillsForWorkspace(input.root),
|
|
365
|
+
...this.context.loadSkillsForWorkspace(input.root),
|
|
978
366
|
capabilityGuides: loadCapabilityGuides(this.config),
|
|
979
367
|
agentProfiles: await loadSubagentProfiles(this.config, input.root),
|
|
980
368
|
activatedSkillDirs: new Set(),
|
|
@@ -1008,8 +396,9 @@ export class WorkspaceRegistry {
|
|
|
1008
396
|
targetBranch: workspace.worktree?.targetBranch,
|
|
1009
397
|
},
|
|
1010
398
|
});
|
|
1011
|
-
const agentsFiles = await this.loadInitialAgentsFiles(workspace);
|
|
1012
|
-
const availableAgentsFiles = await this.findAvailableAgentsFiles(workspace, agentsFiles);
|
|
399
|
+
const agentsFiles = await this.context.loadInitialAgentsFiles(workspace);
|
|
400
|
+
const availableAgentsFiles = await this.context.findAvailableAgentsFiles(workspace, agentsFiles);
|
|
401
|
+
this.context.trackWorkspaceResources(workspace, agentsFiles, availableAgentsFiles);
|
|
1013
402
|
const { contextFingerprint, componentFingerprints: bootstrapComponentFingerprints, } = bootstrapContextFingerprints(workspace, agentsFiles, availableAgentsFiles);
|
|
1014
403
|
return {
|
|
1015
404
|
workspace,
|
|
@@ -1023,320 +412,15 @@ export class WorkspaceRegistry {
|
|
|
1023
412
|
includeBootstrapContext: true,
|
|
1024
413
|
};
|
|
1025
414
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
return {
|
|
1029
|
-
skills: result.skills,
|
|
1030
|
-
skillDiagnostics: result.diagnostics,
|
|
1031
|
-
};
|
|
1032
|
-
}
|
|
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
|
-
}
|
|
415
|
+
discoverPathInstructions(workspace, inputPath) {
|
|
416
|
+
return this.context.discoverPathInstructions(workspace, inputPath);
|
|
1291
417
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
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);
|
|
418
|
+
claimResourceUpdates(workspaceId, conversationScopeId) {
|
|
419
|
+
return this.context.claimResourceUpdates(workspaceId, conversationScopeId);
|
|
1335
420
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
421
|
+
acknowledgeResourceUpdates(workspaceId, conversationScopeId) {
|
|
422
|
+
this.context.acknowledgeResourceUpdates(workspaceId, conversationScopeId);
|
|
1338
423
|
}
|
|
1339
424
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
}
|
|
425
|
+
export { formatAgentsPath };
|
|
426
|
+
export { ensureCheckoutWorkspaceRoot } from "./workspaces/paths.js";
|