@akira-tl/forgerelay 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/capabilities/artifacts-review/GUIDE.md +2 -4
  3. package/dist/artifact-tools.js +0 -128
  4. package/dist/capabilities.js +2 -5
  5. package/dist/db/migrations.js +19 -0
  6. package/dist/db/schema.js +9 -0
  7. package/dist/hooks.js +12 -0
  8. package/dist/mcp/server-instructions.js +4 -9
  9. package/dist/review-checkpoints.js +4 -4
  10. package/dist/server.js +154 -280
  11. package/dist/ui/.vite/manifest.json +280 -280
  12. package/dist/ui/assets/{angular-html-CQmCUfkv.js → angular-html-BT2Z6jee.js} +1 -1
  13. package/dist/ui/assets/{angular-ts-BSbfxpMS.js → angular-ts-BwPE8YDt.js} +1 -1
  14. package/dist/ui/assets/{apl-CWCaH-E3.js → apl-CGbONVvU.js} +1 -1
  15. package/dist/ui/assets/{astro-R_p4G0xA.js → astro-DIAPy7cj.js} +1 -1
  16. package/dist/ui/assets/{blade-irHensDY.js → blade-Cgtd19Ky.js} +1 -1
  17. package/dist/ui/assets/{c-_XEQUoq6.js → c-acnvZ8tm.js} +1 -1
  18. package/dist/ui/assets/{cobol-Css6VQjJ.js → cobol-G-7HLVKW.js} +1 -1
  19. package/dist/ui/assets/{coffee-CpXUkyDm.js → coffee-DQFyKxEB.js} +1 -1
  20. package/dist/ui/assets/{cpp-BQHsPWwI.js → cpp-UyrmHXan.js} +1 -1
  21. package/dist/ui/assets/{crystal-Cbv8J9Fm.js → crystal-DuiWhEO3.js} +1 -1
  22. package/dist/ui/assets/{css-D8DkrS3U.js → css-D_WCbY6m.js} +1 -1
  23. package/dist/ui/assets/{edge-bBL_XDAG.js → edge-COkalhFi.js} +1 -1
  24. package/dist/ui/assets/{elixir-CQjsM9sA.js → elixir-cdlzbYR1.js} +1 -1
  25. package/dist/ui/assets/{elm-DLFl-jEZ.js → elm-ZwDYuYpb.js} +1 -1
  26. package/dist/ui/assets/{erb-BJjsdB_Q.js → erb-C8WG9TtC.js} +1 -1
  27. package/dist/ui/assets/{git-rebase-CqP5A2s0.js → git-rebase-CMlDVJBT.js} +1 -1
  28. package/dist/ui/assets/{glimmer-js-B5Hh_7xe.js → glimmer-js-DeLOQlol.js} +1 -1
  29. package/dist/ui/assets/{glimmer-ts-oaeSAC8s.js → glimmer-ts-DTEM04TK.js} +1 -1
  30. package/dist/ui/assets/{glsl-BylkOpcF.js → glsl-IJV4SB-M.js} +1 -1
  31. package/dist/ui/assets/{graphql-DsVRvP6S.js → graphql-PM-WDnoB.js} +1 -1
  32. package/dist/ui/assets/{hack-D4N5abT_.js → hack-C14sh6Lh.js} +1 -1
  33. package/dist/ui/assets/{haml-CIAJQIox.js → haml-B_5nS6Kx.js} +1 -1
  34. package/dist/ui/assets/{handlebars-DDEZyNXR.js → handlebars-eS-J0O-M.js} +1 -1
  35. package/dist/ui/assets/{heavy-payload-DafM8yjB.js → heavy-payload-BKB0QDOI.js} +1 -1
  36. package/dist/ui/assets/{html-zDy-3lvy.js → html-C3TPjrC5.js} +1 -1
  37. package/dist/ui/assets/{html-derivative-CZ91Y2d5.js → html-derivative-BlVZU2id.js} +1 -1
  38. package/dist/ui/assets/{http-ezaodZRf.js → http-BmbZzA5x.js} +1 -1
  39. package/dist/ui/assets/{hurl-BmL0o7TO.js → hurl-Dnd_YpOD.js} +1 -1
  40. package/dist/ui/assets/{java-Q2C0vwbH.js → java-ne9HdIVn.js} +1 -1
  41. package/dist/ui/assets/{javascript-UDhBkHE3.js → javascript-DpHT58rr.js} +1 -1
  42. package/dist/ui/assets/{jinja-BWRskcYS.js → jinja-DTvkZdHA.js} +1 -1
  43. package/dist/ui/assets/{jison-B18BY9Ds.js → jison-DUJG-tmh.js} +1 -1
  44. package/dist/ui/assets/{json-DNj7MpRr.js → json-DJmnBdxW.js} +1 -1
  45. package/dist/ui/assets/{jsx-CVpHJ6cr.js → jsx-BclJnZnN.js} +1 -1
  46. package/dist/ui/assets/{julia-CJBNBS9c.js → julia-P5B4pHFa.js} +1 -1
  47. package/dist/ui/assets/{just-Bmu2JCQT.js → just-DYaoh7C6.js} +1 -1
  48. package/dist/ui/assets/{latex-BB7F93KY.js → latex-CI5lnkWV.js} +1 -1
  49. package/dist/ui/assets/{liquid-Btnomc6E.js → liquid-D3LnAs_z.js} +1 -1
  50. package/dist/ui/assets/{lua-D0FYT9Vo.js → lua-dZC7ZQIY.js} +1 -1
  51. package/dist/ui/assets/{marko-MHLw0ZlK.js → marko-CRDrjqfS.js} +1 -1
  52. package/dist/ui/assets/{mdc-CSr-suGy.js → mdc-ZzwgmJVn.js} +1 -1
  53. package/dist/ui/assets/{nginx-ur1l6E-g.js → nginx-BAvFXGSV.js} +1 -1
  54. package/dist/ui/assets/{nim-B2k0IugA.js → nim-BHIFawe9.js} +1 -1
  55. package/dist/ui/assets/{perl-BqOHxFDn.js → perl-C_f5AnLa.js} +1 -1
  56. package/dist/ui/assets/{php-DzPK6O1_.js → php-DDqpegL5.js} +1 -1
  57. package/dist/ui/assets/{pug-D169OZkr.js → pug-CSaAZXyd.js} +1 -1
  58. package/dist/ui/assets/{qml-BtemJTC4.js → qml-CNHvRs_z.js} +1 -1
  59. package/dist/ui/assets/{r-BGVHu0bd.js → r-CAfoQND2.js} +1 -1
  60. package/dist/ui/assets/{razor-Diisjg1d.js → razor-BDfAPy7I.js} +1 -1
  61. package/dist/ui/assets/{regexp-B2F5vMZ6.js → regexp-Mug5jltQ.js} +1 -1
  62. package/dist/ui/assets/{review-payload-B-3I3Oj_.js → review-payload-D7GIdYDj.js} +1 -1
  63. package/dist/ui/assets/{rst-BxpeZwuT.js → rst-BzPP2gle.js} +1 -1
  64. package/dist/ui/assets/{ruby-DrCq4riN.js → ruby-CQjzokzX.js} +1 -1
  65. package/dist/ui/assets/{sas-CI_2W-1n.js → sas-Bp37KGr7.js} +1 -1
  66. package/dist/ui/assets/{scrollbar-DHsDF5w-.js → scrollbar-HUMBdLLK.js} +3 -3
  67. package/dist/ui/assets/{scss-BxtyHZ1p.js → scss-C_Z1DYnw.js} +1 -1
  68. package/dist/ui/assets/{shellscript-BY_YYrHT.js → shellscript-CLfJpn3I.js} +1 -1
  69. package/dist/ui/assets/{shellsession-C6uBrLmA.js → shellsession-CNc62UhY.js} +1 -1
  70. package/dist/ui/assets/{soy-BHM9gdPh.js → soy-CF44SpBo.js} +1 -1
  71. package/dist/ui/assets/{sql-XmhfEGz_.js → sql-CYnzfiQ1.js} +1 -1
  72. package/dist/ui/assets/{stata-BbHY7NEo.js → stata-DVeMA68W.js} +1 -1
  73. package/dist/ui/assets/{surrealql-CD49DZ3E.js → surrealql-52sF96Yv.js} +1 -1
  74. package/dist/ui/assets/{svelte-CbYsEYJY.js → svelte-ydwT7UbF.js} +1 -1
  75. package/dist/ui/assets/{templ-B5JoUxEB.js → templ-BXcoVoGh.js} +1 -1
  76. package/dist/ui/assets/{tex-DZcOfTZK.js → tex-COsYFp7O.js} +1 -1
  77. package/dist/ui/assets/{ts-tags-BeU-xqc0.js → ts-tags-CF--LE1s.js} +1 -1
  78. package/dist/ui/assets/{tsx-V7W1xGl3.js → tsx-DLBt2-2f.js} +1 -1
  79. package/dist/ui/assets/{twig-MIaJo5BU.js → twig-YoM-DiI5.js} +1 -1
  80. package/dist/ui/assets/{typescript-C0WESuVc.js → typescript-Ce9JGkNI.js} +1 -1
  81. package/dist/ui/assets/{vue-eJHwc9ra.js → vue-B5LDEy4l.js} +1 -1
  82. package/dist/ui/assets/{vue-html-CLmBujTU.js → vue-html-CPdGGk4T.js} +1 -1
  83. package/dist/ui/assets/{vue-vine-BP5kjIGb.js → vue-vine-B5bjgk0a.js} +1 -1
  84. package/dist/ui/assets/{workspace-app-C1qR5cAY.js → workspace-app-Cm9EX5x4.js} +3 -3
  85. package/dist/ui/assets/{xml-j7pmyP48.js → xml-Cun8xb0T.js} +1 -1
  86. package/dist/ui/assets/{xsl-5VFHbg6o.js → xsl-wls_CEK_.js} +1 -1
  87. package/dist/ui/assets/{yaml-DxFAt-KU.js → yaml-BxuXn9OL.js} +1 -1
  88. package/dist/ui/workspace-app.html +1 -1
  89. package/dist/workspace-store.js +52 -1
  90. package/dist/workspaces.js +209 -29
  91. package/docs/artifact-exchange.md +4 -3
  92. package/docs/chatgpt-coding-workflow.md +70 -11
  93. package/docs/configuration.md +41 -16
  94. package/docs/debugging.md +2 -2
  95. package/docs/gotchas.md +3 -3
  96. package/docs/roadmap.md +12 -1
  97. package/docs/security.md +3 -2
  98. package/package.json +1 -1
  99. package/scripts/debug/accept.mjs +113 -15
package/dist/server.js CHANGED
@@ -17,7 +17,7 @@ import { applyPatch } from "./apply-patch.js";
17
17
  import { buildCapabilityFingerprint } from "./capabilities.js";
18
18
  import { CapabilityError, createCapabilityRegistry, } from "./capability-registry.js";
19
19
  import { deletePath, renamePath } from "./file-mutations.js";
20
- import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform, registerArtifactTools, } from "./artifact-tools.js";
20
+ import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform, } from "./artifact-tools.js";
21
21
  import { ArtifactError } from "./artifact-error.js";
22
22
  import { loadConfig } from "./config.js";
23
23
  import { attachHookReports, HookRunner, runToolWithHooks } from "./hooks.js";
@@ -25,7 +25,7 @@ import { checkHookConfiguration } from "./hook-cli.js";
25
25
  import { buildServerInstructions, buildShellMutationPolicy, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
26
26
  import { createOpenAIIncomingArtifactAdapter, IncomingArtifactAdapterRegistry, } from "./incoming-artifacts.js";
27
27
  import { logEvent, requestIp, requestPath, commandPreview, transportSessionIdPrefix, workspaceLogLabel, } from "./logger.js";
28
- import { editFileTool, findFilesTool, grepFilesTool, listDirectoryTool, readFileTool, writeFileTool, } from "./pi-tools.js";
28
+ import { editFileTool, readFileTool, writeFileTool, } from "./pi-tools.js";
29
29
  import { SingleUserOAuthProvider } from "./oauth-provider.js";
30
30
  import { McpTransportRegistry, } from "./mcp-sessions.js";
31
31
  import { ProcessManager, resolveProcessId, } from "./process-sessions.js";
@@ -67,7 +67,7 @@ function shouldAttachWidget(mode, kind) {
67
67
  case "off":
68
68
  return false;
69
69
  case "changes":
70
- return kind === "workspace" || kind === "show_changes" || kind === "capability";
70
+ return kind === "workspace" || kind === "capability";
71
71
  case "full":
72
72
  return true;
73
73
  }
@@ -164,6 +164,36 @@ const workspaceLocalAgentProviderOutputSchema = z.object({
164
164
  const workspaceAvailableAgentsFileOutputSchema = z.object({
165
165
  path: z.string(),
166
166
  });
167
+ const workspaceInventoryEntryOutputSchema = z.object({
168
+ label: z.string(),
169
+ workspaceId: z.string(),
170
+ root: z.string(),
171
+ status: z.string(),
172
+ state: z.enum(["active", "stale", "invalid", "closed"]),
173
+ mode: z.enum(["checkout", "worktree"]),
174
+ sourceRoot: z.string().optional(),
175
+ branch: z.string().optional(),
176
+ targetBranch: z.string().optional(),
177
+ managed: z.boolean(),
178
+ createdAt: z.string(),
179
+ lastUsedAt: z.string(),
180
+ idleMs: z.number().nonnegative(),
181
+ rootValid: z.boolean(),
182
+ current: z.boolean(),
183
+ });
184
+ const workspaceInventorySummaryOutputSchema = z.object({
185
+ total: z.number().int().nonnegative(),
186
+ matching: z.number().int().nonnegative(),
187
+ active: z.number().int().nonnegative(),
188
+ stale: z.number().int().nonnegative(),
189
+ invalid: z.number().int().nonnegative(),
190
+ closed: z.number().int().nonnegative(),
191
+ });
192
+ const workspaceInventoryPageOutputSchema = z.object({
193
+ offset: z.number().int().nonnegative(),
194
+ limit: z.number().int().positive(),
195
+ hasMore: z.boolean(),
196
+ });
167
197
  const reviewFileOutputSchema = z.object({
168
198
  path: z.string(),
169
199
  previousPath: z.string().optional(),
@@ -711,7 +741,6 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
711
741
  files: review.files,
712
742
  },
713
743
  card: {
714
- tool: "show_changes",
715
744
  summary: review.summary,
716
745
  files: review.files,
717
746
  payload: { patch: review.patch },
@@ -776,20 +805,24 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
776
805
  }, async (uri, _variables, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
777
806
  registerAppTool(server, "open_workspace", {
778
807
  title: "Open workspace",
779
- description: "Open or resume a local coding workspace. Reuse the returned workspaceId for later calls. Default to checkout; use mode=\"worktree\" only when the user explicitly requests isolated or parallel Git work. Every call returns a capability fingerprint; bootstrap calls also expose project context, skills, and capability guides when needed.",
808
+ description: "Open or resume a local coding workspace. Reuse the returned workspaceId for later calls. Default to checkout; use mode=\"worktree\" only when the user explicitly requests isolated or parallel Git work. Every call returns lightweight workspace metadata; bootstrap context is delivered automatically only when needed and can be explicitly suppressed or refreshed.",
780
809
  inputSchema: {
810
+ action: z
811
+ .enum(["open", "list"])
812
+ .optional()
813
+ .describe("Defaults to open. Use list only when you need to inspect or choose logical workspaces before resuming or cleaning them up."),
781
814
  path: z
782
815
  .string()
783
816
  .optional()
784
- .describe("Project path to open. Required unless workspaceId is supplied. With mode=\"worktree\", this may also be a managed worktree path previously returned by ForgeRelay."),
817
+ .describe("Project path to open. Required for action=open unless workspaceId is supplied. With mode=\"worktree\", this may also be a managed worktree path previously returned by ForgeRelay."),
785
818
  workspaceId: z
786
819
  .string()
787
820
  .optional()
788
- .describe("Existing logical workspace ID to resume in this conversation. When supplied, ForgeRelay resumes that workspace rather than allocating another ID."),
821
+ .describe("For action=open, an existing logical workspace ID to resume in this conversation. For action=list, filters inventory to one workspace ID."),
789
822
  mode: z
790
823
  .enum(["checkout", "worktree"])
791
824
  .optional()
792
- .describe("Defaults to checkout, which works in the actual directory. Use worktree only when the user explicitly requests isolated or parallel Git work."),
825
+ .describe("For action=open, defaults to checkout and uses the actual directory unless worktree isolation is explicitly requested. For action=list, filters by workspace mode."),
793
826
  baseRef: z
794
827
  .string()
795
828
  .optional()
@@ -802,11 +835,45 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
802
835
  .boolean()
803
836
  .optional()
804
837
  .describe("When true, allocate a fresh logical workspaceId for the same physical checkout or worktree and bind this conversation to it. Use only after the user explicitly requests a new logical workspace."),
838
+ context: z
839
+ .enum(["auto", "full", "none"])
840
+ .optional()
841
+ .describe("Bootstrap context policy for action=open. auto (default) sends full project context only when this conversation has not received the current context fingerprint; full forces a refresh; none opens/resumes without returning the full bootstrap context."),
842
+ root: z
843
+ .string()
844
+ .optional()
845
+ .describe("For action=list, filter by canonical workspace root or source root."),
846
+ status: z
847
+ .string()
848
+ .optional()
849
+ .describe("For action=list, filter by persisted workspace status such as active or closed."),
850
+ state: z
851
+ .enum(["active", "stale", "invalid", "closed"])
852
+ .optional()
853
+ .describe("For action=list, filter by derived lifecycle state."),
854
+ staleOnly: z
855
+ .boolean()
856
+ .optional()
857
+ .describe("For action=list, return only active logical workspaces idle for more than two days."),
858
+ offset: z
859
+ .number()
860
+ .int()
861
+ .nonnegative()
862
+ .optional()
863
+ .describe("For action=list, zero-based inventory offset. Defaults to 0."),
864
+ limit: z
865
+ .number()
866
+ .int()
867
+ .min(1)
868
+ .max(100)
869
+ .optional()
870
+ .describe("For action=list, maximum records to return. Defaults to 50; maximum 100."),
805
871
  },
806
872
  outputSchema: {
807
- workspaceId: z.string(),
808
- root: z.string(),
809
- mode: z.enum(["checkout", "worktree"]),
873
+ action: z.enum(["open", "list"]),
874
+ workspaceId: z.string().optional(),
875
+ root: z.string().optional(),
876
+ mode: z.enum(["checkout", "worktree"]).optional(),
810
877
  sourceRoot: z.string().optional(),
811
878
  worktree: z
812
879
  .object({
@@ -829,7 +896,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
829
896
  targetBranch: z.string().optional(),
830
897
  managed: z.boolean(),
831
898
  current: z.boolean(),
832
- })),
899
+ })).optional(),
833
900
  staleWorkspaces: z.array(z.object({
834
901
  workspaceId: z.string(),
835
902
  root: z.string(),
@@ -839,9 +906,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
839
906
  branch: z.string().optional(),
840
907
  targetBranch: z.string().optional(),
841
908
  managed: z.boolean(),
842
- })),
843
- capabilityFingerprint: capabilityFingerprintOutputSchema,
844
- capabilityCatalog: z.array(capabilityCatalogOutputSchema),
909
+ })).optional(),
910
+ capabilityFingerprint: capabilityFingerprintOutputSchema.optional(),
911
+ contextFingerprint: z.string().optional(),
912
+ capabilityCatalog: z.array(capabilityCatalogOutputSchema).optional(),
845
913
  capabilityGuides: z.array(capabilityGuideOutputSchema).optional(),
846
914
  agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
847
915
  availableAgentsFiles: z.array(workspaceAvailableAgentsFileOutputSchema).optional(),
@@ -849,6 +917,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
849
917
  agentProviders: z.array(workspaceLocalAgentProviderOutputSchema).optional(),
850
918
  agents: z.array(workspaceLocalAgentOutputSchema).optional(),
851
919
  skillDiagnostics: z.array(z.unknown()).optional(),
920
+ workspaces: z.array(workspaceInventoryEntryOutputSchema).optional(),
921
+ summary: workspaceInventorySummaryOutputSchema.optional(),
922
+ page: workspaceInventoryPageOutputSchema.optional(),
852
923
  instruction: z.string(),
853
924
  },
854
925
  ...toolWidgetDescriptorMeta(config, "workspace"),
@@ -858,11 +929,62 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
858
929
  idempotentHint: false,
859
930
  openWorldHint: false,
860
931
  },
861
- }, async ({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace }, { _meta, sessionId }) => {
932
+ }, async ({ action = "open", path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context, root, status, state, staleOnly, offset, limit, }, { _meta, sessionId }) => {
862
933
  const startedAt = performance.now();
863
- const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, includeBootstrapContext, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace }, {
864
- conversationScopeId: openAiConversationScopeId(_meta),
865
- protectedWorkspaceIds: processSessions.activeWorkspaceIds(),
934
+ const conversationScopeId = openAiConversationScopeId(_meta);
935
+ const protectedWorkspaceIds = processSessions.activeWorkspaceIds();
936
+ if (action === "list") {
937
+ if (path !== undefined || baseRef !== undefined || newWorktree !== undefined ||
938
+ newWorkspace !== undefined || context !== undefined) {
939
+ throw new Error("open_workspace action=list does not accept path, baseRef, newWorktree, newWorkspace, or context. Use root/workspaceId/mode/status/state/staleOnly for inventory filters.");
940
+ }
941
+ const inventory = await workspaces.listWorkspaces({ workspaceId, mode, root, status, state, staleOnly, offset, limit }, { conversationScopeId, protectedWorkspaceIds });
942
+ const nextOffset = inventory.page.offset + inventory.page.limit;
943
+ const instruction = [
944
+ "Resume a selected workspaceId with open_workspace(action=\"open\", workspaceId=...).",
945
+ "Use close_workspace only after the user chooses cleanup; never close inventory entries automatically.",
946
+ inventory.page.hasMore
947
+ ? `More matching workspaces are available; continue with offset=${nextOffset}.`
948
+ : undefined,
949
+ ].filter(Boolean).join(" ");
950
+ const result = [
951
+ `Logical workspace inventory: ${inventory.summary.matching} matching of ${inventory.summary.total} stored records.`,
952
+ `States: active=${inventory.summary.active}, stale=${inventory.summary.stale}, invalid=${inventory.summary.invalid}, closed=${inventory.summary.closed}.`,
953
+ ...inventory.workspaces.map((entry) => [
954
+ entry.label,
955
+ `state=${entry.state}`,
956
+ `status=${entry.status}`,
957
+ `mode=${entry.mode}`,
958
+ entry.managed ? "managed" : undefined,
959
+ entry.current ? "current" : undefined,
960
+ `root=${entry.root}`,
961
+ `last-used=${entry.lastUsedAt}`,
962
+ ].filter(Boolean).join(" ")),
963
+ instruction,
964
+ ].join("\n");
965
+ logToolCall(config, {
966
+ tool: "open_workspace",
967
+ action: "list",
968
+ path: root,
969
+ success: true,
970
+ durationMs: Math.round(performance.now() - startedAt),
971
+ });
972
+ return {
973
+ content: [textBlock(result)],
974
+ structuredContent: {
975
+ action: "list",
976
+ ...inventory,
977
+ instruction,
978
+ },
979
+ };
980
+ }
981
+ if (root !== undefined || status !== undefined || state !== undefined ||
982
+ staleOnly !== undefined || offset !== undefined || limit !== undefined) {
983
+ throw new Error("open_workspace inventory filters root, status, state, staleOnly, offset, and limit are only valid with action=list.");
984
+ }
985
+ const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, includeBootstrapContext, contextFingerprint, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context }, {
986
+ conversationScopeId,
987
+ protectedWorkspaceIds,
866
988
  });
867
989
  const knownWorktrees = await workspaces.listKnownWorktrees(workspace);
868
990
  const staleWorkspaces = await workspaces.listStaleWorkspaces(workspace);
@@ -912,20 +1034,26 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
912
1034
  const visibleAgents = includeBootstrapContext ? cardAgents : [];
913
1035
  const loadedAgentsFiles = includeBootstrapContext ? cardAgentsFiles : [];
914
1036
  const availableAgentsFileOutputs = includeBootstrapContext ? cardAvailableAgentsFiles : [];
1037
+ const workspaceContextInstruction = "For later open_workspace calls, context=\"auto\" avoids repeating unchanged bootstrap context; use context=\"none\" when only the workspace handle/metadata is needed, or context=\"full\" to force a refresh.";
1038
+ const workspaceManagementInstruction = "When you need to continue an earlier logical workspace or organize workspace state, use open_workspace(action=\"list\") to inspect candidates, then resume a selected workspaceId or ask the user before close_workspace cleanup.";
915
1039
  const cardInstruction = config.skillsEnabled
916
- ? "Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches an available skill or capability guide, read its advertised path before proceeding."
917
- : "Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches a capability guide, read its advertised path before proceeding.";
1040
+ ? `Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches an available skill or capability guide, read its advertised path before proceeding. ${workspaceContextInstruction} ${workspaceManagementInstruction}`
1041
+ : `Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches a capability guide, read its advertised path before proceeding. ${workspaceContextInstruction} ${workspaceManagementInstruction}`;
918
1042
  const instruction = workspaceReused
919
1043
  ? includeBootstrapContext
920
1044
  ? [
921
1045
  `Workspace already exists as ${workspace.id} for this directory.`,
922
1046
  "Reuse this workspaceId for subsequent tool calls.",
923
1047
  "The complete project context is included because it has not yet been provided in this conversation or host context.",
1048
+ workspaceContextInstruction,
1049
+ workspaceManagementInstruction,
924
1050
  ].join("\n\n")
925
1051
  : [
926
1052
  `Workspace already open as ${workspace.id}.`,
927
1053
  "Reuse this workspaceId for subsequent tool calls. This is the same directory previously opened in this conversation.",
928
1054
  "Continue following the project instructions, nested instruction files, skills, capability guides, agent profiles, and diagnostics previously provided for this workspace. They remain active and are not repeated here.",
1055
+ workspaceContextInstruction,
1056
+ workspaceManagementInstruction,
929
1057
  ].join("\n\n")
930
1058
  : workspace.mode === "worktree"
931
1059
  ? "Use this workspaceId for subsequent tool calls. Follow the project instructions, nested instruction files, skills, agent profiles, and diagnostics returned for this isolated worktree."
@@ -999,6 +1127,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
999
1127
  worktrees: knownWorktrees,
1000
1128
  staleWorkspaces,
1001
1129
  capabilityFingerprint,
1130
+ contextFingerprint,
1002
1131
  capabilityCatalog,
1003
1132
  agentsFiles: cardAgentsFiles,
1004
1133
  availableAgentsFiles: cardAvailableAgentsFiles,
@@ -1018,6 +1147,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1018
1147
  },
1019
1148
  },
1020
1149
  structuredContent: {
1150
+ action: "open",
1021
1151
  workspaceId: workspace.id,
1022
1152
  root: workspace.root,
1023
1153
  mode: workspace.mode,
@@ -1026,6 +1156,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1026
1156
  worktrees: knownWorktrees,
1027
1157
  staleWorkspaces,
1028
1158
  capabilityFingerprint,
1159
+ contextFingerprint,
1029
1160
  capabilityCatalog,
1030
1161
  ...(includeBootstrapContext
1031
1162
  ? {
@@ -1120,9 +1251,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1120
1251
  ...(execution.card
1121
1252
  ? {
1122
1253
  _meta: {
1123
- tool: execution.card.tool ?? toolNames.capability,
1254
+ tool: toolNames.capability,
1124
1255
  card: {
1125
1256
  workspaceId,
1257
+ capabilityName: name,
1126
1258
  summary: execution.card.summary ?? {},
1127
1259
  files: execution.card.files,
1128
1260
  payload: execution.card.payload ?? {},
@@ -1716,255 +1848,6 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1716
1848
  });
1717
1849
  });
1718
1850
  }
1719
- if (config.widgets === "changes") {
1720
- registerAppTool(server, "show_changes", {
1721
- title: "Show changes",
1722
- description: "Show the changes made in this turn for an open workspace. Call this once after the final related file change and before your final response so the user can review the combined diff. Do not call it after each individual file change.",
1723
- inputSchema: {
1724
- workspaceId: z
1725
- .string()
1726
- .describe("Workspace identifier returned by open_workspace."),
1727
- },
1728
- outputSchema: resultOutputSchema(),
1729
- ...toolWidgetDescriptorMeta(config, "show_changes"),
1730
- annotations: { readOnlyHint: true },
1731
- }, async ({ workspaceId }, extra) => {
1732
- const workspace = workspaces.getWorkspace(workspaceId);
1733
- return runToolWithHooks(hooks, {
1734
- tool: "show_changes",
1735
- invocation: workspaceHookInvocation(workspace),
1736
- operation: async () => {
1737
- const startedAt = performance.now();
1738
- const review = await reviewWorkspaceChanges(reviewCheckpoints, workspace);
1739
- const content = [textBlock(review.result)];
1740
- logToolCall(config, {
1741
- tool: "show_changes",
1742
- ...workspaceLogContext(workspace, extra.sessionId),
1743
- success: true,
1744
- durationMs: Math.round(performance.now() - startedAt),
1745
- });
1746
- return {
1747
- content,
1748
- _meta: {
1749
- tool: "show_changes",
1750
- card: {
1751
- workspaceId,
1752
- summary: review.summary,
1753
- files: review.files,
1754
- payload: {
1755
- patch: review.patch,
1756
- },
1757
- },
1758
- },
1759
- structuredContent: {
1760
- result: contentText(content),
1761
- },
1762
- };
1763
- },
1764
- });
1765
- });
1766
- }
1767
- if (config.toolMode === "full") {
1768
- registerAppTool(server, toolNames.grep, {
1769
- title: "Grep",
1770
- description: "Search file contents inside an open workspace or the OS temp directory. Use this before broad reads when looking for symbols, text, or usage sites. Respects project ignore rules. Call open_workspace first and pass workspaceId.",
1771
- inputSchema: {
1772
- workspaceId: z
1773
- .string()
1774
- .describe("Workspace identifier returned by open_workspace."),
1775
- pattern: z.string().describe("Search pattern."),
1776
- path: z
1777
- .string()
1778
- .optional()
1779
- .describe("Optional path or glob scope relative to the workspace root, or an absolute path inside the OS temp directory."),
1780
- include: z.string().optional().describe("Optional include glob."),
1781
- },
1782
- outputSchema: resultOutputSchema(),
1783
- ...toolWidgetDescriptorMeta(config, "search"),
1784
- annotations: { readOnlyHint: true },
1785
- }, async ({ workspaceId, ...input }, extra) => {
1786
- const workspace = workspaces.getWorkspace(workspaceId);
1787
- return runToolWithHooks(hooks, {
1788
- tool: toolNames.grep,
1789
- invocation: workspaceHookInvocation(workspace),
1790
- payload: { pattern: input.pattern, path: input.path, include: input.include },
1791
- isFailure: toolResultIsError,
1792
- operation: async () => {
1793
- const startedAt = performance.now();
1794
- const response = await grepFilesTool(input, {
1795
- cwd: workspace.root,
1796
- root: workspace.root,
1797
- fileRoots: workspaces.fileToolRoots(workspace),
1798
- });
1799
- if (response.isError) {
1800
- logFailedToolResponse(config, {
1801
- tool: toolNames.grep,
1802
- ...workspaceLogContext(workspace, extra.sessionId),
1803
- path: input.path,
1804
- }, response.content, startedAt);
1805
- return response;
1806
- }
1807
- const summary = {
1808
- pattern: input.pattern,
1809
- scope: input.path ?? ".",
1810
- ...textSummary(response.content),
1811
- };
1812
- logToolCall(config, {
1813
- tool: toolNames.grep,
1814
- ...workspaceLogContext(workspace, extra.sessionId),
1815
- path: input.path,
1816
- success: true,
1817
- durationMs: Math.round(performance.now() - startedAt),
1818
- });
1819
- return {
1820
- ...response,
1821
- _meta: {
1822
- tool: toolNames.grep,
1823
- card: {
1824
- workspaceId,
1825
- path: input.path,
1826
- summary,
1827
- payload: { content: response.content },
1828
- },
1829
- },
1830
- structuredContent: {
1831
- result: contentText(response.content),
1832
- },
1833
- };
1834
- },
1835
- });
1836
- });
1837
- registerAppTool(server, toolNames.glob, {
1838
- title: "Glob",
1839
- description: "Find files by glob pattern inside an open workspace or the OS temp directory. Use this to discover filenames or narrow file sets before reading. Respects project ignore rules. Call open_workspace first and pass workspaceId.",
1840
- inputSchema: {
1841
- workspaceId: z
1842
- .string()
1843
- .describe("Workspace identifier returned by open_workspace."),
1844
- pattern: z.string().describe("File glob pattern."),
1845
- path: z
1846
- .string()
1847
- .optional()
1848
- .describe("Optional path scope relative to the workspace root, or an absolute path inside the OS temp directory."),
1849
- },
1850
- outputSchema: resultOutputSchema(),
1851
- ...toolWidgetDescriptorMeta(config, "search"),
1852
- annotations: { readOnlyHint: true },
1853
- }, async ({ workspaceId, ...input }, extra) => {
1854
- const workspace = workspaces.getWorkspace(workspaceId);
1855
- return runToolWithHooks(hooks, {
1856
- tool: toolNames.glob,
1857
- invocation: workspaceHookInvocation(workspace),
1858
- payload: { pattern: input.pattern, path: input.path },
1859
- isFailure: toolResultIsError,
1860
- operation: async () => {
1861
- const startedAt = performance.now();
1862
- const response = await findFilesTool(input, {
1863
- cwd: workspace.root,
1864
- root: workspace.root,
1865
- fileRoots: workspaces.fileToolRoots(workspace),
1866
- });
1867
- if (response.isError) {
1868
- logFailedToolResponse(config, {
1869
- tool: toolNames.glob,
1870
- ...workspaceLogContext(workspace, extra.sessionId),
1871
- path: input.path,
1872
- }, response.content, startedAt);
1873
- return response;
1874
- }
1875
- const summary = {
1876
- pattern: input.pattern,
1877
- scope: input.path ?? ".",
1878
- ...textSummary(response.content),
1879
- };
1880
- logToolCall(config, {
1881
- tool: toolNames.glob,
1882
- ...workspaceLogContext(workspace, extra.sessionId),
1883
- path: input.path,
1884
- success: true,
1885
- durationMs: Math.round(performance.now() - startedAt),
1886
- });
1887
- return {
1888
- ...response,
1889
- _meta: {
1890
- tool: toolNames.glob,
1891
- card: {
1892
- workspaceId,
1893
- path: input.path,
1894
- summary,
1895
- payload: { content: response.content },
1896
- },
1897
- },
1898
- structuredContent: {
1899
- result: contentText(response.content),
1900
- },
1901
- };
1902
- },
1903
- });
1904
- });
1905
- registerAppTool(server, toolNames.ls, {
1906
- title: "Ls",
1907
- description: "List a directory inside an open workspace or the OS temp directory. Use this for directory inspection before reading files. Call open_workspace first and pass workspaceId.",
1908
- inputSchema: {
1909
- workspaceId: z
1910
- .string()
1911
- .describe("Workspace identifier returned by open_workspace."),
1912
- path: z
1913
- .string()
1914
- .describe("Directory path to list, relative to the workspace root or absolute inside the OS temp directory."),
1915
- },
1916
- outputSchema: resultOutputSchema(),
1917
- ...toolWidgetDescriptorMeta(config, "directory"),
1918
- annotations: { readOnlyHint: true },
1919
- }, async ({ workspaceId, ...input }, extra) => {
1920
- const workspace = workspaces.getWorkspace(workspaceId);
1921
- return runToolWithHooks(hooks, {
1922
- tool: toolNames.ls,
1923
- invocation: workspaceHookInvocation(workspace),
1924
- payload: { path: input.path },
1925
- isFailure: toolResultIsError,
1926
- operation: async () => {
1927
- const startedAt = performance.now();
1928
- const response = await listDirectoryTool(input, {
1929
- cwd: workspace.root,
1930
- root: workspace.root,
1931
- fileRoots: workspaces.fileToolRoots(workspace),
1932
- });
1933
- if (response.isError) {
1934
- logFailedToolResponse(config, {
1935
- tool: toolNames.ls,
1936
- ...workspaceLogContext(workspace, extra.sessionId),
1937
- path: input.path,
1938
- }, response.content, startedAt);
1939
- return response;
1940
- }
1941
- const summary = textSummary(response.content);
1942
- logToolCall(config, {
1943
- tool: toolNames.ls,
1944
- ...workspaceLogContext(workspace, extra.sessionId),
1945
- path: input.path,
1946
- success: true,
1947
- durationMs: Math.round(performance.now() - startedAt),
1948
- });
1949
- return {
1950
- ...response,
1951
- _meta: {
1952
- tool: toolNames.ls,
1953
- card: {
1954
- workspaceId,
1955
- path: input.path,
1956
- summary,
1957
- payload: { content: response.content },
1958
- },
1959
- },
1960
- structuredContent: {
1961
- result: contentText(response.content),
1962
- },
1963
- };
1964
- },
1965
- });
1966
- });
1967
- }
1968
1851
  if (config.toolMode !== "codex") {
1969
1852
  registerAppTool(server, toolNames.shell, {
1970
1853
  title: "Bash",
@@ -2146,15 +2029,6 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2146
2029
  });
2147
2030
  }
2148
2031
  registerProcessTools(server, config, workspaces, processSessions, hooks);
2149
- if (config.artifactsEnabled && isArtifactDownloadSupportedPlatform()) {
2150
- registerArtifactTools(server, {
2151
- config,
2152
- workspaces,
2153
- hooks,
2154
- incomingArtifactAdapters,
2155
- incomingArtifactRegistry,
2156
- });
2157
- }
2158
2032
  return server;
2159
2033
  }
2160
2034
  export function createServer(config = loadConfig(), options = {}) {