@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
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
2
|
+
import * as z from "zod/v4";
|
|
3
|
+
import { CapabilityError } from "../../core/capability-registry.js";
|
|
4
|
+
import { attachHookReports, runToolWithHooks } from "../../../hooks/hooks.js";
|
|
5
|
+
import { toolNames } from "../../../server-instructions.js";
|
|
6
|
+
import { formatAgentsPath } from "../../../../workspaces.js";
|
|
7
|
+
import { activityRelationFor, activityRequestFor, runActivityToolWithHooks, } from "../../core/activity-support.js";
|
|
8
|
+
import { capabilityContextFor, compositeCapabilityContext, workspaceHookInvocation, } from "../../core/capability-support.js";
|
|
9
|
+
import { capabilityErrorOutputSchema, resultOutputSchema } from "../../core/schemas.js";
|
|
10
|
+
import { logFailedToolResponse, logToolCall, textBlock, toolResultIsError, workspaceLogContext, } from "../../core/tool-support.js";
|
|
11
|
+
const WRITE_TOOL_ANNOTATIONS = {
|
|
12
|
+
readOnlyHint: false,
|
|
13
|
+
destructiveHint: true,
|
|
14
|
+
idempotentHint: false,
|
|
15
|
+
openWorldHint: false,
|
|
16
|
+
};
|
|
17
|
+
export function registerWorkspaceAuxiliaryTools(options) {
|
|
18
|
+
const { server, config, workspaces, remoteWorkspaces, compositeWorkspaces, compositeTaskGuides, capabilityRegistry, coreOperations, activityLifecycle, hooks, workspaceTasks, taskReminders, activityQueries, compositeActivity, workspacePanelStates, processSessions, reviewCheckpoints, codeIntelligence, resolveExecutionTarget, prepareExecutionContext, hostScopeIdFor, presentExecutionResult, presentSemanticWorkResult, } = options;
|
|
19
|
+
registerAppTool(server, "workspace_instruction", {
|
|
20
|
+
title: "Read Workspace instruction",
|
|
21
|
+
description: "App-only lazy data source for one instruction file already advertised by the Workspace presentation. Reading through this UI source does not activate nested instructions or create an Activity.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
workspaceId: z.string().min(1),
|
|
24
|
+
path: z.string().min(1),
|
|
25
|
+
},
|
|
26
|
+
outputSchema: {
|
|
27
|
+
path: z.string(),
|
|
28
|
+
content: z.string(),
|
|
29
|
+
status: z.enum(["loaded", "available"]),
|
|
30
|
+
},
|
|
31
|
+
_meta: { ui: { visibility: ["app"] } },
|
|
32
|
+
annotations: {
|
|
33
|
+
readOnlyHint: true,
|
|
34
|
+
destructiveHint: false,
|
|
35
|
+
idempotentHint: true,
|
|
36
|
+
openWorldHint: false,
|
|
37
|
+
},
|
|
38
|
+
}, async ({ workspaceId, path }, extra) => {
|
|
39
|
+
if (remoteWorkspaces.has(workspaceId)) {
|
|
40
|
+
return remoteWorkspaces.workspaceInstruction(workspaceId, path, hostScopeIdFor(extra._meta, extra.sessionId));
|
|
41
|
+
}
|
|
42
|
+
if (compositeWorkspaces.has(workspaceId)) {
|
|
43
|
+
throw new Error("Composite Workspace instructions belong to a selected member; open that member context before viewing its instruction files.");
|
|
44
|
+
}
|
|
45
|
+
const workspace = workspaces.getWorkspace(workspaceId);
|
|
46
|
+
const instruction = await workspaces.readAdvertisedInstruction(workspace, path);
|
|
47
|
+
return {
|
|
48
|
+
content: [],
|
|
49
|
+
structuredContent: {
|
|
50
|
+
path: formatAgentsPath(instruction.path, workspace.root),
|
|
51
|
+
content: instruction.content,
|
|
52
|
+
status: instruction.status,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
registerAppTool(server, toolNames.capability, {
|
|
57
|
+
title: "Use optional capability",
|
|
58
|
+
description: "Describe or run one optional ForgeRelay capability advertised by open_workspace. Use describe when the capability contract is unfamiliar, then read its advertised guide if needed. Run dispatches only explicitly registered capabilities; it cannot invoke arbitrary shell commands, URLs, or methods.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
|
|
61
|
+
member: z.string().optional().describe("Required for a Composite Workspace; explicit member name whose capability surface is used."),
|
|
62
|
+
name: z
|
|
63
|
+
.string()
|
|
64
|
+
.regex(/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/)
|
|
65
|
+
.describe("Stable dotted capability name advertised by open_workspace."),
|
|
66
|
+
action: z.enum(["describe", "run"]),
|
|
67
|
+
arguments: z
|
|
68
|
+
.record(z.string(), z.unknown())
|
|
69
|
+
.optional()
|
|
70
|
+
.describe("Capability-specific JSON arguments. Omit for describe and for capabilities with no arguments."),
|
|
71
|
+
file: z
|
|
72
|
+
.unknown()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe("Host-native file value. Only capabilities whose describe result advertises native-file transport may consume it."),
|
|
75
|
+
},
|
|
76
|
+
outputSchema: {
|
|
77
|
+
name: z.string(),
|
|
78
|
+
action: z.enum(["describe", "run"]),
|
|
79
|
+
member: z.string().optional(),
|
|
80
|
+
capability: z.unknown().optional(),
|
|
81
|
+
result: z.unknown().optional(),
|
|
82
|
+
error: capabilityErrorOutputSchema.optional(),
|
|
83
|
+
},
|
|
84
|
+
_meta: {
|
|
85
|
+
"openai/fileParams": ["file"],
|
|
86
|
+
},
|
|
87
|
+
annotations: {
|
|
88
|
+
readOnlyHint: false,
|
|
89
|
+
destructiveHint: false,
|
|
90
|
+
idempotentHint: false,
|
|
91
|
+
openWorldHint: true,
|
|
92
|
+
},
|
|
93
|
+
}, async ({ workspaceId, member, name, action, arguments: capabilityArguments, file }, extra) => {
|
|
94
|
+
if (name === "workspace.tasks" && compositeWorkspaces.has(workspaceId)) {
|
|
95
|
+
if (member !== undefined) {
|
|
96
|
+
throw new Error(`workspace.tasks belongs to Composite Workspace ${workspaceId} itself and does not accept member.`);
|
|
97
|
+
}
|
|
98
|
+
if (!compositeWorkspaces.isActive(workspaceId)) {
|
|
99
|
+
throw new Error(`Composite Workspace ${workspaceId} is closed. Reopen it with open_workspace before use.`);
|
|
100
|
+
}
|
|
101
|
+
const startedAt = performance.now();
|
|
102
|
+
const context = compositeCapabilityContext(workspaceId, compositeTaskGuides);
|
|
103
|
+
try {
|
|
104
|
+
if (action === "run") {
|
|
105
|
+
const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, context, {
|
|
106
|
+
nativeFile: file,
|
|
107
|
+
signal: extra.signal,
|
|
108
|
+
requestMeta: extra._meta,
|
|
109
|
+
sessionId: extra.sessionId,
|
|
110
|
+
});
|
|
111
|
+
const result = {
|
|
112
|
+
content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
|
|
113
|
+
structuredContent: { name, action, result: execution.value },
|
|
114
|
+
};
|
|
115
|
+
logToolCall(config, {
|
|
116
|
+
tool: toolNames.capability,
|
|
117
|
+
capability: name,
|
|
118
|
+
action,
|
|
119
|
+
success: true,
|
|
120
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
121
|
+
});
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
const capability = capabilityRegistry.describe(name, context);
|
|
125
|
+
const result = {
|
|
126
|
+
content: [textBlock([
|
|
127
|
+
`${capability.name}: ${capability.description}`,
|
|
128
|
+
`Available: ${capability.available}`,
|
|
129
|
+
`Guide: ${capability.guide.path}`,
|
|
130
|
+
capability.guide.readBeforeFirstUse
|
|
131
|
+
? "Read the guide before first use when this contract is unfamiliar."
|
|
132
|
+
: undefined,
|
|
133
|
+
].filter(Boolean).join("\n"))],
|
|
134
|
+
structuredContent: { name, action, capability },
|
|
135
|
+
};
|
|
136
|
+
logToolCall(config, {
|
|
137
|
+
tool: toolNames.capability,
|
|
138
|
+
capability: name,
|
|
139
|
+
action,
|
|
140
|
+
success: true,
|
|
141
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
142
|
+
});
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (extra.signal.aborted)
|
|
147
|
+
throw error;
|
|
148
|
+
const capabilityError = error instanceof CapabilityError
|
|
149
|
+
? error
|
|
150
|
+
: new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
|
|
151
|
+
return {
|
|
152
|
+
content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
|
|
153
|
+
structuredContent: {
|
|
154
|
+
name,
|
|
155
|
+
action,
|
|
156
|
+
error: { code: capabilityError.code, message: capabilityError.message },
|
|
157
|
+
},
|
|
158
|
+
isError: true,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const target = resolveExecutionTarget(workspaceId, member);
|
|
163
|
+
const executionWorkspaceId = target.executionWorkspaceId;
|
|
164
|
+
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
165
|
+
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
166
|
+
const response = await remoteWorkspaces.capability(executionWorkspaceId, {
|
|
167
|
+
name,
|
|
168
|
+
action,
|
|
169
|
+
...(capabilityArguments !== undefined ? { arguments: capabilityArguments } : {}),
|
|
170
|
+
...(file !== undefined ? { file } : {}),
|
|
171
|
+
}, hostScopeIdFor(extra._meta, extra.sessionId));
|
|
172
|
+
return action === "run" && name !== "workspace.tasks"
|
|
173
|
+
? presentSemanticWorkResult(response, target)
|
|
174
|
+
: presentExecutionResult(response, target);
|
|
175
|
+
}
|
|
176
|
+
if (action === "run" && name === "batch.execute") {
|
|
177
|
+
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
178
|
+
const startedAt = performance.now();
|
|
179
|
+
try {
|
|
180
|
+
const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), {
|
|
181
|
+
nativeFile: file,
|
|
182
|
+
signal: extra.signal,
|
|
183
|
+
requestMeta: extra._meta,
|
|
184
|
+
sessionId: extra.sessionId,
|
|
185
|
+
});
|
|
186
|
+
const result = {
|
|
187
|
+
content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
|
|
188
|
+
structuredContent: { name, action, result: execution.value },
|
|
189
|
+
};
|
|
190
|
+
logToolCall(config, {
|
|
191
|
+
tool: toolNames.capability,
|
|
192
|
+
...workspaceLogContext(workspace, extra.sessionId),
|
|
193
|
+
capability: name,
|
|
194
|
+
action,
|
|
195
|
+
success: true,
|
|
196
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
197
|
+
});
|
|
198
|
+
return presentSemanticWorkResult(result, target);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (extra.signal.aborted)
|
|
202
|
+
throw error;
|
|
203
|
+
const capabilityError = error instanceof CapabilityError
|
|
204
|
+
? error
|
|
205
|
+
: new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
|
|
206
|
+
const result = {
|
|
207
|
+
content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
|
|
208
|
+
structuredContent: {
|
|
209
|
+
name,
|
|
210
|
+
action,
|
|
211
|
+
error: { code: capabilityError.code, message: capabilityError.message },
|
|
212
|
+
},
|
|
213
|
+
isError: true,
|
|
214
|
+
};
|
|
215
|
+
logFailedToolResponse(config, {
|
|
216
|
+
tool: toolNames.capability,
|
|
217
|
+
...workspaceLogContext(workspace, extra.sessionId),
|
|
218
|
+
capability: name,
|
|
219
|
+
action,
|
|
220
|
+
}, result.content, startedAt);
|
|
221
|
+
return presentExecutionResult(result, target);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (action === "run") {
|
|
225
|
+
const response = await coreOperations.capabilityRun({ workspaceId: executionWorkspaceId, name, arguments: capabilityArguments, file }, executionContext);
|
|
226
|
+
return name === "workspace.tasks"
|
|
227
|
+
? presentExecutionResult(response, target)
|
|
228
|
+
: presentSemanticWorkResult(response, target);
|
|
229
|
+
}
|
|
230
|
+
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
231
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, name, action, arguments: capabilityArguments, file }, executionContext), {
|
|
232
|
+
signal: extra.signal,
|
|
233
|
+
tool: toolNames.capability,
|
|
234
|
+
invocation: workspaceHookInvocation(workspace),
|
|
235
|
+
payload: { name, action },
|
|
236
|
+
isFailure: toolResultIsError,
|
|
237
|
+
operation: async () => {
|
|
238
|
+
const startedAt = performance.now();
|
|
239
|
+
try {
|
|
240
|
+
const capability = capabilityRegistry.describe(name, capabilityContextFor(workspace));
|
|
241
|
+
const result = {
|
|
242
|
+
content: [textBlock([
|
|
243
|
+
`${capability.name}: ${capability.description}`,
|
|
244
|
+
`Available: ${capability.available}`,
|
|
245
|
+
`Guide: ${capability.guide.path}`,
|
|
246
|
+
capability.guide.readBeforeFirstUse
|
|
247
|
+
? "Read the guide before first use when this contract is unfamiliar."
|
|
248
|
+
: undefined,
|
|
249
|
+
].filter(Boolean).join("\n"))],
|
|
250
|
+
structuredContent: { name, action, capability },
|
|
251
|
+
};
|
|
252
|
+
logToolCall(config, {
|
|
253
|
+
tool: toolNames.capability,
|
|
254
|
+
...workspaceLogContext(workspace),
|
|
255
|
+
capability: name,
|
|
256
|
+
action,
|
|
257
|
+
success: true,
|
|
258
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
259
|
+
});
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
const capabilityError = error instanceof CapabilityError
|
|
264
|
+
? error
|
|
265
|
+
: new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
|
|
266
|
+
const result = {
|
|
267
|
+
content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
|
|
268
|
+
structuredContent: {
|
|
269
|
+
name,
|
|
270
|
+
action,
|
|
271
|
+
error: { code: capabilityError.code, message: capabilityError.message },
|
|
272
|
+
},
|
|
273
|
+
isError: true,
|
|
274
|
+
};
|
|
275
|
+
logFailedToolResponse(config, {
|
|
276
|
+
tool: toolNames.capability,
|
|
277
|
+
...workspaceLogContext(workspace),
|
|
278
|
+
capability: name,
|
|
279
|
+
action,
|
|
280
|
+
}, result.content, startedAt);
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
}, activityRelationFor(executionContext)).then((result) => presentExecutionResult(result, target));
|
|
285
|
+
});
|
|
286
|
+
registerAppTool(server, toolNames.closeWorkspace, {
|
|
287
|
+
title: "Close workspace",
|
|
288
|
+
description: "Close or explicitly delete one Workspace after the user chooses cleanup. action=close (default) preserves checkout, managed-worktree, Composite, and relayed identity for later reopen. action=delete permanently removes ForgeRelay-owned state. Managed-worktree-backed Workspaces still finalize safely when active and require commitMessage. Composite delete dissolves only Composite-owned state and never closes member Workspaces. Checkout project files are never deleted.",
|
|
289
|
+
inputSchema: {
|
|
290
|
+
workspaceId: z.string().describe("Workspace identifier to close or delete."),
|
|
291
|
+
action: z
|
|
292
|
+
.enum(["close", "delete"])
|
|
293
|
+
.optional()
|
|
294
|
+
.describe("Defaults to close. close preserves checkout identity, managed-worktree identity, and Composite identity for later reopen; delete removes ForgeRelay-owned state. Composite delete dissolves only the Composite relationship. Active managed worktrees still require safe finalization and commitMessage; checkout project files are never deleted."),
|
|
295
|
+
commitMessage: z
|
|
296
|
+
.string()
|
|
297
|
+
.min(1)
|
|
298
|
+
.optional()
|
|
299
|
+
.describe("Required only for a managed-worktree-backed workspace; concise Git commit message for remaining worktree changes."),
|
|
300
|
+
},
|
|
301
|
+
outputSchema: resultOutputSchema({
|
|
302
|
+
workspaceId: z.string(),
|
|
303
|
+
action: z.enum(["close", "delete"]).optional(),
|
|
304
|
+
kind: z.enum(["workspace", "composite"]).optional(),
|
|
305
|
+
mode: z.enum(["checkout", "worktree"]).optional(),
|
|
306
|
+
name: z.string().optional(),
|
|
307
|
+
members: z.array(z.object({
|
|
308
|
+
name: z.string(),
|
|
309
|
+
purpose: z.string(),
|
|
310
|
+
workspaceId: z.string(),
|
|
311
|
+
})).optional(),
|
|
312
|
+
status: z.enum(["active", "closed"]).optional(),
|
|
313
|
+
dissolved: z.boolean().optional(),
|
|
314
|
+
sourceRoot: z.string().optional(),
|
|
315
|
+
branch: z.string().optional(),
|
|
316
|
+
targetBranch: z.string().optional(),
|
|
317
|
+
commitSha: z.string().optional(),
|
|
318
|
+
mergedSha: z.string().optional(),
|
|
319
|
+
committed: z.boolean().optional(),
|
|
320
|
+
cleanupWarning: z.string().optional(),
|
|
321
|
+
}),
|
|
322
|
+
_meta: {},
|
|
323
|
+
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
324
|
+
}, async ({ workspaceId, action = "close", commitMessage }, extra) => {
|
|
325
|
+
if (compositeWorkspaces.has(workspaceId)) {
|
|
326
|
+
if (commitMessage !== undefined) {
|
|
327
|
+
throw new Error("close_workspace commitMessage is not valid for a Composite Workspace.");
|
|
328
|
+
}
|
|
329
|
+
const composite = action === "delete"
|
|
330
|
+
? compositeWorkspaces.dissolve(workspaceId)
|
|
331
|
+
: compositeWorkspaces.close(workspaceId);
|
|
332
|
+
if (action === "delete") {
|
|
333
|
+
workspaceTasks.deleteWorkspace(workspaceId);
|
|
334
|
+
taskReminders.forget(workspaceId);
|
|
335
|
+
activityQueries.deleteWorkspaceHistory(config.stateDir, workspaceId);
|
|
336
|
+
}
|
|
337
|
+
compositeActivity.forgetComposite(workspaceId);
|
|
338
|
+
workspacePanelStates.delete(workspaceId);
|
|
339
|
+
const result = [
|
|
340
|
+
action === "delete"
|
|
341
|
+
? `Deleted Composite Workspace ${composite.name} (${workspaceId}); its Composite relationship and ForgeRelay-owned Composite state were dissolved.`
|
|
342
|
+
: `Closed Composite Workspace ${composite.name} (${workspaceId}); its identity and member topology were preserved for later reopen.`,
|
|
343
|
+
composite.members.length > 0
|
|
344
|
+
? `Preserved member Workspaces: ${composite.members.map((member) => `${member.name} [${member.workspaceId}]`).join(", ")}.`
|
|
345
|
+
: "The Composite Workspace had no members.",
|
|
346
|
+
"Member Workspace handles, managed worktrees, processes, files, and Workspace Relay routes were not closed, finalized, deleted, or otherwise mutated.",
|
|
347
|
+
].join("\n");
|
|
348
|
+
return {
|
|
349
|
+
content: [textBlock(result)],
|
|
350
|
+
_meta: {
|
|
351
|
+
tool: toolNames.closeWorkspace,
|
|
352
|
+
card: {
|
|
353
|
+
workspaceId,
|
|
354
|
+
action,
|
|
355
|
+
kind: "composite",
|
|
356
|
+
name: composite.name,
|
|
357
|
+
members: composite.members,
|
|
358
|
+
...(action === "close" ? { status: "closed" } : {}),
|
|
359
|
+
dissolved: action === "delete",
|
|
360
|
+
payload: { content: [textBlock(result)] },
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
structuredContent: {
|
|
364
|
+
result,
|
|
365
|
+
workspaceId,
|
|
366
|
+
action,
|
|
367
|
+
kind: "composite",
|
|
368
|
+
name: composite.name,
|
|
369
|
+
members: composite.members,
|
|
370
|
+
...(action === "close" ? { status: "closed" } : {}),
|
|
371
|
+
dissolved: action === "delete",
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (remoteWorkspaces.has(workspaceId)) {
|
|
376
|
+
const response = await remoteWorkspaces.closeWorkspace(workspaceId, { action, ...(commitMessage !== undefined ? { commitMessage } : {}) }, hostScopeIdFor(extra._meta, extra.sessionId));
|
|
377
|
+
if (action === "delete") {
|
|
378
|
+
activityQueries.deleteWorkspaceHistory(config.stateDir, workspaceId);
|
|
379
|
+
}
|
|
380
|
+
workspacePanelStates.delete(workspaceId);
|
|
381
|
+
return response;
|
|
382
|
+
}
|
|
383
|
+
const session = workspaces.getWorkspaceSession(workspaceId);
|
|
384
|
+
if (action === "delete" && session.mode === "checkout") {
|
|
385
|
+
if (commitMessage !== undefined) {
|
|
386
|
+
throw new Error("close_workspace commitMessage is not valid with action=delete for a checkout Workspace.");
|
|
387
|
+
}
|
|
388
|
+
if (processSessions.activeWorkspaceIds().has(session.id)) {
|
|
389
|
+
throw new Error(`Workspace ${session.id} still owns a running process. Poll, interrupt, or wait for it before deleting this Workspace.`);
|
|
390
|
+
}
|
|
391
|
+
const response = await runToolWithHooks(hooks, {
|
|
392
|
+
signal: extra.signal,
|
|
393
|
+
tool: toolNames.closeWorkspace,
|
|
394
|
+
invocation: {
|
|
395
|
+
workspaceId: session.id,
|
|
396
|
+
workspaceRoot: session.root,
|
|
397
|
+
workspaceMode: session.mode,
|
|
398
|
+
sourceRoot: session.sourceRoot,
|
|
399
|
+
},
|
|
400
|
+
payload: { workspaceId: session.id, action: "delete", mode: session.mode },
|
|
401
|
+
operation: async () => {
|
|
402
|
+
workspaces.deleteWorkspace(session.id);
|
|
403
|
+
workspaceTasks.deleteWorkspace(session.id);
|
|
404
|
+
taskReminders.forget(session.id);
|
|
405
|
+
activityQueries.deleteWorkspaceHistory(config.stateDir, session.id);
|
|
406
|
+
await reviewCheckpoints.releaseWorkspace(session.id);
|
|
407
|
+
const result = `Deleted ForgeRelay Workspace ${session.id}. Physical project files were not removed.`;
|
|
408
|
+
return {
|
|
409
|
+
content: [textBlock(result)],
|
|
410
|
+
_meta: {
|
|
411
|
+
tool: toolNames.closeWorkspace,
|
|
412
|
+
card: {
|
|
413
|
+
workspaceId: session.id,
|
|
414
|
+
action: "delete",
|
|
415
|
+
mode: "checkout",
|
|
416
|
+
payload: { content: [textBlock(result)] },
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
structuredContent: {
|
|
420
|
+
result,
|
|
421
|
+
workspaceId: session.id,
|
|
422
|
+
action: "delete",
|
|
423
|
+
mode: "checkout",
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
workspacePanelStates.delete(session.id);
|
|
429
|
+
return response;
|
|
430
|
+
}
|
|
431
|
+
if (action === "delete" && session.mode === "worktree" && session.status === "closed") {
|
|
432
|
+
if (commitMessage !== undefined) {
|
|
433
|
+
throw new Error("close_workspace commitMessage is not needed when deleting an already-closed managed-worktree Workspace.");
|
|
434
|
+
}
|
|
435
|
+
const hookRoot = session.sourceRoot ?? session.root;
|
|
436
|
+
const response = await runToolWithHooks(hooks, {
|
|
437
|
+
signal: extra.signal,
|
|
438
|
+
tool: toolNames.closeWorkspace,
|
|
439
|
+
invocation: {
|
|
440
|
+
workspaceId: session.id,
|
|
441
|
+
workspaceRoot: hookRoot,
|
|
442
|
+
workspaceMode: session.mode,
|
|
443
|
+
sourceRoot: session.sourceRoot,
|
|
444
|
+
},
|
|
445
|
+
payload: { workspaceId: session.id, action: "delete", mode: session.mode },
|
|
446
|
+
operation: async () => {
|
|
447
|
+
workspaces.deleteWorkspace(session.id);
|
|
448
|
+
workspaceTasks.deleteWorkspace(session.id);
|
|
449
|
+
taskReminders.forget(session.id);
|
|
450
|
+
activityQueries.deleteWorkspaceHistory(config.stateDir, session.id);
|
|
451
|
+
await reviewCheckpoints.releaseWorkspace(session.id);
|
|
452
|
+
const result = `Deleted closed managed-worktree Workspace ${session.id}. Its already-removed worktree backing was not recreated.`;
|
|
453
|
+
return {
|
|
454
|
+
content: [textBlock(result)],
|
|
455
|
+
_meta: {
|
|
456
|
+
tool: toolNames.closeWorkspace,
|
|
457
|
+
card: {
|
|
458
|
+
workspaceId: session.id,
|
|
459
|
+
action: "delete",
|
|
460
|
+
mode: "worktree",
|
|
461
|
+
sourceRoot: session.sourceRoot,
|
|
462
|
+
targetBranch: session.targetBranch,
|
|
463
|
+
payload: { content: [textBlock(result)] },
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
structuredContent: {
|
|
467
|
+
result,
|
|
468
|
+
workspaceId: session.id,
|
|
469
|
+
action: "delete",
|
|
470
|
+
mode: "worktree",
|
|
471
|
+
sourceRoot: session.sourceRoot,
|
|
472
|
+
targetBranch: session.targetBranch,
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
workspacePanelStates.delete(session.id);
|
|
478
|
+
return response;
|
|
479
|
+
}
|
|
480
|
+
const workspace = workspaces.getWorkspace(session.id);
|
|
481
|
+
const response = await runToolWithHooks(hooks, {
|
|
482
|
+
signal: extra.signal,
|
|
483
|
+
tool: toolNames.closeWorkspace,
|
|
484
|
+
invocation: workspaceHookInvocation(workspace),
|
|
485
|
+
payload: { workspaceId: workspace.id, action, commitMessage, mode: workspace.mode },
|
|
486
|
+
afterCwd: (response) => "sourceRoot" in response.structuredContent &&
|
|
487
|
+
typeof response.structuredContent.sourceRoot === "string"
|
|
488
|
+
? response.structuredContent.sourceRoot
|
|
489
|
+
: undefined,
|
|
490
|
+
operation: async () => {
|
|
491
|
+
if (workspace.mode === "worktree") {
|
|
492
|
+
if (!commitMessage) {
|
|
493
|
+
throw new Error(`Managed-worktree-backed Workspace ${workspace.id} requires commitMessage when ${action === "delete" ? "deleting active work" : "closing"}.`);
|
|
494
|
+
}
|
|
495
|
+
const physicalWorkspaceIds = workspaces.workspaceIdsForPhysicalWorkspace(workspace);
|
|
496
|
+
const busyWorkspaceIds = physicalWorkspaceIds
|
|
497
|
+
.filter((id) => processSessions.activeWorkspaceIds().has(id));
|
|
498
|
+
if (busyWorkspaceIds.length > 0) {
|
|
499
|
+
throw new Error(`Cannot close this worktree-backed Workspace while Workspace processes are still running: ${busyWorkspaceIds.join(", ")}.`);
|
|
500
|
+
}
|
|
501
|
+
const startedAt = performance.now();
|
|
502
|
+
const retirement = await codeIntelligence.retireWorkspaceRoot(workspace.root);
|
|
503
|
+
let closed;
|
|
504
|
+
try {
|
|
505
|
+
closed = await workspaces.closeWorktree(workspace.id, commitMessage);
|
|
506
|
+
}
|
|
507
|
+
finally {
|
|
508
|
+
codeIntelligence.restoreWorkspaceRoot(retirement.root);
|
|
509
|
+
}
|
|
510
|
+
await Promise.all(physicalWorkspaceIds.map((id) => reviewCheckpoints.releaseWorkspace(id)));
|
|
511
|
+
if (action === "delete") {
|
|
512
|
+
workspaces.deleteWorkspace(workspace.id);
|
|
513
|
+
workspaceTasks.deleteWorkspace(workspace.id);
|
|
514
|
+
taskReminders.forget(workspace.id);
|
|
515
|
+
activityQueries.deleteWorkspaceHistory(config.stateDir, workspace.id);
|
|
516
|
+
}
|
|
517
|
+
const result = [
|
|
518
|
+
action === "delete"
|
|
519
|
+
? `Safely finalized and deleted managed-worktree Workspace ${workspace.id}.`
|
|
520
|
+
: `Closed managed-worktree-backed Workspace ${workspace.id}; its identity was preserved for later reopen.`,
|
|
521
|
+
`Merged ${closed.branch} into ${closed.targetBranch} by fast-forward.`,
|
|
522
|
+
`Source checkout: ${closed.sourceRoot}`,
|
|
523
|
+
`Commit: ${closed.commitSha}`,
|
|
524
|
+
closed.cleanupWarning
|
|
525
|
+
? `Cleanup warning: ${closed.cleanupWarning}`
|
|
526
|
+
: "The managed worktree directory and branch were removed.",
|
|
527
|
+
].join("\n");
|
|
528
|
+
logToolCall(config, {
|
|
529
|
+
tool: toolNames.closeWorkspace,
|
|
530
|
+
...workspaceLogContext(workspace, extra.sessionId),
|
|
531
|
+
path: closed.sourceRoot,
|
|
532
|
+
success: true,
|
|
533
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
534
|
+
});
|
|
535
|
+
return attachHookReports({
|
|
536
|
+
content: [textBlock(result)],
|
|
537
|
+
_meta: {
|
|
538
|
+
tool: toolNames.closeWorkspace,
|
|
539
|
+
card: {
|
|
540
|
+
workspaceId: workspace.id,
|
|
541
|
+
action,
|
|
542
|
+
mode: "worktree",
|
|
543
|
+
sourceRoot: closed.sourceRoot,
|
|
544
|
+
branch: closed.branch,
|
|
545
|
+
targetBranch: closed.targetBranch,
|
|
546
|
+
commitSha: closed.commitSha,
|
|
547
|
+
mergedSha: closed.mergedSha,
|
|
548
|
+
committed: closed.committed,
|
|
549
|
+
cleanupWarning: closed.cleanupWarning,
|
|
550
|
+
payload: { content: [textBlock(result)] },
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
structuredContent: {
|
|
554
|
+
result,
|
|
555
|
+
workspaceId: workspace.id,
|
|
556
|
+
action,
|
|
557
|
+
mode: "worktree",
|
|
558
|
+
sourceRoot: closed.sourceRoot,
|
|
559
|
+
branch: closed.branch,
|
|
560
|
+
targetBranch: closed.targetBranch,
|
|
561
|
+
commitSha: closed.commitSha,
|
|
562
|
+
mergedSha: closed.mergedSha,
|
|
563
|
+
committed: closed.committed,
|
|
564
|
+
cleanupWarning: closed.cleanupWarning,
|
|
565
|
+
},
|
|
566
|
+
}, closed.hookReports);
|
|
567
|
+
}
|
|
568
|
+
if (commitMessage !== undefined) {
|
|
569
|
+
throw new Error("close_workspace commitMessage is only valid for managed-worktree-backed workspaces.");
|
|
570
|
+
}
|
|
571
|
+
const checkoutWorkspaceId = workspace.id;
|
|
572
|
+
if (processSessions.activeWorkspaceIds().has(checkoutWorkspaceId)) {
|
|
573
|
+
throw new Error(`Workspace ${checkoutWorkspaceId} still owns a running process. Poll, interrupt, or wait for it before closing this workspace.`);
|
|
574
|
+
}
|
|
575
|
+
workspaces.closeWorkspace(checkoutWorkspaceId);
|
|
576
|
+
await reviewCheckpoints.releaseWorkspace(checkoutWorkspaceId);
|
|
577
|
+
const result = `Closed checkout-backed Workspace ${checkoutWorkspaceId}; its ForgeRelay identity was preserved for later reopen. Physical project files were not removed.`;
|
|
578
|
+
return {
|
|
579
|
+
content: [textBlock(result)],
|
|
580
|
+
_meta: {
|
|
581
|
+
tool: toolNames.closeWorkspace,
|
|
582
|
+
card: {
|
|
583
|
+
workspaceId: checkoutWorkspaceId,
|
|
584
|
+
action: "close",
|
|
585
|
+
mode: "checkout",
|
|
586
|
+
payload: { content: [textBlock(result)] },
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
structuredContent: {
|
|
590
|
+
result,
|
|
591
|
+
workspaceId: checkoutWorkspaceId,
|
|
592
|
+
action: "close",
|
|
593
|
+
mode: "checkout",
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
workspacePanelStates.delete(workspace.id);
|
|
599
|
+
return response;
|
|
600
|
+
});
|
|
601
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { expandHomePath } from "
|
|
4
|
-
import { mergeHookConfigs, parseHookConfig } from "
|
|
3
|
+
import { expandHomePath } from "../../mcp/filesystem/roots.js";
|
|
4
|
+
import { mergeHookConfigs, parseHookConfig } from "../../mcp/hooks/hooks.js";
|
|
5
5
|
import { forgerelayAgentsDir, forgerelaySkillsDir, generateInstanceId, loadForgeRelayFiles, } from "./user-config.js";
|
|
6
6
|
const DEFAULT_OAUTH_ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
|
|
7
7
|
const DEFAULT_OAUTH_REFRESH_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
@@ -163,7 +163,7 @@ function parseOAuthConfig(env, ownerToken) {
|
|
|
163
163
|
ownerToken: parseRequiredSecret(productEnv(env, "OAUTH_OWNER_TOKEN") ?? ownerToken, "FORGERELAY_OAUTH_OWNER_TOKEN"),
|
|
164
164
|
accessTokenTtlSeconds: parsePositiveInteger(productEnv(env, "OAUTH_ACCESS_TOKEN_TTL_SECONDS"), DEFAULT_OAUTH_ACCESS_TOKEN_TTL_SECONDS, "FORGERELAY_OAUTH_ACCESS_TOKEN_TTL_SECONDS"),
|
|
165
165
|
refreshTokenTtlSeconds: parsePositiveInteger(productEnv(env, "OAUTH_REFRESH_TOKEN_TTL_SECONDS"), DEFAULT_OAUTH_REFRESH_TOKEN_TTL_SECONDS, "FORGERELAY_OAUTH_REFRESH_TOKEN_TTL_SECONDS"),
|
|
166
|
-
scopes: parseStringList(productEnv(env, "OAUTH_SCOPES"), ["
|
|
166
|
+
scopes: parseStringList(productEnv(env, "OAUTH_SCOPES"), ["forgerelay"]),
|
|
167
167
|
allowedRedirectHosts: parseStringList(productEnv(env, "OAUTH_ALLOWED_REDIRECT_HOSTS"), [
|
|
168
168
|
"chatgpt.com",
|
|
169
169
|
"localhost",
|