@akira-tl/forgerelay 0.8.7 → 0.8.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +10 -18
- package/capabilities/host-integration/GUIDE.md +2 -2
- 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/setup-support.js +116 -0
- package/dist/cli.js +18 -126
- package/dist/lsp/code-intelligence.js +1 -1
- package/dist/lsp/test-support/server-fixture.js +9 -9
- 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/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} +2 -2
- 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 +207 -0
- package/dist/mcp/server/core/version.js +9 -0
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +400 -0
- package/dist/mcp/server/operations/runtime/operation-runtime.js +523 -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 +222 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
- package/dist/mcp/server/workspace/runtime/workspace-open.js +552 -0
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
- package/dist/{config.js → runtime/config/config.js} +3 -3
- package/dist/{user-config.js → runtime/config/user-config.js} +14 -54
- 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 +139 -4192
- 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 -409
- 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 +302 -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/workspaces/relay/types.js +1 -0
- package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
- package/dist/workspaces/resources/skills.js +285 -0
- package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
- package/dist/workspaces/sessions.js +457 -0
- package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
- package/dist/workspaces.js +69 -991
- package/docs/chatgpt-coding-workflow.md +3 -11
- package/docs/configuration.md +12 -25
- 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 -8
- 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/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-CUAN6zyW.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-CgzrutLm.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-BrLbezbq.js +0 -1
- package/dist/ui/assets/scrollbar-CbhpdW05.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-Bhj96tsR.js +0 -1
- package/dist/ui/assets/workspace-app-CxwJuZyS.js +0 -111
- package/dist/ui/assets/workspace-app-D6UR0AFl.js +0 -5
- package/dist/ui/assets/workspace-app-ldjBmCJR.css +0 -1
- package/dist/ui/assets/workspace-lifecycle-app-Cqfhx9pV.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/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
- /package/capabilities/{workspace-tasks → workspace/workspace-tasks}/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/{process-platform.js → mcp/process/process-platform.js} +0 -0
- /package/dist/{process-sessions.js → mcp/process/process-sessions.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/{remote-transport.js → workspaces/relay/transport/remote-transport.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,25 @@ 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());
|
|
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
|
-
|
|
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
|
-
};
|
|
54
|
+
inspectWorkspace(workspaceId) {
|
|
55
|
+
return this.inventory.inspectWorkspace(workspaceId);
|
|
79
56
|
}
|
|
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
|
-
};
|
|
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,531 +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
|
-
const agentsFiles = await this.loadInitialAgentsFiles(workspace);
|
|
704
|
-
const availableAgentsFiles = await this.findAvailableAgentsFiles(workspace, agentsFiles);
|
|
705
|
-
const { contextFingerprint, componentFingerprints: bootstrapComponentFingerprints, } = bootstrapContextFingerprints(workspace, agentsFiles, availableAgentsFiles);
|
|
706
|
-
return {
|
|
707
|
-
workspace,
|
|
708
|
-
agentsFiles,
|
|
709
|
-
availableAgentsFiles,
|
|
710
|
-
contextFingerprint,
|
|
711
|
-
bootstrapComponentFingerprints,
|
|
712
|
-
bootstrapContextComponents: [...BOOTSTRAP_CONTEXT_COMPONENTS],
|
|
713
|
-
hookReports: [],
|
|
714
|
-
workspaceReused: true,
|
|
715
|
-
includeBootstrapContext: true,
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
async workspaceForOpen(workspaceId) {
|
|
719
|
-
const session = this.store?.getSession(workspaceId);
|
|
720
|
-
if (session?.status === "closed" && session.mode === "checkout") {
|
|
721
|
-
this.store?.setSessionStatus(session.id, "active");
|
|
722
|
-
const reopened = this.store?.getSession(session.id);
|
|
723
|
-
if (!reopened) {
|
|
724
|
-
throw new Error(`Unknown workspaceId: ${workspaceId}. Call open_workspace first.`);
|
|
725
|
-
}
|
|
726
|
-
return this.workspaceFromSession(reopened, true);
|
|
727
|
-
}
|
|
728
|
-
return this.getWorkspace(workspaceId);
|
|
729
|
-
}
|
|
730
|
-
async reopenClosedManagedWorktreeContext(session) {
|
|
731
|
-
const operationKey = JSON.stringify(["worktree-reopen", session.id]);
|
|
732
|
-
return this.openOnce(operationKey, async () => {
|
|
733
|
-
const current = this.store?.getSession(session.id);
|
|
734
|
-
if (!current) {
|
|
735
|
-
throw new Error(`Unknown workspaceId: ${session.id}. Call open_workspace first.`);
|
|
736
|
-
}
|
|
737
|
-
if (current.status === "active") {
|
|
738
|
-
return this.reusedWorkspaceContext(this.getWorkspace(current.id));
|
|
739
|
-
}
|
|
740
|
-
if (current.status !== "closed" || current.mode !== "worktree") {
|
|
741
|
-
throw new Error(`Workspace ${current.id} is not a closed managed-worktree Workspace.`);
|
|
742
|
-
}
|
|
743
|
-
return this.reopenClosedManagedWorktreeContextUnlocked(current);
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
async reopenClosedManagedWorktreeContextUnlocked(session) {
|
|
747
|
-
if (!this.store) {
|
|
748
|
-
throw new Error(`Workspace ${session.id} cannot be reopened without persistent Workspace state.`);
|
|
749
|
-
}
|
|
750
|
-
if (!session.managed || !session.sourceRoot || !session.targetBranch) {
|
|
751
|
-
throw new Error(`Workspace ${session.id} does not have enough managed-worktree metadata to recreate its execution backing.`);
|
|
752
|
-
}
|
|
753
|
-
const worktree = await createManagedWorktree({
|
|
754
|
-
sourcePath: session.sourceRoot,
|
|
755
|
-
baseRef: session.targetBranch,
|
|
756
|
-
config: this.config,
|
|
757
|
-
});
|
|
758
|
-
const candidateSession = {
|
|
759
|
-
...session,
|
|
760
|
-
root: worktree.path,
|
|
761
|
-
status: "active",
|
|
762
|
-
sourceRoot: worktree.sourceRoot,
|
|
763
|
-
baseRef: worktree.baseRef,
|
|
764
|
-
baseSha: worktree.baseSha,
|
|
765
|
-
branch: worktree.branch,
|
|
766
|
-
targetBranch: worktree.targetBranch,
|
|
767
|
-
managed: true,
|
|
768
|
-
};
|
|
769
|
-
const workspace = this.workspaceFromSession(candidateSession, false);
|
|
770
|
-
try {
|
|
771
|
-
const context = await this.reusedWorkspaceContext(workspace);
|
|
772
|
-
this.store.replaceWorktreeBacking({
|
|
773
|
-
id: session.id,
|
|
774
|
-
root: worktree.path,
|
|
775
|
-
sourceRoot: worktree.sourceRoot,
|
|
776
|
-
baseRef: worktree.baseRef,
|
|
777
|
-
baseSha: worktree.baseSha,
|
|
778
|
-
branch: worktree.branch,
|
|
779
|
-
targetBranch: worktree.targetBranch,
|
|
780
|
-
});
|
|
781
|
-
return context;
|
|
782
|
-
}
|
|
783
|
-
catch (error) {
|
|
784
|
-
this.workspaces.delete(session.id);
|
|
785
|
-
try {
|
|
786
|
-
await discardFreshManagedWorktree({ worktree, config: this.config });
|
|
787
|
-
}
|
|
788
|
-
catch (cleanupError) {
|
|
789
|
-
const original = error instanceof Error ? error.message : String(error);
|
|
790
|
-
const cleanup = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
791
|
-
throw new Error(`${original} Reopen rollback also failed: ${cleanup}`);
|
|
792
|
-
}
|
|
793
|
-
throw error;
|
|
794
|
-
}
|
|
307
|
+
return this.sessions.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
|
|
795
308
|
}
|
|
796
309
|
getWorkspaceSession(workspaceId) {
|
|
797
|
-
|
|
798
|
-
if (session)
|
|
799
|
-
return session;
|
|
800
|
-
const workspace = this.workspaces.get(workspaceId);
|
|
801
|
-
if (!workspace) {
|
|
802
|
-
throw new Error(`Unknown workspaceId: ${workspaceId}. Call open_workspace first.`);
|
|
803
|
-
}
|
|
804
|
-
return {
|
|
805
|
-
id: workspace.id,
|
|
806
|
-
root: workspace.root,
|
|
807
|
-
status: "active",
|
|
808
|
-
mode: workspace.mode,
|
|
809
|
-
sourceRoot: workspace.sourceRoot,
|
|
810
|
-
baseRef: workspace.worktree?.baseRef,
|
|
811
|
-
baseSha: workspace.worktree?.baseSha,
|
|
812
|
-
branch: workspace.worktree?.branch,
|
|
813
|
-
targetBranch: workspace.worktree?.targetBranch,
|
|
814
|
-
managed: workspace.worktree?.managed ?? false,
|
|
815
|
-
createdAt: "",
|
|
816
|
-
lastUsedAt: "",
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
deleteConversationBindingsForWorkspace(workspaceId) {
|
|
820
|
-
if (!this.store)
|
|
821
|
-
return;
|
|
822
|
-
for (const binding of this.store.listConversationBindings()) {
|
|
823
|
-
if (binding.workspaceSessionId === workspaceId) {
|
|
824
|
-
this.store.deleteConversationBinding(binding.conversationScopeId, binding.targetKey);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
310
|
+
return this.sessions.getWorkspaceSession(workspaceId);
|
|
827
311
|
}
|
|
828
312
|
getWorkspace(workspaceId) {
|
|
829
|
-
|
|
830
|
-
if (workspace) {
|
|
831
|
-
this.store?.touchSession(workspaceId);
|
|
832
|
-
return workspace;
|
|
833
|
-
}
|
|
834
|
-
const session = this.store?.getSession(workspaceId);
|
|
835
|
-
if (!session || session.status !== "active") {
|
|
836
|
-
throw new Error(`Unknown workspaceId: ${workspaceId}. Call open_workspace first.`);
|
|
837
|
-
}
|
|
838
|
-
return this.workspaceFromSession(session, true);
|
|
839
|
-
}
|
|
840
|
-
workspaceFromSession(session, touch) {
|
|
841
|
-
const existing = this.workspaces.get(session.id);
|
|
842
|
-
if (existing) {
|
|
843
|
-
if (touch)
|
|
844
|
-
this.store?.touchSession(session.id);
|
|
845
|
-
return existing;
|
|
846
|
-
}
|
|
847
|
-
const root = this.assertWorkspaceRootAllowed(session.root, session.mode, session.sourceRoot);
|
|
848
|
-
const restoredWorkspace = {
|
|
849
|
-
id: session.id,
|
|
850
|
-
root,
|
|
851
|
-
mode: session.mode,
|
|
852
|
-
sourceRoot: session.sourceRoot,
|
|
853
|
-
worktree: session.mode === "worktree"
|
|
854
|
-
? {
|
|
855
|
-
path: root,
|
|
856
|
-
baseRef: session.baseRef ?? "HEAD",
|
|
857
|
-
baseSha: session.baseSha ?? "",
|
|
858
|
-
branch: session.branch,
|
|
859
|
-
targetBranch: session.targetBranch,
|
|
860
|
-
dirtySource: false,
|
|
861
|
-
detached: !session.branch,
|
|
862
|
-
managed: session.managed,
|
|
863
|
-
}
|
|
864
|
-
: undefined,
|
|
865
|
-
...this.loadSkillsForWorkspace(root),
|
|
866
|
-
capabilityGuides: loadCapabilityGuides(this.config),
|
|
867
|
-
agentProfiles: [],
|
|
868
|
-
activatedSkillDirs: new Set(),
|
|
869
|
-
activatedCapabilityGuideDirs: new Set(),
|
|
870
|
-
scannedInstructionDirs: new Set(),
|
|
871
|
-
knownInstructionPathsByDir: new Map(),
|
|
872
|
-
loadedInstructionRealPaths: new Set(),
|
|
873
|
-
};
|
|
874
|
-
if (touch)
|
|
875
|
-
this.store?.touchSession(session.id);
|
|
876
|
-
this.workspaces.set(restoredWorkspace.id, restoredWorkspace);
|
|
877
|
-
return restoredWorkspace;
|
|
313
|
+
return this.sessions.getWorkspace(workspaceId);
|
|
878
314
|
}
|
|
879
315
|
fileToolRoots(workspace) {
|
|
880
|
-
return
|
|
316
|
+
return this.context.fileToolRoots(workspace);
|
|
881
317
|
}
|
|
882
318
|
resolvePath(workspace, inputPath) {
|
|
883
|
-
|
|
884
|
-
if (!isPathInsideRoot(absolutePath, workspace.root)) {
|
|
885
|
-
throw new Error(`Path is outside workspace root: ${inputPath}`);
|
|
886
|
-
}
|
|
887
|
-
return absolutePath;
|
|
319
|
+
return this.context.resolvePath(workspace, inputPath);
|
|
888
320
|
}
|
|
889
321
|
resolveReadPath(workspace, inputPath) {
|
|
890
|
-
|
|
891
|
-
const skillRead = resolveSkillReadPath(workspace.skills, workspace.activatedSkillDirs, inputPath);
|
|
892
|
-
if (!skillRead) {
|
|
893
|
-
throw new Error(`Skill resource is not readable before its skill is loaded: ${inputPath}. Read skills://<name> first.`);
|
|
894
|
-
}
|
|
895
|
-
return {
|
|
896
|
-
absolutePath: skillRead.absolutePath,
|
|
897
|
-
readRoots: [workspace.root, skillRead.skill.baseDir],
|
|
898
|
-
skillRead,
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
try {
|
|
902
|
-
return {
|
|
903
|
-
absolutePath: this.resolvePath(workspace, inputPath),
|
|
904
|
-
readRoots: [workspace.root],
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
catch (workspaceError) {
|
|
908
|
-
const skillRead = resolveSkillReadPath(workspace.skills, workspace.activatedSkillDirs, inputPath);
|
|
909
|
-
if (skillRead) {
|
|
910
|
-
return {
|
|
911
|
-
absolutePath: skillRead.absolutePath,
|
|
912
|
-
readRoots: [workspace.root, skillRead.skill.baseDir],
|
|
913
|
-
skillRead,
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
const capabilityGuideRead = resolveCapabilityGuideReadPath(workspace.capabilityGuides, workspace.activatedCapabilityGuideDirs, inputPath);
|
|
917
|
-
if (capabilityGuideRead) {
|
|
918
|
-
return {
|
|
919
|
-
absolutePath: capabilityGuideRead.absolutePath,
|
|
920
|
-
readRoots: [workspace.root, capabilityGuideRead.guide.baseDir],
|
|
921
|
-
capabilityGuideRead,
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
try {
|
|
925
|
-
return {
|
|
926
|
-
absolutePath: resolveAllowedPath(inputPath, workspace.root, [tmpdir()]),
|
|
927
|
-
readRoots: this.fileToolRoots(workspace),
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
catch {
|
|
931
|
-
throw workspaceError;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
322
|
+
return this.context.resolveReadPath(workspace, inputPath);
|
|
934
323
|
}
|
|
935
324
|
markReadPathLoaded(workspace, readPath) {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
markCapabilityGuideActivated(workspace.activatedCapabilityGuideDirs, readPath.capabilityGuideRead.guide);
|
|
941
|
-
}
|
|
325
|
+
this.context.markReadPathLoaded(workspace, readPath);
|
|
326
|
+
}
|
|
327
|
+
readAdvertisedInstruction(workspace, inputPath) {
|
|
328
|
+
return this.context.readAdvertisedInstruction(workspace, inputPath);
|
|
942
329
|
}
|
|
943
330
|
resolveWorkingDirectory(workspace, workingDirectory) {
|
|
944
|
-
|
|
945
|
-
return assertAllowedPath(directory, [workspace.root]);
|
|
331
|
+
return this.context.resolveWorkingDirectory(workspace, workingDirectory);
|
|
946
332
|
}
|
|
947
333
|
async openCheckoutWorkspace(path) {
|
|
948
334
|
const root = assertAllowedPath(path, this.config.allowedRoots);
|
|
@@ -972,7 +358,7 @@ export class WorkspaceRegistry {
|
|
|
972
358
|
mode: input.mode,
|
|
973
359
|
sourceRoot: input.sourceRoot,
|
|
974
360
|
worktree: input.worktree,
|
|
975
|
-
...this.loadSkillsForWorkspace(input.root),
|
|
361
|
+
...this.context.loadSkillsForWorkspace(input.root),
|
|
976
362
|
capabilityGuides: loadCapabilityGuides(this.config),
|
|
977
363
|
agentProfiles: await loadSubagentProfiles(this.config, input.root),
|
|
978
364
|
activatedSkillDirs: new Set(),
|
|
@@ -980,6 +366,7 @@ export class WorkspaceRegistry {
|
|
|
980
366
|
scannedInstructionDirs: new Set(),
|
|
981
367
|
knownInstructionPathsByDir: new Map(),
|
|
982
368
|
loadedInstructionRealPaths: new Set(),
|
|
369
|
+
loadedInstructionPaths: new Set(),
|
|
983
370
|
};
|
|
984
371
|
this.store?.createSession({
|
|
985
372
|
id: workspace.id,
|
|
@@ -1005,8 +392,8 @@ export class WorkspaceRegistry {
|
|
|
1005
392
|
targetBranch: workspace.worktree?.targetBranch,
|
|
1006
393
|
},
|
|
1007
394
|
});
|
|
1008
|
-
const agentsFiles = await this.loadInitialAgentsFiles(workspace);
|
|
1009
|
-
const availableAgentsFiles = await this.findAvailableAgentsFiles(workspace, agentsFiles);
|
|
395
|
+
const agentsFiles = await this.context.loadInitialAgentsFiles(workspace);
|
|
396
|
+
const availableAgentsFiles = await this.context.findAvailableAgentsFiles(workspace, agentsFiles);
|
|
1010
397
|
const { contextFingerprint, componentFingerprints: bootstrapComponentFingerprints, } = bootstrapContextFingerprints(workspace, agentsFiles, availableAgentsFiles);
|
|
1011
398
|
return {
|
|
1012
399
|
workspace,
|
|
@@ -1020,318 +407,9 @@ export class WorkspaceRegistry {
|
|
|
1020
407
|
includeBootstrapContext: true,
|
|
1021
408
|
};
|
|
1022
409
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
return {
|
|
1026
|
-
skills: result.skills,
|
|
1027
|
-
skillDiagnostics: result.diagnostics,
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
assertWorkspaceRootAllowed(root, mode, sourceRoot) {
|
|
1031
|
-
if (mode === "worktree") {
|
|
1032
|
-
if (!sourceRoot) {
|
|
1033
|
-
throw new Error(`Stored worktree workspace is missing sourceRoot: ${root}`);
|
|
1034
|
-
}
|
|
1035
|
-
assertAllowedPath(sourceRoot, this.config.allowedRoots);
|
|
1036
|
-
return assertAllowedPath(root, [this.config.worktreeRoot]);
|
|
1037
|
-
}
|
|
1038
|
-
return assertAllowedPath(root, this.config.allowedRoots);
|
|
1039
|
-
}
|
|
1040
|
-
async loadInitialAgentsFiles(workspace) {
|
|
1041
|
-
const systemInstructionsPath = resolve(this.config.systemInstructionsPath);
|
|
1042
|
-
const loadedFiles = [];
|
|
1043
|
-
const systemInstructions = await readSystemInstructions(systemInstructionsPath);
|
|
1044
|
-
const systemInstructionsRealPath = await tryRealpath(systemInstructionsPath);
|
|
1045
|
-
if (systemInstructions !== undefined) {
|
|
1046
|
-
loadedFiles.push({
|
|
1047
|
-
path: systemInstructionsPath,
|
|
1048
|
-
content: systemInstructions,
|
|
1049
|
-
});
|
|
1050
|
-
if (systemInstructionsRealPath) {
|
|
1051
|
-
workspace.loadedInstructionRealPaths.add(systemInstructionsRealPath);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
await this.discoverInstructionTree(workspace, workspace.root, INITIAL_INSTRUCTION_DISCOVERY_DEPTH);
|
|
1055
|
-
loadedFiles.push(...await this.loadKnownInstructionsInDirectory(workspace, workspace.root));
|
|
1056
|
-
return loadedFiles;
|
|
1057
|
-
}
|
|
1058
|
-
async findAvailableAgentsFiles(workspace, loadedFiles) {
|
|
1059
|
-
const loadedPaths = new Set(loadedFiles.map((file) => resolve(file.path)));
|
|
1060
|
-
const discovered = [];
|
|
1061
|
-
for (const paths of workspace.knownInstructionPathsByDir.values()) {
|
|
1062
|
-
for (const path of paths) {
|
|
1063
|
-
if (loadedPaths.has(path))
|
|
1064
|
-
continue;
|
|
1065
|
-
const realPath = await tryRealpath(path);
|
|
1066
|
-
if (realPath && workspace.loadedInstructionRealPaths.has(realPath))
|
|
1067
|
-
continue;
|
|
1068
|
-
discovered.push({ path });
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
return discovered.sort((a, b) => a.path.localeCompare(b.path));
|
|
1072
|
-
}
|
|
1073
|
-
async discoverPathInstructions(workspace, inputPath) {
|
|
1074
|
-
const absolutePath = resolve(inputPath);
|
|
1075
|
-
if (!isPathInsideRoot(absolutePath, workspace.root))
|
|
1076
|
-
return [];
|
|
1077
|
-
const targetDirectory = dirname(absolutePath);
|
|
1078
|
-
const relationship = relative(workspace.root, targetDirectory);
|
|
1079
|
-
if (relationship === ".." ||
|
|
1080
|
-
relationship.startsWith(`..${sep}`) ||
|
|
1081
|
-
resolve(targetDirectory) === resolve(this.config.agentDir) ||
|
|
1082
|
-
isPathInsideRoot(targetDirectory, resolve(this.config.agentDir))) {
|
|
1083
|
-
return [];
|
|
1084
|
-
}
|
|
1085
|
-
const directories = [resolve(workspace.root)];
|
|
1086
|
-
if (relationship) {
|
|
1087
|
-
let current = resolve(workspace.root);
|
|
1088
|
-
for (const segment of relationship.split(sep).filter(Boolean)) {
|
|
1089
|
-
if (SKIPPED_CONTEXT_DIRS.has(segment))
|
|
1090
|
-
break;
|
|
1091
|
-
current = join(current, segment);
|
|
1092
|
-
directories.push(current);
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
const loaded = [];
|
|
1096
|
-
for (const directory of directories) {
|
|
1097
|
-
await this.discoverInstructionTree(workspace, directory, 0);
|
|
1098
|
-
loaded.push(...await this.loadKnownInstructionsInDirectory(workspace, directory));
|
|
1099
|
-
}
|
|
1100
|
-
return loaded;
|
|
1101
|
-
}
|
|
1102
|
-
async discoverInstructionTree(workspace, directory, remainingDepth) {
|
|
1103
|
-
const resolvedDirectory = resolve(directory);
|
|
1104
|
-
if (workspace.scannedInstructionDirs.has(resolvedDirectory))
|
|
1105
|
-
return;
|
|
1106
|
-
workspace.scannedInstructionDirs.add(resolvedDirectory);
|
|
1107
|
-
if (resolvedDirectory !== resolve(workspace.root) &&
|
|
1108
|
-
isPathInsideRoot(resolvedDirectory, resolve(this.config.agentDir))) {
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
let entries;
|
|
1112
|
-
try {
|
|
1113
|
-
entries = await opendir(resolvedDirectory);
|
|
1114
|
-
}
|
|
1115
|
-
catch {
|
|
1116
|
-
return;
|
|
1117
|
-
}
|
|
1118
|
-
const instructionPaths = [];
|
|
1119
|
-
const childDirectories = [];
|
|
1120
|
-
for await (const entry of entries) {
|
|
1121
|
-
const path = join(resolvedDirectory, entry.name);
|
|
1122
|
-
if (entry.isFile() && CONTEXT_FILE_NAMES.has(entry.name)) {
|
|
1123
|
-
instructionPaths.push(path);
|
|
1124
|
-
continue;
|
|
1125
|
-
}
|
|
1126
|
-
if (remainingDepth > 0 &&
|
|
1127
|
-
entry.isDirectory() &&
|
|
1128
|
-
!SKIPPED_CONTEXT_DIRS.has(entry.name)) {
|
|
1129
|
-
childDirectories.push(path);
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
workspace.knownInstructionPathsByDir.set(resolvedDirectory, instructionPaths.sort((left, right) => left.localeCompare(right)));
|
|
1133
|
-
if (remainingDepth <= 0)
|
|
1134
|
-
return;
|
|
1135
|
-
for (const childDirectory of childDirectories) {
|
|
1136
|
-
await this.discoverInstructionTree(workspace, childDirectory, remainingDepth - 1);
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
async loadKnownInstructionsInDirectory(workspace, directory) {
|
|
1140
|
-
const resolvedDirectory = resolve(directory);
|
|
1141
|
-
const paths = workspace.knownInstructionPathsByDir.get(resolvedDirectory) ?? [];
|
|
1142
|
-
const loaded = [];
|
|
1143
|
-
const resolvedRoot = (await tryRealpath(workspace.root)) ?? resolve(workspace.root);
|
|
1144
|
-
const realDirectory = (await tryRealpath(resolvedDirectory)) ?? resolvedDirectory;
|
|
1145
|
-
for (const path of paths) {
|
|
1146
|
-
const realPath = await tryRealpath(path);
|
|
1147
|
-
if (!realPath)
|
|
1148
|
-
continue;
|
|
1149
|
-
if (!isPathInsideRoot(realPath, resolvedRoot))
|
|
1150
|
-
continue;
|
|
1151
|
-
if (dirname(realPath) !== realDirectory)
|
|
1152
|
-
continue;
|
|
1153
|
-
if (workspace.loadedInstructionRealPaths.has(realPath))
|
|
1154
|
-
continue;
|
|
1155
|
-
let content;
|
|
1156
|
-
try {
|
|
1157
|
-
content = await readFile(realPath, "utf8");
|
|
1158
|
-
}
|
|
1159
|
-
catch (error) {
|
|
1160
|
-
if (isErrnoException(error) && (error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
1161
|
-
continue;
|
|
1162
|
-
throw error;
|
|
1163
|
-
}
|
|
1164
|
-
workspace.loadedInstructionRealPaths.add(realPath);
|
|
1165
|
-
loaded.push({ path, content });
|
|
1166
|
-
}
|
|
1167
|
-
return loaded;
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
const BOOTSTRAP_CONTEXT_COMPONENTS = [
|
|
1171
|
-
"agentsFiles",
|
|
1172
|
-
"availableAgentsFiles",
|
|
1173
|
-
"skills",
|
|
1174
|
-
"skillDiagnostics",
|
|
1175
|
-
"capabilityGuides",
|
|
1176
|
-
"agentProfiles",
|
|
1177
|
-
];
|
|
1178
|
-
function resolveBootstrapContextComponents(mode, currentFingerprints, deliveries, contextFingerprint) {
|
|
1179
|
-
if (mode === "none")
|
|
1180
|
-
return [];
|
|
1181
|
-
if (mode === "full")
|
|
1182
|
-
return [...BOOTSTRAP_CONTEXT_COMPONENTS];
|
|
1183
|
-
if (deliveries.length === 0)
|
|
1184
|
-
return [...BOOTSTRAP_CONTEXT_COMPONENTS];
|
|
1185
|
-
if (contextFingerprint &&
|
|
1186
|
-
deliveries.some((delivery) => !delivery.componentFingerprints && delivery.contextFingerprint === contextFingerprint)) {
|
|
1187
|
-
return [];
|
|
1188
|
-
}
|
|
1189
|
-
return BOOTSTRAP_CONTEXT_COMPONENTS.filter((component) => !deliveries.some((delivery) => delivery.componentFingerprints?.[component] === currentFingerprints[component]));
|
|
1190
|
-
}
|
|
1191
|
-
function bootstrapContextFingerprints(workspace, agentsFiles, availableAgentsFiles) {
|
|
1192
|
-
const payload = {
|
|
1193
|
-
agentsFiles: agentsFiles
|
|
1194
|
-
.map((file) => ({ path: resolve(file.path), content: file.content }))
|
|
1195
|
-
.sort((left, right) => left.path.localeCompare(right.path)),
|
|
1196
|
-
availableAgentsFiles: availableAgentsFiles
|
|
1197
|
-
.map((file) => resolve(file.path))
|
|
1198
|
-
.sort((left, right) => left.localeCompare(right)),
|
|
1199
|
-
skills: workspace.skills
|
|
1200
|
-
.map((skill) => ({
|
|
1201
|
-
name: skill.name,
|
|
1202
|
-
description: skill.description,
|
|
1203
|
-
filePath: resolve(skill.filePath),
|
|
1204
|
-
disableModelInvocation: skill.disableModelInvocation ?? false,
|
|
1205
|
-
}))
|
|
1206
|
-
.sort((left, right) => left.name.localeCompare(right.name) || left.filePath.localeCompare(right.filePath)),
|
|
1207
|
-
skillDiagnostics: workspace.skillDiagnostics,
|
|
1208
|
-
capabilityGuides: workspace.capabilityGuides
|
|
1209
|
-
.map((guide) => ({
|
|
1210
|
-
name: guide.name,
|
|
1211
|
-
description: guide.description,
|
|
1212
|
-
whenToRead: guide.whenToRead,
|
|
1213
|
-
filePath: resolve(guide.filePath),
|
|
1214
|
-
}))
|
|
1215
|
-
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
1216
|
-
agentProfiles: workspace.agentProfiles
|
|
1217
|
-
.map((profile) => ({
|
|
1218
|
-
name: profile.name,
|
|
1219
|
-
description: profile.description,
|
|
1220
|
-
provider: profile.provider,
|
|
1221
|
-
model: profile.model,
|
|
1222
|
-
thinking: profile.thinking,
|
|
1223
|
-
filePath: resolve(profile.filePath),
|
|
1224
|
-
}))
|
|
1225
|
-
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
1226
|
-
};
|
|
1227
|
-
const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
1228
|
-
return {
|
|
1229
|
-
contextFingerprint: hash(payload),
|
|
1230
|
-
componentFingerprints: {
|
|
1231
|
-
agentsFiles: hash(payload.agentsFiles),
|
|
1232
|
-
availableAgentsFiles: hash(payload.availableAgentsFiles),
|
|
1233
|
-
skills: hash(payload.skills),
|
|
1234
|
-
skillDiagnostics: hash(payload.skillDiagnostics),
|
|
1235
|
-
capabilityGuides: hash(payload.capabilityGuides),
|
|
1236
|
-
agentProfiles: hash(payload.agentProfiles),
|
|
1237
|
-
},
|
|
1238
|
-
};
|
|
1239
|
-
}
|
|
1240
|
-
function canonicalPersistedWorkspacePath(path) {
|
|
1241
|
-
const missingSegments = [];
|
|
1242
|
-
let candidate = path;
|
|
1243
|
-
while (true) {
|
|
1244
|
-
try {
|
|
1245
|
-
return resolve(realpathSync(candidate), ...missingSegments.slice().reverse());
|
|
1246
|
-
}
|
|
1247
|
-
catch (error) {
|
|
1248
|
-
if (!isErrnoException(error) || (error.code !== "ENOENT" && error.code !== "ENOTDIR")) {
|
|
1249
|
-
return resolve(path);
|
|
1250
|
-
}
|
|
1251
|
-
const parent = dirname(candidate);
|
|
1252
|
-
if (parent === candidate)
|
|
1253
|
-
return resolve(path);
|
|
1254
|
-
missingSegments.push(basename(candidate));
|
|
1255
|
-
candidate = parent;
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
async function canonicalPath(path) {
|
|
1260
|
-
const missingSegments = [];
|
|
1261
|
-
let candidate = path;
|
|
1262
|
-
while (true) {
|
|
1263
|
-
try {
|
|
1264
|
-
return resolve(await realpath(candidate), ...missingSegments.slice().reverse());
|
|
1265
|
-
}
|
|
1266
|
-
catch (error) {
|
|
1267
|
-
if (!isErrnoException(error) || (error.code !== "ENOENT" && error.code !== "ENOTDIR")) {
|
|
1268
|
-
throw error;
|
|
1269
|
-
}
|
|
1270
|
-
const parent = dirname(candidate);
|
|
1271
|
-
if (parent === candidate)
|
|
1272
|
-
return path;
|
|
1273
|
-
missingSegments.push(basename(candidate));
|
|
1274
|
-
candidate = parent;
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
export async function ensureCheckoutWorkspaceRoot(path, ops = { stat, mkdir }) {
|
|
1279
|
-
try {
|
|
1280
|
-
return await ops.stat(path);
|
|
1281
|
-
}
|
|
1282
|
-
catch (error) {
|
|
1283
|
-
if (!isErrnoException(error) || error.code !== "ENOENT") {
|
|
1284
|
-
throw error;
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
await ops.mkdir(path, { recursive: true });
|
|
1288
|
-
return await ops.stat(path);
|
|
1289
|
-
}
|
|
1290
|
-
const CONTEXT_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
|
|
1291
|
-
const SKIPPED_CONTEXT_DIRS = new Set([
|
|
1292
|
-
".git",
|
|
1293
|
-
".hg",
|
|
1294
|
-
".svn",
|
|
1295
|
-
".forgerelay",
|
|
1296
|
-
".devspace",
|
|
1297
|
-
"node_modules",
|
|
1298
|
-
"dist",
|
|
1299
|
-
"build",
|
|
1300
|
-
".next",
|
|
1301
|
-
".turbo",
|
|
1302
|
-
".cache",
|
|
1303
|
-
]);
|
|
1304
|
-
export function formatAgentsPath(path, workspaceRoot) {
|
|
1305
|
-
if (!workspaceRoot)
|
|
1306
|
-
return path.split(sep).join("/");
|
|
1307
|
-
const relationship = relative(workspaceRoot, path);
|
|
1308
|
-
if (relationship === "" ||
|
|
1309
|
-
relationship.startsWith("..") ||
|
|
1310
|
-
relationship === ".." ||
|
|
1311
|
-
relationship.includes(`..${sep}`)) {
|
|
1312
|
-
return path.split(sep).join("/");
|
|
410
|
+
discoverPathInstructions(workspace, inputPath) {
|
|
411
|
+
return this.context.discoverPathInstructions(workspace, inputPath);
|
|
1313
412
|
}
|
|
1314
|
-
return relationship.split(sep).join("/");
|
|
1315
|
-
}
|
|
1316
|
-
async function readSystemInstructions(path) {
|
|
1317
|
-
try {
|
|
1318
|
-
return await readFile(path, "utf8");
|
|
1319
|
-
}
|
|
1320
|
-
catch (error) {
|
|
1321
|
-
if (isErrnoException(error) && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1322
|
-
return undefined;
|
|
1323
|
-
}
|
|
1324
|
-
throw error;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
async function tryRealpath(path) {
|
|
1328
|
-
try {
|
|
1329
|
-
return await realpath(path);
|
|
1330
|
-
}
|
|
1331
|
-
catch {
|
|
1332
|
-
return undefined;
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
function isErrnoException(error) {
|
|
1336
|
-
return error instanceof Error && "code" in error;
|
|
1337
413
|
}
|
|
414
|
+
export { formatAgentsPath };
|
|
415
|
+
export { ensureCheckoutWorkspaceRoot } from "./workspaces/paths.js";
|