@akira-tl/forgerelay 1.2.2 → 1.2.4

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 (108) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/capabilities/workspace/managed-worktrees/GUIDE.md +4 -2
  3. package/dist/mcp/server/core/schemas.js +6 -0
  4. package/dist/mcp/server/transport/http-server.js +24 -16
  5. package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +4 -2
  6. package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +6 -1
  7. package/dist/mcp/server/workspace/runtime/workspace-open.js +19 -13
  8. package/dist/runtime/logging/logger.js +19 -19
  9. package/dist/ui/.vite/manifest.json +276 -276
  10. package/dist/ui/activity-panel-app.html +2 -2
  11. package/dist/ui/assets/activity-panel-app-Dq2xLIrs.css +1 -0
  12. package/dist/ui/assets/{activity-panel-app-BeDiwd7D.js → activity-panel-app-hkM8rX3z.js} +1 -1
  13. package/dist/ui/assets/{angular-html-DuoCE7nn.js → angular-html-UD0GEFFl.js} +1 -1
  14. package/dist/ui/assets/{angular-ts-D5tGk0hL.js → angular-ts-B2awh-Q1.js} +1 -1
  15. package/dist/ui/assets/{apl-lciOTPbw.js → apl-CpbhnJc8.js} +1 -1
  16. package/dist/ui/assets/{astro-vJbqfNnJ.js → astro-CkVVuF5B.js} +1 -1
  17. package/dist/ui/assets/{blade-C7655Q4K.js → blade-czG58aop.js} +1 -1
  18. package/dist/ui/assets/{c-DdHp1Jrw.js → c-DSq6h06b.js} +1 -1
  19. package/dist/ui/assets/{cobol-DgvzQxZP.js → cobol-CYnmhT2E.js} +1 -1
  20. package/dist/ui/assets/{coffee-BIjVUOUd.js → coffee-ClLhNqd5.js} +1 -1
  21. package/dist/ui/assets/{cpp-CU8tGzvI.js → cpp-BZDC6U43.js} +1 -1
  22. package/dist/ui/assets/{crystal-CUQAwsg9.js → crystal-BN9jCJyU.js} +1 -1
  23. package/dist/ui/assets/{css-DtMTqD3F.js → css-8dMfiihi.js} +1 -1
  24. package/dist/ui/assets/{edge-DoCX9RnL.js → edge-DemPBRNz.js} +1 -1
  25. package/dist/ui/assets/{elixir-DJxV9_U6.js → elixir-bLdHW8dz.js} +1 -1
  26. package/dist/ui/assets/{elm-DIgWqK4r.js → elm-DlZPcXmu.js} +1 -1
  27. package/dist/ui/assets/{erb-BHxP0c_e.js → erb-wmTqbHwm.js} +1 -1
  28. package/dist/ui/assets/{git-rebase-HbI_Vo8d.js → git-rebase-BqrZJVYO.js} +1 -1
  29. package/dist/ui/assets/{glimmer-js-CthY4Ty_.js → glimmer-js-CkPzdNHI.js} +1 -1
  30. package/dist/ui/assets/{glimmer-ts-B2-_A1Xz.js → glimmer-ts-Bd0vTdJC.js} +1 -1
  31. package/dist/ui/assets/{glsl-D9V9gq98.js → glsl-DKK0Kibz.js} +1 -1
  32. package/dist/ui/assets/{graphql-CddeAaS3.js → graphql-DmFr8ZvB.js} +1 -1
  33. package/dist/ui/assets/{hack-D_loGkOT.js → hack-BQcuk9lx.js} +1 -1
  34. package/dist/ui/assets/{haml-BoWDczd0.js → haml-Dbx0cG5B.js} +1 -1
  35. package/dist/ui/assets/{handlebars-ZoSOvw9y.js → handlebars-D0nD8yLi.js} +1 -1
  36. package/dist/ui/assets/{heavy-payload-DO34LwoZ.js → heavy-payload-CPOM8G8z.js} +3 -3
  37. package/dist/ui/assets/{html-CE-obXVo.js → html-9xkoIci1.js} +1 -1
  38. package/dist/ui/assets/{html-derivative-Cd-smAbW.js → html-derivative-Cg7n4YW0.js} +1 -1
  39. package/dist/ui/assets/{http-CZrkbrSl.js → http-Ckt_4_pP.js} +1 -1
  40. package/dist/ui/assets/{hurl-CxTuxBt1.js → hurl-BxMgqn9u.js} +1 -1
  41. package/dist/ui/assets/{java-BshLjCat.js → java-DxijgO-5.js} +1 -1
  42. package/dist/ui/assets/{javascript-D2pxQTiX.js → javascript-CusLzlRb.js} +1 -1
  43. package/dist/ui/assets/{jinja-kbfPP7-F.js → jinja-DBTRfImq.js} +1 -1
  44. package/dist/ui/assets/{jison-C2mt9l1M.js → jison-oCbVte0g.js} +1 -1
  45. package/dist/ui/assets/{json-DU_rg_AU.js → json-uq2yfoO2.js} +1 -1
  46. package/dist/ui/assets/{jsx-D_8aQMlx.js → jsx-B3AfJWyQ.js} +1 -1
  47. package/dist/ui/assets/{julia-BmefTaVV.js → julia-BFvq-UTm.js} +1 -1
  48. package/dist/ui/assets/{just-DXV8vk1S.js → just-Ck0W9Rcj.js} +1 -1
  49. package/dist/ui/assets/{latex-Cjm2MvGW.js → latex-BjKhTmaO.js} +1 -1
  50. package/dist/ui/assets/{liquid-DyQDlo0u.js → liquid-CsaE0q4b.js} +1 -1
  51. package/dist/ui/assets/{lua-B2A1vzzy.js → lua-D0MSBhv0.js} +1 -1
  52. package/dist/ui/assets/{marko-B9vOkXOU.js → marko-CTLxSPPr.js} +1 -1
  53. package/dist/ui/assets/{mdc-D42m6spU.js → mdc-BwDM_Bct.js} +1 -1
  54. package/dist/ui/assets/{nginx-TpfVxSUY.js → nginx-C5Q5rNaB.js} +1 -1
  55. package/dist/ui/assets/{nim-CJr5T1FK.js → nim-BjKHRt6k.js} +1 -1
  56. package/dist/ui/assets/{perl-cJZcsdm9.js → perl-BrABfniL.js} +1 -1
  57. package/dist/ui/assets/{php-CSZL5KRT.js → php-BWY__Khq.js} +1 -1
  58. package/dist/ui/assets/{pug-B1eMKzuA.js → pug-BD4PlwIK.js} +1 -1
  59. package/dist/ui/assets/{qml-Dl2_7zc9.js → qml-yQtdZNrK.js} +1 -1
  60. package/dist/ui/assets/{r-F0aaO9gG.js → r-D0tlixrf.js} +1 -1
  61. package/dist/ui/assets/{razor-BPnkSMCw.js → razor-CsmyuKJs.js} +1 -1
  62. package/dist/ui/assets/{regexp-BnGHMFbv.js → regexp-BXU4cTCD.js} +1 -1
  63. package/dist/ui/assets/{rst-B3CO7WYO.js → rst-CGlzAlrM.js} +1 -1
  64. package/dist/ui/assets/{ruby-B6VMzT1T.js → ruby-CRKDbfJj.js} +1 -1
  65. package/dist/ui/assets/{sas-Df2lHVuU.js → sas-CKDY6iPG.js} +1 -1
  66. package/dist/ui/assets/{scss-4XxI6sgu.js → scss-BCoXLSqD.js} +1 -1
  67. package/dist/ui/assets/{shellscript-DYASCN4r.js → shellscript-BVuRs_tF.js} +1 -1
  68. package/dist/ui/assets/{shellsession-Br6AGJO_.js → shellsession-alj1b7vN.js} +1 -1
  69. package/dist/ui/assets/{soy-p3ZNq1We.js → soy-BbQDrw9i.js} +1 -1
  70. package/dist/ui/assets/{sql-CAdv2OXL.js → sql-BdcqHaEA.js} +1 -1
  71. package/dist/ui/assets/{stata-Dx7yUbG_.js → stata-5IC5m_XB.js} +1 -1
  72. package/dist/ui/assets/{surrealql-6YsE8HQa.js → surrealql-BfVFLYzV.js} +1 -1
  73. package/dist/ui/assets/{svelte-YgzedwpD.js → svelte-B22aFi25.js} +1 -1
  74. package/dist/ui/assets/{templ-Bahqx3uD.js → templ-BRo7TcBX.js} +1 -1
  75. package/dist/ui/assets/{tex-o6fTQjkH.js → tex-CYhw-JbD.js} +1 -1
  76. package/dist/ui/assets/{ts-tags-BO-Co0Ju.js → ts-tags-C56aGOzq.js} +1 -1
  77. package/dist/ui/assets/{tsx-DtQe5AoH.js → tsx-IkFyQoro.js} +1 -1
  78. package/dist/ui/assets/{twig-BcUs6Kqc.js → twig-NkXaxf7l.js} +1 -1
  79. package/dist/ui/assets/{typescript-BPtM8h4u.js → typescript-AgbZakqy.js} +1 -1
  80. package/dist/ui/assets/{vue-5KkGUzJJ.js → vue-Cxz1Rk-f.js} +1 -1
  81. package/dist/ui/assets/{vue-html-Df_Xfsz9.js → vue-html-DWHLzP-w.js} +1 -1
  82. package/dist/ui/assets/{vue-vine-hUIe_u_Y.js → vue-vine-WR64TKte.js} +1 -1
  83. package/dist/ui/assets/{xml-C3Jy7as5.js → xml-CNdUn7jo.js} +1 -1
  84. package/dist/ui/assets/{xsl-BnF9OuFo.js → xsl-DLRRUZ98.js} +1 -1
  85. package/dist/ui/assets/{yaml-DyrFgCou.js → yaml-gltXViA8.js} +1 -1
  86. package/dist/workspace-colors.js +54 -0
  87. package/dist/workspaces/git/git-worktrees.js +53 -14
  88. package/dist/workspaces/inventory.js +2 -0
  89. package/dist/workspaces/presentation/workspace-presentation.js +13 -0
  90. package/dist/workspaces/relay/workspace-relay.js +3 -0
  91. package/dist/workspaces/sessions.js +28 -8
  92. package/dist/workspaces.js +9 -9
  93. package/docs/chatgpt-coding-workflow.md +16 -8
  94. package/docs/gotchas.md +2 -2
  95. package/package.json +2 -1
  96. package/scripts/ci/cmd-acceptance.mjs +3 -85
  97. package/scripts/ci/gates/acceptance-runtime.mjs +45 -0
  98. package/scripts/ci/gates/platform-tests.mjs +27 -17
  99. package/scripts/ci/gates/prepare-windows-shell-runtime.mjs +45 -0
  100. package/scripts/ci/gates/test-shard.mjs +29 -7
  101. package/scripts/ci/powershell51-acceptance.mjs +10 -91
  102. package/scripts/ci/pwsh-acceptance.mjs +7 -87
  103. package/scripts/ci/windows-product-acceptance.mjs +55 -0
  104. package/scripts/debug/runtime.mjs +14 -0
  105. package/scripts/debug/runtime.test.mjs +13 -1
  106. package/scripts/release/release-gate.test.mjs +121 -13
  107. package/scripts/release-parity.mjs +12 -1
  108. package/dist/ui/assets/activity-panel-app-DkfJzTZ3.css +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.2.4] - 2026-09-15
8
+
9
+ ### Added
10
+
11
+ - Managed worktrees can now start from arbitrary Git commit-ish refs while keeping their integration target branch independent, so a pinned historical baseline no longer has to be a local branch.
12
+
13
+ ### Changed
14
+
15
+ - Release verification now splits more independent CI work in parallel and reuses pinned npm / packaged shell runtime state where safe, reducing repeated setup in the cross-platform release matrix.
16
+
17
+ ### Fixed
18
+
19
+ - Pinned-base managed worktrees now preserve their baseline metadata across reuse, reopen, recovery, rebase, and close, while still integrating into the configured target branch; pinned workspaces with different immutable baselines no longer collapse onto one reuse identity.
20
+ - Workspace Panel long paths now wrap instead of truncating important Root / checkout details, and Workspace identity color is shared consistently between pretty logs and Panel SVG icons. The identity palette now uses 14 normal/bright terminal color slots, avoids duplicates for the first 14 distinct Workspace names in one process, and normalizes both POSIX and Windows-style paths consistently.
21
+
22
+ ## [1.2.3] - 2026-09-14
23
+
24
+ ### Fixed
25
+
26
+ - MCP App 静态资源现在在 API Origin validation 之前进入专用静态资源路由,使 ChatGPT `*.web-sandbox.oaiusercontent.com` iframe 可以跨域加载 JavaScript/CSS;MCP、OAuth 与其他 API 路由仍继续拒绝未授权 Origin,不扩大 API 信任边界。
27
+ - 交互式 `7677` debug 实例现在复用生产 ForgeRelay 的 Skill source(默认 `~/.forgerelay/skills`),同时继续隔离 debug 的 config、auth、state 与端口,使 Activity Panel / Workspace Skill 展示与生产发现语义一致。
28
+
7
29
  ## [1.2.2] - 2026-09-13
8
30
 
9
31
  ### Fixed
@@ -15,8 +15,10 @@
15
15
 
16
16
  - 默认 `mode="checkout"`,直接使用用户已有 checkout;
17
17
  - 只有明确需要隔离/并行 Git 工作时才选 `mode="worktree"`;
18
- - `baseRef` 只用于 managed worktree,默认取 source checkout 当前分支;
19
- - 已有 `workspaceId` 用于恢复同一个逻辑 workspace
18
+ - `baseRef` 只用于 managed worktree,表示新 managed branch 的起始 commit-ish;可以是本地 branch、tag、commit SHA 等可解析到 commit 的 ref,默认使用 source checkout 当前 `HEAD`;
19
+ - `targetBranch` 表示最终允许 `close_workspace` fast-forward 的本地 branch;未显式指定时,本地 branch `baseRef` 自身优先作为 target,否则使用 source checkout 当前本地 branchdetached source 无法推导 target 时必须显式提供;
20
+ - 显式固定到历史 base 的请求按解析后的 `baseSha` 区分复用身份,不会与同一 target 上另一历史 base 或普通 branch-following backing 混用;
21
+ - 已有 `workspaceId` 用于恢复同一个逻辑 workspace;closed managed-worktree Workspace reopen 时从当前持久化 `targetBranch` 建立新 backing,不重新钉回最初的历史 `baseRef`;
20
22
  - `newWorkspace` 只创建新的逻辑 handle;
21
23
  - `newWorktree` 才表示同一项目再创建一个独立物理 worktree。
22
24
 
@@ -122,6 +122,8 @@ export const workspaceInventoryEntryOutputSchema = z.object({
122
122
  state: z.enum(["active", "stale", "invalid", "closed"]),
123
123
  mode: z.enum(["checkout", "worktree"]),
124
124
  sourceRoot: z.string().optional(),
125
+ baseRef: z.string().optional(),
126
+ baseSha: z.string().optional(),
125
127
  branch: z.string().optional(),
126
128
  targetBranch: z.string().optional(),
127
129
  managed: z.boolean(),
@@ -181,6 +183,8 @@ export const workspaceInspectionOutputSchema = z.union([
181
183
  state: z.enum(["active", "stale", "invalid", "closed"]),
182
184
  mode: z.enum(["checkout", "worktree"]),
183
185
  sourceRoot: z.string().optional(),
186
+ baseRef: z.string().optional(),
187
+ baseSha: z.string().optional(),
184
188
  branch: z.string().optional(),
185
189
  targetBranch: z.string().optional(),
186
190
  managed: z.boolean(),
@@ -201,6 +205,8 @@ export const workspaceInspectionOutputSchema = z.union([
201
205
  state: z.enum(["active", "stale", "invalid", "closed"]).optional(),
202
206
  mode: z.enum(["checkout", "worktree"]),
203
207
  sourceRoot: z.string().optional(),
208
+ baseRef: z.string().optional(),
209
+ baseSha: z.string().optional(),
204
210
  branch: z.string().optional(),
205
211
  targetBranch: z.string().optional(),
206
212
  managed: z.boolean().optional(),
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { checkResourceAllowed, createMcpHandler, isInitializeRequest, isLegacyRequest, localhostAllowedOrigins, resourceUrlFromServerUrl, } from "@modelcontextprotocol/server";
3
- import { createMcpExpressApp, getOAuthProtectedResourceMetadataUrl, requireBearerAuth, } from "@modelcontextprotocol/express";
3
+ import { getOAuthProtectedResourceMetadataUrl, hostHeaderValidation, localhostHostValidation, originValidation, requireBearerAuth, } from "@modelcontextprotocol/express";
4
4
  import { NodeStreamableHTTPServerTransport, toNodeHandler, toWebRequest, } from "@modelcontextprotocol/node";
5
5
  import express from "express";
6
6
  import { ActivityAuditStore } from "../../../activity/history/audit-store.js";
@@ -40,11 +40,14 @@ export function createHttpServer(config, options, createMcpServer) {
40
40
  ...localhostAllowedOrigins(),
41
41
  ...routeBaseUrls.map((baseUrl) => baseUrl.hostname),
42
42
  ]));
43
- const app = createMcpExpressApp({
44
- host: config.host,
45
- ...(allowedHosts ? { allowedHosts } : {}),
46
- allowedOrigins,
47
- });
43
+ const app = express();
44
+ app.use(express.json());
45
+ if (allowedHosts) {
46
+ app.use(hostHeaderValidation(allowedHosts));
47
+ }
48
+ else if (["127.0.0.1", "localhost", "::1"].includes(config.host)) {
49
+ app.use(localhostHostValidation());
50
+ }
48
51
  const transports = new McpTransportRegistry({
49
52
  maxTransports: MAX_MCP_TRANSPORT_SESSIONS,
50
53
  });
@@ -193,16 +196,10 @@ export function createHttpServer(config, options, createMcpServer) {
193
196
  });
194
197
  next();
195
198
  });
196
- app.use(createForgeRelayAuthRouter({
197
- provider: oauthProvider,
198
- cliAuthenticationProvider: oauthProvider,
199
- instanceId: config.instanceId,
200
- issuerUrl: new URL(config.publicBaseUrl),
201
- resourceServerUrl,
202
- routeBaseUrls,
203
- scopesSupported: config.oauth.scopes,
204
- resourceName: "ForgeRelay",
205
- }));
199
+ // MCP App assets are intentionally public, cross-origin resources. ChatGPT
200
+ // renders an app on a dedicated `*.web-sandbox.oaiusercontent.com` origin,
201
+ // so these routes must run before MCP/API Origin validation. Host validation
202
+ // still applies above, while MCP, OAuth, and health routes remain protected.
206
203
  app.options(activityPanelAssetsPaths.map((assetPath) => `${assetPath}/{*asset}`), (_req, res) => {
207
204
  setActivityPanelAssetHeaders(res);
208
205
  res.sendStatus(204);
@@ -213,6 +210,17 @@ export function createHttpServer(config, options, createMcpServer) {
213
210
  fallthrough: false,
214
211
  setHeaders: setActivityPanelAssetHeaders,
215
212
  }));
213
+ app.use(originValidation(allowedOrigins));
214
+ app.use(createForgeRelayAuthRouter({
215
+ provider: oauthProvider,
216
+ cliAuthenticationProvider: oauthProvider,
217
+ instanceId: config.instanceId,
218
+ issuerUrl: new URL(config.publicBaseUrl),
219
+ resourceServerUrl,
220
+ routeBaseUrls,
221
+ scopesSupported: config.oauth.scopes,
222
+ resourceName: "ForgeRelay",
223
+ }));
216
224
  app.get(healthPaths, (_req, res) => {
217
225
  res.json({ ok: true, name: "forgerelay" });
218
226
  });
@@ -10,13 +10,14 @@ import { capabilityContextFor } from "../../core/capability-support.js";
10
10
  import { redactSkillDiagnosticPaths } from "../../core/schemas.js";
11
11
  import { logToolCall, workspaceLogContext } from "../../core/tool-support.js";
12
12
  import { buildExecutionShellContext } from "../../../server-instructions.js";
13
+ import { workspaceColorFor, workspaceIdentityName } from "../../../../workspace-colors.js";
13
14
  export const workspaceTaskUsageInstruction = "Use workspace.tasks proactively for work that spans multiple steps or sessions: create or update Tasks to preserve useful next steps and current state, and read its summary when resuming relevant unfinished work. Do not query it mechanically on every open_workspace call.";
14
15
  export const capabilityDiscoveryInstruction = "For capability describe/run, use only names present in capabilityCatalog. capabilityFingerprint.capabilities is a semantic feature fingerprint, not a callable registry; capabilityGuides are documentation descriptors, not callable capabilities.";
15
16
  export async function presentLocalWorkspaceOpen(options, input, contextData) {
16
17
  const { config, forgerelayVersion: FORGERELAY_VERSION, workspaces, workspaceTasks, reviewCheckpoints, capabilityRegistry, subagentProviders, hooks, rememberWorkspacePanelState, } = options;
17
- const { path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context } = input;
18
+ const { path, workspaceId, mode, baseRef, targetBranch, newWorktree, newWorkspace, context } = input;
18
19
  const { conversationScopeId, protectedWorkspaceIds, startedAt, sessionId } = contextData;
19
- const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, bootstrapContextComponents, contextFingerprint, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context }, {
20
+ const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, bootstrapContextComponents, contextFingerprint, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, targetBranch, newWorktree, newWorkspace, context }, {
20
21
  conversationScopeId,
21
22
  protectedWorkspaceIds,
22
23
  });
@@ -189,6 +190,7 @@ export async function presentLocalWorkspaceOpen(options, input, contextData) {
189
190
  kind: "workspace",
190
191
  root: workspace.root,
191
192
  path: workspace.root,
193
+ workspaceColor: workspaceColorFor(workspaceIdentityName(workspace.root)),
192
194
  mode: workspace.mode,
193
195
  workspaceReused,
194
196
  includeBootstrapContext: effectiveIncludeBootstrapContext,
@@ -21,6 +21,7 @@ export const openWorkspaceToolDefinition = {
21
21
  relay: z.string().optional().describe("Optional registered remote ForgeRelay alias for a path-backed member."),
22
22
  mode: z.enum(["checkout", "worktree"]).optional(),
23
23
  baseRef: z.string().optional(),
24
+ targetBranch: z.string().optional(),
24
25
  newWorktree: z.boolean().optional(),
25
26
  newWorkspace: z.boolean().optional(),
26
27
  }).optional().describe("Composite member definition used by action=member."),
@@ -55,7 +56,11 @@ export const openWorkspaceToolDefinition = {
55
56
  baseRef: z
56
57
  .string()
57
58
  .optional()
58
- .describe("Local branch to base a managed worktree on and eventually merge back into. Only used with mode=\"worktree\". Defaults to the source checkout's current branch."),
59
+ .describe("Commit-ish to base a managed worktree on. Only used with mode=\"worktree\"; may be a local branch, tag, commit SHA, or other ref that resolves to a commit. Defaults to HEAD."),
60
+ targetBranch: z
61
+ .string()
62
+ .optional()
63
+ .describe("Existing local branch that close_workspace may fast-forward when finalizing the managed worktree. Only used with mode=\"worktree\". Defaults to baseRef when baseRef names a local branch, otherwise the source checkout's current local branch."),
59
64
  newWorktree: z
60
65
  .boolean()
61
66
  .optional()
@@ -4,6 +4,7 @@ import { formatPathForPrompt } from "../../../../workspaces/resources/skills.js"
4
4
  import { compositeCapabilityContext } from "../../core/capability-support.js";
5
5
  import { compositeWorkspaceLogContext, logToolCall, textBlock } from "../../core/tool-support.js";
6
6
  import { openWorkspaceToolDefinition } from "./workspace-open-schema.js";
7
+ import { workspaceColorFor, workspaceIdentityName } from "../../../../workspace-colors.js";
7
8
  import { presentLocalWorkspaceOpen, workspaceTaskUsageInstruction, } from "./workspace-open-presentation.js";
8
9
  export function registerOpenWorkspaceTool(options) {
9
10
  const { server } = options;
@@ -11,7 +12,7 @@ export function registerOpenWorkspaceTool(options) {
11
12
  }
12
13
  async function handleOpenWorkspace(options, input, requestContext) {
13
14
  const { config, workspaces, remoteWorkspaces, compositeWorkspaces, workspaceTasks, processSessions, capabilityRegistry, compositeTaskGuides, loadCompositeMemberContext, rememberWorkspacePanelState, hostScopeIdFor, contextDeliveryScopeIdFor, presentation, } = options;
14
- const { action = "open", memberAction, member, kind, name, memberName, path, relay, workspaceId, mode, baseRef, newWorktree, newWorkspace, context, root, status, state, staleOnly, offset, limit, } = input;
15
+ const { action = "open", memberAction, member, kind, name, memberName, path, relay, workspaceId, mode, baseRef, targetBranch, newWorktree, newWorkspace, context, root, status, state, staleOnly, offset, limit, } = input;
15
16
  const startedAt = performance.now();
16
17
  const conversationScopeId = contextDeliveryScopeIdFor(requestContext);
17
18
  const protectedWorkspaceIds = processSessions.activeWorkspaceIds();
@@ -82,7 +83,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
82
83
  }
83
84
  if (memberAction !== undefined || member !== undefined || kind !== undefined || name !== undefined ||
84
85
  memberName !== undefined || path !== undefined || relay !== undefined || mode !== undefined ||
85
- baseRef !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
86
+ baseRef !== undefined || targetBranch !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
86
87
  root !== undefined || status !== undefined || state !== undefined || staleOnly !== undefined ||
87
88
  offset !== undefined || limit !== undefined) {
88
89
  throw new Error("open_workspace action=inspect accepts only workspaceId. It never opens, resumes, binds, or mutates the inspected Workspace.");
@@ -156,7 +157,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
156
157
  throw new Error("open_workspace action=member requires memberAction and member.");
157
158
  }
158
159
  if (kind !== undefined || name !== undefined || memberName !== undefined || path !== undefined || relay !== undefined || mode !== undefined ||
159
- baseRef !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
160
+ baseRef !== undefined || targetBranch !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
160
161
  root !== undefined || status !== undefined || state !== undefined || staleOnly !== undefined ||
161
162
  offset !== undefined || limit !== undefined) {
162
163
  throw new Error("open_workspace action=member accepts workspaceId, memberAction, and member only. Put any Workspace open definition inside member.");
@@ -173,8 +174,8 @@ async function handleOpenWorkspace(options, input, requestContext) {
173
174
  throw new Error("A Composite Workspace cannot be mounted as a Composite Workspace member.");
174
175
  }
175
176
  if (member.relay !== undefined || member.mode !== undefined || member.baseRef !== undefined ||
176
- member.newWorktree !== undefined || member.newWorkspace !== undefined) {
177
- throw new Error("member.workspaceId cannot be combined with relay/mode/baseRef/newWorktree/newWorkspace.");
177
+ member.targetBranch !== undefined || member.newWorktree !== undefined || member.newWorkspace !== undefined) {
178
+ throw new Error("member.workspaceId cannot be combined with relay/mode/baseRef/targetBranch/newWorktree/newWorkspace.");
178
179
  }
179
180
  if (!remoteWorkspaces.has(targetWorkspaceId))
180
181
  workspaces.getWorkspace(targetWorkspaceId);
@@ -185,6 +186,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
185
186
  path: member.path,
186
187
  ...(member.mode ? { mode: member.mode } : {}),
187
188
  ...(member.baseRef ? { baseRef: member.baseRef } : {}),
189
+ ...(member.targetBranch ? { targetBranch: member.targetBranch } : {}),
188
190
  ...(member.newWorktree !== undefined ? { newWorktree: member.newWorktree } : {}),
189
191
  ...(member.newWorkspace !== undefined ? { newWorkspace: member.newWorkspace } : {}),
190
192
  context: "none",
@@ -195,6 +197,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
195
197
  path: member.path,
196
198
  ...(member.mode ? { mode: member.mode } : {}),
197
199
  ...(member.baseRef ? { baseRef: member.baseRef } : {}),
200
+ ...(member.targetBranch ? { targetBranch: member.targetBranch } : {}),
198
201
  ...(member.newWorktree !== undefined ? { newWorktree: member.newWorktree } : {}),
199
202
  ...(member.newWorkspace !== undefined ? { newWorkspace: member.newWorkspace } : {}),
200
203
  context: "none",
@@ -218,8 +221,8 @@ async function handleOpenWorkspace(options, input, requestContext) {
218
221
  }
219
222
  else if (memberAction === "update") {
220
223
  const targetFieldsPresent = member.workspaceId !== undefined || member.path !== undefined || member.relay !== undefined ||
221
- member.mode !== undefined || member.baseRef !== undefined || member.newWorktree !== undefined ||
222
- member.newWorkspace !== undefined;
224
+ member.mode !== undefined || member.baseRef !== undefined || member.targetBranch !== undefined ||
225
+ member.newWorktree !== undefined || member.newWorkspace !== undefined;
223
226
  if (member.newName === undefined && member.purpose === undefined && !targetFieldsPresent) {
224
227
  throw new Error("Updating a Composite Workspace member requires newName, purpose, or a replacement Workspace target.");
225
228
  }
@@ -235,7 +238,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
235
238
  else {
236
239
  if (member.newName !== undefined || member.purpose !== undefined || member.workspaceId !== undefined || member.path !== undefined ||
237
240
  member.relay !== undefined || member.mode !== undefined || member.baseRef !== undefined ||
238
- member.newWorktree !== undefined || member.newWorkspace !== undefined) {
241
+ member.targetBranch !== undefined || member.newWorktree !== undefined || member.newWorkspace !== undefined) {
239
242
  throw new Error("Removing a Composite Workspace member accepts only member.name.");
240
243
  }
241
244
  composite = compositeWorkspaces.removeMember(workspaceId, member.name);
@@ -291,9 +294,9 @@ async function handleOpenWorkspace(options, input, requestContext) {
291
294
  return response;
292
295
  }
293
296
  if (action === "list") {
294
- if (path !== undefined || relay !== undefined || name !== undefined || memberName !== undefined || baseRef !== undefined || newWorktree !== undefined ||
295
- newWorkspace !== undefined || context !== undefined) {
296
- throw new Error("open_workspace action=list does not accept path, relay, name, memberName, baseRef, newWorktree, newWorkspace, or context. Use kind/root/workspaceId/mode/status/state/staleOnly for inventory filters.");
297
+ if (path !== undefined || relay !== undefined || name !== undefined || memberName !== undefined || baseRef !== undefined || targetBranch !== undefined ||
298
+ newWorktree !== undefined || newWorkspace !== undefined || context !== undefined) {
299
+ throw new Error("open_workspace action=list does not accept path, relay, name, memberName, baseRef, targetBranch, newWorktree, newWorkspace, or context. Use kind/root/workspaceId/mode/status/state/staleOnly for inventory filters.");
297
300
  }
298
301
  const compositeInventory = () => compositeWorkspaces.list()
299
302
  .filter((entry) => workspaceId === undefined || entry.id === workspaceId)
@@ -382,7 +385,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
382
385
  const openingComposite = kind === "composite" ||
383
386
  (workspaceId !== undefined && compositeWorkspaces.has(workspaceId));
384
387
  if (openingComposite) {
385
- if (relay !== undefined || path !== undefined || mode !== undefined || baseRef !== undefined ||
388
+ if (relay !== undefined || path !== undefined || mode !== undefined || baseRef !== undefined || targetBranch !== undefined ||
386
389
  newWorktree !== undefined || newWorkspace !== undefined) {
387
390
  throw new Error("Composite Workspace open accepts name/workspaceId/context only; members are attached separately and keep their own Workspace definitions.");
388
391
  }
@@ -432,6 +435,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
432
435
  kind: "composite",
433
436
  name: composite.name,
434
437
  path: composite.name,
438
+ workspaceColor: workspaceColorFor(workspaceIdentityName(composite.name)),
435
439
  members: composite.members,
436
440
  instruction,
437
441
  summary: { members: composite.members.length, status: composite.status },
@@ -464,7 +468,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
464
468
  throw new Error("open_workspace name/memberName are only valid for a Composite Workspace.");
465
469
  }
466
470
  if (workspaceId !== undefined && remoteWorkspaces.has(workspaceId)) {
467
- if (path !== undefined || relay !== undefined || mode !== undefined || baseRef !== undefined ||
471
+ if (path !== undefined || relay !== undefined || mode !== undefined || baseRef !== undefined || targetBranch !== undefined ||
468
472
  newWorktree !== undefined || newWorkspace !== undefined) {
469
473
  throw new Error("Resuming a relayed Workspace by workspaceId accepts context only.");
470
474
  }
@@ -479,6 +483,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
479
483
  path,
480
484
  mode,
481
485
  baseRef,
486
+ targetBranch,
482
487
  newWorktree,
483
488
  newWorkspace,
484
489
  context,
@@ -511,6 +516,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
511
516
  kind: "workspace",
512
517
  root: opened.root,
513
518
  path: opened.root,
519
+ workspaceColor: workspaceColorFor(workspaceIdentityName(opened.root)),
514
520
  mode: opened.mode,
515
521
  relay,
516
522
  instruction: opened.instruction,
@@ -1,5 +1,5 @@
1
- import { basename } from "node:path";
2
1
  import { styleText } from "node:util";
2
+ import { workspaceColorFor, workspaceIdentityName, } from "../../workspace-colors.js";
3
3
  const LEVEL_WEIGHT = {
4
4
  silent: 0,
5
5
  error: 1,
@@ -13,14 +13,22 @@ const LEVEL_STYLE = {
13
13
  info: "green",
14
14
  debug: "gray",
15
15
  };
16
- const WORKSPACE_PROJECT_COLORS = [
17
- "cyanBright",
18
- "greenBright",
19
- "yellowBright",
20
- "magentaBright",
21
- "blueBright",
22
- "whiteBright",
23
- ];
16
+ const WORKSPACE_PROJECT_COLORS = {
17
+ red: "red",
18
+ green: "green",
19
+ yellow: "yellow",
20
+ blue: "blue",
21
+ magenta: "magenta",
22
+ cyan: "cyan",
23
+ white: "white",
24
+ redBright: "redBright",
25
+ greenBright: "greenBright",
26
+ yellowBright: "yellowBright",
27
+ blueBright: "blueBright",
28
+ magentaBright: "magentaBright",
29
+ cyanBright: "cyanBright",
30
+ whiteBright: "whiteBright",
31
+ };
24
32
  export function shouldLog(config, level) {
25
33
  return LEVEL_WEIGHT[config.level] >= LEVEL_WEIGHT[level];
26
34
  }
@@ -59,7 +67,7 @@ export function transportSessionIdPrefix(transportSessionId) {
59
67
  /** @deprecated Use transportSessionIdPrefix. */
60
68
  export const sessionIdPrefix = transportSessionIdPrefix;
61
69
  export function workspaceLogLabel(root, workspaceId) {
62
- return `${basename(root)}/${workspaceId}`;
70
+ return `${workspaceIdentityName(root)}/${workspaceId}`;
63
71
  }
64
72
  export function commandPreview(command) {
65
73
  const normalized = command.replace(/\s+/g, " ").trim();
@@ -215,17 +223,9 @@ function formatPrettySource(source, options) {
215
223
  }
216
224
  const project = source.slice(0, separator);
217
225
  const workspace = source.slice(separator + 1);
218
- const projectColor = WORKSPACE_PROJECT_COLORS[stableColorIndex(project)];
226
+ const projectColor = WORKSPACE_PROJECT_COLORS[workspaceColorFor(project)];
219
227
  return `${style([projectColor, "bold"], project, options)}/${style(["cyan", "underline"], workspace, options)}`;
220
228
  }
221
- function stableColorIndex(value) {
222
- let hash = 2166136261;
223
- for (let index = 0; index < value.length; index += 1) {
224
- hash ^= value.charCodeAt(index);
225
- hash = Math.imul(hash, 16777619);
226
- }
227
- return (hash >>> 0) % WORKSPACE_PROJECT_COLORS.length;
228
- }
229
229
  function formatRuntimeResources(entry) {
230
230
  const rssMb = bytesToMegabytes(numberField(entry.rssBytes));
231
231
  const heapUsedMb = bytesToMegabytes(numberField(entry.heapUsedBytes));