@akira-tl/forgerelay 0.3.3 → 0.3.5

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 (97) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -2
  3. package/capabilities/artifacts-review/GUIDE.md +2 -4
  4. package/capabilities/managed-worktrees/GUIDE.md +12 -7
  5. package/capabilities/shell-processes/GUIDE.md +10 -8
  6. package/dist/artifact-tools.js +0 -128
  7. package/dist/capabilities.js +4 -7
  8. package/dist/hooks.js +12 -0
  9. package/dist/mcp/server-instructions.js +7 -13
  10. package/dist/review-checkpoints.js +4 -4
  11. package/dist/server.js +217 -365
  12. package/dist/ui/.vite/manifest.json +280 -280
  13. package/dist/ui/assets/{angular-html-CQmCUfkv.js → angular-html-BT2Z6jee.js} +1 -1
  14. package/dist/ui/assets/{angular-ts-BSbfxpMS.js → angular-ts-BwPE8YDt.js} +1 -1
  15. package/dist/ui/assets/{apl-CWCaH-E3.js → apl-CGbONVvU.js} +1 -1
  16. package/dist/ui/assets/{astro-R_p4G0xA.js → astro-DIAPy7cj.js} +1 -1
  17. package/dist/ui/assets/{blade-irHensDY.js → blade-Cgtd19Ky.js} +1 -1
  18. package/dist/ui/assets/{c-_XEQUoq6.js → c-acnvZ8tm.js} +1 -1
  19. package/dist/ui/assets/{cobol-Css6VQjJ.js → cobol-G-7HLVKW.js} +1 -1
  20. package/dist/ui/assets/{coffee-CpXUkyDm.js → coffee-DQFyKxEB.js} +1 -1
  21. package/dist/ui/assets/{cpp-BQHsPWwI.js → cpp-UyrmHXan.js} +1 -1
  22. package/dist/ui/assets/{crystal-Cbv8J9Fm.js → crystal-DuiWhEO3.js} +1 -1
  23. package/dist/ui/assets/{css-D8DkrS3U.js → css-D_WCbY6m.js} +1 -1
  24. package/dist/ui/assets/{edge-bBL_XDAG.js → edge-COkalhFi.js} +1 -1
  25. package/dist/ui/assets/{elixir-CQjsM9sA.js → elixir-cdlzbYR1.js} +1 -1
  26. package/dist/ui/assets/{elm-DLFl-jEZ.js → elm-ZwDYuYpb.js} +1 -1
  27. package/dist/ui/assets/{erb-BJjsdB_Q.js → erb-C8WG9TtC.js} +1 -1
  28. package/dist/ui/assets/{git-rebase-CqP5A2s0.js → git-rebase-CMlDVJBT.js} +1 -1
  29. package/dist/ui/assets/{glimmer-js-B5Hh_7xe.js → glimmer-js-DeLOQlol.js} +1 -1
  30. package/dist/ui/assets/{glimmer-ts-oaeSAC8s.js → glimmer-ts-DTEM04TK.js} +1 -1
  31. package/dist/ui/assets/{glsl-BylkOpcF.js → glsl-IJV4SB-M.js} +1 -1
  32. package/dist/ui/assets/{graphql-DsVRvP6S.js → graphql-PM-WDnoB.js} +1 -1
  33. package/dist/ui/assets/{hack-D4N5abT_.js → hack-C14sh6Lh.js} +1 -1
  34. package/dist/ui/assets/{haml-CIAJQIox.js → haml-B_5nS6Kx.js} +1 -1
  35. package/dist/ui/assets/{handlebars-DDEZyNXR.js → handlebars-eS-J0O-M.js} +1 -1
  36. package/dist/ui/assets/{heavy-payload-DafM8yjB.js → heavy-payload-BKB0QDOI.js} +1 -1
  37. package/dist/ui/assets/{html-zDy-3lvy.js → html-C3TPjrC5.js} +1 -1
  38. package/dist/ui/assets/{html-derivative-CZ91Y2d5.js → html-derivative-BlVZU2id.js} +1 -1
  39. package/dist/ui/assets/{http-ezaodZRf.js → http-BmbZzA5x.js} +1 -1
  40. package/dist/ui/assets/{hurl-BmL0o7TO.js → hurl-Dnd_YpOD.js} +1 -1
  41. package/dist/ui/assets/{java-Q2C0vwbH.js → java-ne9HdIVn.js} +1 -1
  42. package/dist/ui/assets/{javascript-UDhBkHE3.js → javascript-DpHT58rr.js} +1 -1
  43. package/dist/ui/assets/{jinja-BWRskcYS.js → jinja-DTvkZdHA.js} +1 -1
  44. package/dist/ui/assets/{jison-B18BY9Ds.js → jison-DUJG-tmh.js} +1 -1
  45. package/dist/ui/assets/{json-DNj7MpRr.js → json-DJmnBdxW.js} +1 -1
  46. package/dist/ui/assets/{jsx-CVpHJ6cr.js → jsx-BclJnZnN.js} +1 -1
  47. package/dist/ui/assets/{julia-CJBNBS9c.js → julia-P5B4pHFa.js} +1 -1
  48. package/dist/ui/assets/{just-Bmu2JCQT.js → just-DYaoh7C6.js} +1 -1
  49. package/dist/ui/assets/{latex-BB7F93KY.js → latex-CI5lnkWV.js} +1 -1
  50. package/dist/ui/assets/{liquid-Btnomc6E.js → liquid-D3LnAs_z.js} +1 -1
  51. package/dist/ui/assets/{lua-D0FYT9Vo.js → lua-dZC7ZQIY.js} +1 -1
  52. package/dist/ui/assets/{marko-MHLw0ZlK.js → marko-CRDrjqfS.js} +1 -1
  53. package/dist/ui/assets/{mdc-CSr-suGy.js → mdc-ZzwgmJVn.js} +1 -1
  54. package/dist/ui/assets/{nginx-ur1l6E-g.js → nginx-BAvFXGSV.js} +1 -1
  55. package/dist/ui/assets/{nim-B2k0IugA.js → nim-BHIFawe9.js} +1 -1
  56. package/dist/ui/assets/{perl-BqOHxFDn.js → perl-C_f5AnLa.js} +1 -1
  57. package/dist/ui/assets/{php-DzPK6O1_.js → php-DDqpegL5.js} +1 -1
  58. package/dist/ui/assets/{pug-D169OZkr.js → pug-CSaAZXyd.js} +1 -1
  59. package/dist/ui/assets/{qml-BtemJTC4.js → qml-CNHvRs_z.js} +1 -1
  60. package/dist/ui/assets/{r-BGVHu0bd.js → r-CAfoQND2.js} +1 -1
  61. package/dist/ui/assets/{razor-Diisjg1d.js → razor-BDfAPy7I.js} +1 -1
  62. package/dist/ui/assets/{regexp-B2F5vMZ6.js → regexp-Mug5jltQ.js} +1 -1
  63. package/dist/ui/assets/{review-payload-B-3I3Oj_.js → review-payload-D7GIdYDj.js} +1 -1
  64. package/dist/ui/assets/{rst-BxpeZwuT.js → rst-BzPP2gle.js} +1 -1
  65. package/dist/ui/assets/{ruby-DrCq4riN.js → ruby-CQjzokzX.js} +1 -1
  66. package/dist/ui/assets/{sas-CI_2W-1n.js → sas-Bp37KGr7.js} +1 -1
  67. package/dist/ui/assets/{scrollbar-DHsDF5w-.js → scrollbar-HUMBdLLK.js} +3 -3
  68. package/dist/ui/assets/{scss-BxtyHZ1p.js → scss-C_Z1DYnw.js} +1 -1
  69. package/dist/ui/assets/{shellscript-BY_YYrHT.js → shellscript-CLfJpn3I.js} +1 -1
  70. package/dist/ui/assets/{shellsession-C6uBrLmA.js → shellsession-CNc62UhY.js} +1 -1
  71. package/dist/ui/assets/{soy-BHM9gdPh.js → soy-CF44SpBo.js} +1 -1
  72. package/dist/ui/assets/{sql-XmhfEGz_.js → sql-CYnzfiQ1.js} +1 -1
  73. package/dist/ui/assets/{stata-BbHY7NEo.js → stata-DVeMA68W.js} +1 -1
  74. package/dist/ui/assets/{surrealql-CD49DZ3E.js → surrealql-52sF96Yv.js} +1 -1
  75. package/dist/ui/assets/{svelte-CbYsEYJY.js → svelte-ydwT7UbF.js} +1 -1
  76. package/dist/ui/assets/{templ-B5JoUxEB.js → templ-BXcoVoGh.js} +1 -1
  77. package/dist/ui/assets/{tex-DZcOfTZK.js → tex-COsYFp7O.js} +1 -1
  78. package/dist/ui/assets/{ts-tags-BeU-xqc0.js → ts-tags-CF--LE1s.js} +1 -1
  79. package/dist/ui/assets/{tsx-V7W1xGl3.js → tsx-DLBt2-2f.js} +1 -1
  80. package/dist/ui/assets/{twig-MIaJo5BU.js → twig-YoM-DiI5.js} +1 -1
  81. package/dist/ui/assets/{typescript-C0WESuVc.js → typescript-Ce9JGkNI.js} +1 -1
  82. package/dist/ui/assets/{vue-eJHwc9ra.js → vue-B5LDEy4l.js} +1 -1
  83. package/dist/ui/assets/{vue-html-CLmBujTU.js → vue-html-CPdGGk4T.js} +1 -1
  84. package/dist/ui/assets/{vue-vine-BP5kjIGb.js → vue-vine-B5bjgk0a.js} +1 -1
  85. package/dist/ui/assets/{workspace-app-C1qR5cAY.js → workspace-app-Cm9EX5x4.js} +3 -3
  86. package/dist/ui/assets/{xml-j7pmyP48.js → xml-Cun8xb0T.js} +1 -1
  87. package/dist/ui/assets/{xsl-5VFHbg6o.js → xsl-wls_CEK_.js} +1 -1
  88. package/dist/ui/assets/{yaml-DxFAt-KU.js → yaml-BxuXn9OL.js} +1 -1
  89. package/dist/ui/workspace-app.html +1 -1
  90. package/docs/artifact-exchange.md +4 -3
  91. package/docs/chatgpt-coding-workflow.md +31 -27
  92. package/docs/configuration.md +27 -20
  93. package/docs/debugging.md +3 -3
  94. package/docs/gotchas.md +9 -8
  95. package/docs/security.md +6 -5
  96. package/package.json +1 -1
  97. package/scripts/debug/accept.mjs +80 -25
@@ -78,7 +78,7 @@ FORGERELAY_ARTIFACTS=1 npx @akira-tl/forgerelay serve
78
78
 
79
79
  | Variable | Default | Purpose |
80
80
  | --- | --- | --- |
81
- | `FORGERELAY_ARTIFACTS` | `0` | Expose `download_artifact` for trusted native files. |
81
+ | `FORGERELAY_ARTIFACTS` | `0` | Advertise the `artifact.download` Capability for trusted native files. |
82
82
  | `FORGERELAY_ARTIFACT_MAX_FILE_BYTES` | `104857600` | Maximum streamed size of one file (100 MiB). |
83
83
 
84
84
  The same settings may be persisted as `artifactsEnabled` and
@@ -115,20 +115,22 @@ MCP clients discover metadata from:
115
115
 
116
116
  | Value | Behavior |
117
117
  | --- | --- |
118
- | `minimal` | Default. Exposes `open_workspace`, `close_workspace`, `read`, `write`, `edit`, `rename`, `delete`, `bash`, `write_stdin`, and `close_worktree`. |
119
- | `full` | Adds dedicated `grep`, `glob`, and `ls` tools. |
120
- | `codex` | Experimental Codex-shaped tool surface using `open_workspace`, `close_workspace`, `read`, `rename`, `delete`, `apply_patch`, `exec_command`, `write_stdin`, and `close_worktree`. |
118
+ | `minimal` | Default canonical surface: `open_workspace`, `capability`, `close_workspace`, `read`, `write`, `edit`, `rename`, `delete`, and `bash`. |
119
+ | `full` | Compatibility value. Uses the same canonical 9-tool surface as `minimal`; search and directory inspection go through `bash`. |
120
+ | `codex` | Experimental Codex-shaped compatibility adapter using `open_workspace`, `close_workspace`, `read`, `rename`, `delete`, `apply_patch`, `exec_command`, `write_stdin`, and `capability`. It does not define the ForgeRelay canonical interface. |
121
121
 
122
122
  `FORGERELAY_MINIMAL_TOOLS` remains a compatibility-style boolean alias when the
123
123
  explicit tool mode is unset. The corresponding legacy `DEVSPACE_*` names are
124
124
  also accepted.
125
125
 
126
- The selected mode controls the real `tools/list` surface. ForgeRelay does not
127
- hide callable tools behind capability documentation. In every mode,
126
+ `minimal` and `full` now resolve to the same regular 9-tool `tools/list`; `full`
127
+ is retained only as a configuration-compatibility value. `codex` selects a
128
+ separate compatibility adapter. ForgeRelay does not hide core callable tools
129
+ behind capability documentation. In every mode,
128
130
  `open_workspace` returns a `capabilityFingerprint` containing the package
129
131
  version, tool mode, and stable semantic capability names. The fingerprint also
130
132
  reports enabled optional domains such as subagent profile discovery, native
131
- artifact download, MCP App UI, or aggregate `show_changes` review when those
133
+ artifact download, MCP App UI, or aggregate `review.changes` when those
132
134
  features are actually available; it remains a semantic summary rather than a
133
135
  copy of `tools/list`.
134
136
 
@@ -142,8 +144,8 @@ the corresponding feature is enabled, so disabled features do not add bootstrap
142
144
  context.
143
145
 
144
146
  There is no separate progressive-disclosure configuration switch. Capability
145
- Guide discovery is built in, while actual tool exposure continues to be
146
- controlled by `FORGERELAY_TOOL_MODE` and feature-specific settings. If the
147
+ Guide discovery is built in, while optional low-frequency actions are advertised
148
+ through the workspace Capability catalog instead of adding top-level tools. If the
147
149
  fingerprint reports a capability that is missing from the Host's current tool
148
150
  snapshot, treat that as stale Host MCP metadata: reconnect/refresh the integration
149
151
  or use a Host context that reloads `tools/list`. The ForgeRelay process cannot
@@ -163,16 +165,21 @@ current conversation. `newWorkspace: true` allocates a new logical handle withou
163
165
  creating another checkout or Git worktree and should be used only on explicit user
164
166
  request. Logical workspaces idle for more than two days are returned in `staleWorkspaces` so
165
167
  the user can choose whether to resume or release them. `close_workspace` removes a
166
- logical handle without deleting checkout files; it refuses to remove the last
167
- handle anchoring a physical worktree.
168
-
169
- `bash` has no execution-timeout input. It waits in the foreground for at most 300
170
- seconds; if the process is still alive, the result contains `running: true` and a
171
- canonical `processId`. `write_stdin` can poll or interact with that process for up
172
- to another 300 seconds per call. The former `sessionId` field remains a deprecated
173
- alias during the 0.2.x compatibility window. ForgeRelay does not kill a process
174
- merely because a wait window expires. Completed background processes are delivered once with a later
175
- tool result for the same logical workspace ID.
168
+ checkout-backed logical handle without deleting checkout files. For a managed-worktree-backed
169
+ workspace, `close_workspace` requires `commitMessage` and runs the existing safe
170
+ worktree finalize lifecycle: close Hooks, commit when needed, fast-forward-only
171
+ integration, cleanup, and alias invalidation.
172
+
173
+ Regular `bash` has no execution-timeout input. `action="run"` (the default) waits
174
+ in the foreground for at most 300 seconds; if the process is still alive, the
175
+ result contains `running: true` and a canonical `processId`. Reuse the same `bash`
176
+ with `action="process"` to poll/wait, send `input`, resize a PTY, or set
177
+ `interrupt:true`; each wait can be up to 300 seconds. ForgeRelay does not kill a
178
+ process merely because a wait window expires. Completed background processes are
179
+ delivered once with a later tool result for the same logical workspace ID.
180
+
181
+ Codex mode retains `write_stdin` only as an experimental compatibility adapter;
182
+ regular Agent workflows should use the single `bash` process lifecycle.
176
183
 
177
184
  ## Widgets
178
185
 
@@ -181,7 +188,7 @@ tool result for the same logical workspace ID.
181
188
  | Value | Behavior |
182
189
  | --- | --- |
183
190
  | `full` | Default. Attach UI to exposed workspace/file/edit/shell tools. |
184
- | `changes` | Attach UI to `open_workspace` and aggregate `show_changes`. |
191
+ | `changes` | Attach UI to `open_workspace` and Capability Gateway review results from `review.changes`. |
185
192
  | `off` | Disable widget UI. |
186
193
 
187
194
  ## Lifecycle hooks
package/docs/debugging.md CHANGED
@@ -59,11 +59,11 @@ The acceptance checks:
59
59
  3. unauthenticated `/mcp` rejection;
60
60
  4. dynamic OAuth client registration, PKCE Owner-password approval, and access-token exchange;
61
61
  5. MCP `initialize`, including package/server version consistency and the shell mutation safety contract;
62
- 6. `tools/list` for the full debug tool surface, including `close_workspace`, `write_stdin`, canonical `processId` plus the deprecated `sessionId` compatibility alias, the non-blanket `bash` mutation policy, no kill-timeout input, the 300-second foreground-wait contract, workspace resume/stale-workspace schema, and MCP App tool metadata;
62
+ 6. `tools/list` for the exact canonical 9-tool regular surface, including unified `close_workspace`, the single `capability` Gateway, canonical `bash` `action="run"` / `action="process"` plus `processId`, absence of retired dedicated aliases/search tools, the non-blanket shell mutation policy, workspace resume/stale-workspace schema, and MCP App tool metadata;
63
63
  7. the full MCP App template chain: `resources/list`, `resources/templates/list`, current content-hashed `resources/read`, legacy/historical template compatibility reads, `text/html;profile=mcp-app`, the unique app domain plus CSP resource domains, and an HTTP fetch of the JavaScript asset referenced by the template;
64
- 8. a real checkout workspace with `write`, `read`, `rename`, `delete`, foreground `bash` through `ProcessManager`, and a deliberate failed `edit`;
64
+ 8. a real Git checkout workspace with Capability catalog/describe/read/run, `review.changes`, `write`, `read`, `rename`, `delete`, foreground `bash`, `bash` long-process `run` → `processId` → `process`, and a deliberate failed `edit`; on Linux the catalog/describe path also verifies `artifact.download` native-file transport metadata;
65
65
  9. OS temp-directory `write` → `read` → `edit` → `rename` → `delete` over the same real MCP transport session, plus rejection of an arbitrary path outside the workspace/temp roots;
66
- 10. a temporary Git repository with managed worktree creation, file modification, and `close_worktree`;
66
+ 10. a temporary Git repository with managed worktree creation, file modification, and `close_workspace` worktree finalization;
67
67
  11. 本地 bare remote 上的 release-tag-push Hook:成功 Hook 必须先运行再允许 `v0.2.0` push,失败 Hook 必须在 remote mutation 前阻断 `v0.2.1`;
68
68
  12. deterministic local subagent error path,不联系任何模型 provider;
69
69
  13. debug hook recorder 覆盖全部九个 Hooks v1 lifecycle events。
package/docs/gotchas.md CHANGED
@@ -165,18 +165,19 @@ Legacy persisted `devspace/*` branches remain valid and closable.
165
165
  Uncommitted source-checkout changes are not automatically copied into a newly
166
166
  created worktree.
167
167
 
168
- ## `close_worktree` refuses to finish
168
+ ## `close_workspace` refuses to finalize a managed worktree
169
169
 
170
- Close is deliberately refused when:
170
+ For a managed-worktree-backed workspace, close is deliberately refused when:
171
171
 
172
172
  - the source checkout is dirty;
173
173
  - the source checkout is no longer on the recorded target branch;
174
174
  - the managed worktree is on the wrong branch;
175
175
  - source and managed histories diverged.
176
176
 
177
- Integration is fast-forward-only. If histories diverge, rebase and verify inside
178
- the managed worktree, then retry. ForgeRelay does not intentionally leave the
179
- source checkout in a merge-conflict state.
177
+ A `commitMessage` is also required for managed-worktree-backed close. Integration
178
+ is fast-forward-only. If histories diverge, rebase and verify inside the managed
179
+ worktree, then retry the same `close_workspace` call with the original workspaceId.
180
+ ForgeRelay does not intentionally leave the source checkout in a merge-conflict state.
180
181
 
181
182
  ## Windows shell commands fail
182
183
 
@@ -233,8 +234,8 @@ Per-tool widgets default to:
233
234
  FORGERELAY_WIDGETS=full
234
235
  ```
235
236
 
236
- Use `FORGERELAY_WIDGETS=changes` for aggregate `show_changes`, or `off` to
237
- disable UI. Plain MCP clients may ignore MCP App widget metadata.
237
+ Use `FORGERELAY_WIDGETS=changes` for aggregate `review.changes` Capability results,
238
+ or `off` to disable UI. Plain MCP clients may ignore MCP App widget metadata.
238
239
 
239
240
  If ChatGPT shows `Failed to fetch template`, first verify the server-side template
240
241
  chain with:
@@ -244,7 +245,7 @@ npm run build
244
245
  npm run debug:accept
245
246
  ```
246
247
 
247
- The acceptance runner enables full widgets and checks that the tool advertises a
248
+ The acceptance runner enables review-focused widgets and checks that the Gateway advertises a
248
249
  content-hashed `ui://forgerelay/workspace-app-<hash>.html` resource, that
249
250
  `resources/read` returns `text/html;profile=mcp-app`, and that the referenced
250
251
  JavaScript asset is reachable. ForgeRelay also keeps the legacy
package/docs/security.md CHANGED
@@ -141,9 +141,9 @@ Do not describe ForgeRelay as a sandboxed coding environment.
141
141
 
142
142
  Shell execution has a 300-second foreground wait ceiling, not a 300-second
143
143
  process lifetime. When `bash` is still running after that window, ForgeRelay
144
- returns a canonical `processId` and leaves the process alive. `write_stdin` can
145
- poll, wait, interact, or explicitly interrupt it. The former process `sessionId`
146
- remains a deprecated compatibility alias during 0.2.x. An asynchronously completed process
144
+ returns a canonical `processId` and leaves the process alive. Regular tool modes
145
+ reuse `bash(action="process")` to poll, wait, write input, resize a PTY, or
146
+ explicitly interrupt that process. An asynchronously completed process
147
147
  is reported on a later tool result for the same logical workspace ID, including
148
148
  error-result paths, and is never broadcast to another workspace ID. Explicitly
149
149
  resuming the same workspace ID in another conversation intentionally transfers
@@ -179,8 +179,9 @@ the source checkout into a merge-conflict state.
179
179
 
180
180
  ## Native artifact download
181
181
 
182
- When enabled, `download_artifact` accepts only the supported native file value
183
- provided by the MCP host. It does not accept arbitrary signed URLs, local paths,
182
+ When enabled, the registered `artifact.download` Capability accepts only the supported
183
+ native file value provided by the MCP host through the `capability` Gateway. It does
184
+ not accept arbitrary signed URLs, local paths,
184
185
  embedded credentials, or base64 strings as substitutes.
185
186
 
186
187
  Downloads are streamed, size-limited, created without overwrite, and published
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Local development control plane for MCP coding agents.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Akira-TL/forgerelay#readme",
@@ -37,14 +37,16 @@ assertCurlAvailable();
37
37
  await assertDebugPortFree();
38
38
  rmSync(acceptanceRoot, { recursive: true, force: true });
39
39
  mkdirSync(acceptanceRoot, { recursive: true });
40
+ setupGitProject(checkoutWorkspace);
40
41
 
41
42
  const { env } = createDebugEnvironment({
42
43
  ownerToken,
43
44
  stateDir,
44
45
  worktreeRoot,
45
46
  hookLog,
46
- widgets: "full",
47
+ widgets: "changes",
47
48
  });
49
+ env.FORGERELAY_ARTIFACTS = "1";
48
50
  const doctor = spawnSync(process.execPath, ["--import", "tsx", "src/cli.ts", "doctor"], {
49
51
  cwd: repoRoot,
50
52
  env,
@@ -53,7 +55,7 @@ const doctor = spawnSync(process.execPath, ["--import", "tsx", "src/cli.ts", "do
53
55
  assert.equal(doctor.status, 0, doctor.stderr);
54
56
  assert.match(doctor.stdout, /Public base URL: http:\/\/127\.0\.0\.1:7677/);
55
57
  assert.match(doctor.stdout, /Tool mode: full/);
56
- assert.match(doctor.stdout, /Widgets: full/);
58
+ assert.match(doctor.stdout, /Widgets: changes/);
57
59
  assert.match(doctor.stdout, /Trust proxy: off/);
58
60
  pass("doctor resolved MCP shape", "public URL + tool/widgets/proxy state");
59
61
 
@@ -129,9 +131,17 @@ try {
129
131
  params: {},
130
132
  }).message.result.tools;
131
133
  const toolNames = tools.map((tool) => tool.name);
132
- for (const expected of ["open_workspace", "close_workspace", "close_worktree", "read", "write", "edit", "rename", "delete", "grep", "glob", "ls", "bash", "write_stdin", "capability"]) {
133
- assert.ok(toolNames.includes(expected), `missing debug tool ${expected}`);
134
- }
134
+ assert.deepEqual(toolNames, [
135
+ "open_workspace",
136
+ "capability",
137
+ "close_workspace",
138
+ "read",
139
+ "write",
140
+ "edit",
141
+ "rename",
142
+ "delete",
143
+ "bash",
144
+ ]);
135
145
  const bashTool = tools.find((tool) => tool.name === "bash");
136
146
  assert.match(bashTool?.description ?? "", /local user's authority/);
137
147
  assert.doesNotMatch(bashTool?.description ?? "", /may modify ordinary project files/);
@@ -139,18 +149,10 @@ try {
139
149
  assert.doesNotMatch(bashTool?.description ?? "", /external device or hardware mutations/);
140
150
  assert.doesNotMatch(bashTool?.description ?? "", /Do not use bash to create, move, rename, or delete project files/);
141
151
  assert.equal(bashTool?.inputSchema?.properties?.timeout, undefined);
142
- assert.match(bashTool?.description ?? "", /waits up to 300 seconds/);
143
- assert.match(bashTool?.description ?? "", /write_stdin/);
144
- const writeStdinTool = tools.find((tool) => tool.name === "write_stdin");
145
- assert.equal(writeStdinTool?.inputSchema?.properties?.yieldTimeMs?.maximum, 300000);
146
- assert.match(
147
- writeStdinTool?.inputSchema?.properties?.processId?.description ?? "",
148
- /Canonical process identifier/,
149
- );
150
- assert.match(
151
- writeStdinTool?.inputSchema?.properties?.sessionId?.description ?? "",
152
- /Deprecated alias for processId/,
153
- );
152
+ assert.match(bashTool?.description ?? "", /action=process/);
153
+ assert.equal(bashTool?.inputSchema?.properties?.yieldTimeMs?.maximum, 300000);
154
+ assert.match(bashTool?.inputSchema?.properties?.processId?.description ?? "", /action=process/);
155
+ assert.match(bashTool?.inputSchema?.properties?.interrupt?.description ?? "", /SIGINT/);
154
156
  const openWorkspaceTool = tools.find((tool) => tool.name === "open_workspace");
155
157
  assert.ok(openWorkspaceTool?.inputSchema?.properties?.workspaceId);
156
158
  assert.ok(openWorkspaceTool?.inputSchema?.properties?.newWorkspace);
@@ -158,14 +160,16 @@ try {
158
160
  assert.ok(openWorkspaceTool?.outputSchema?.properties?.capabilityFingerprint);
159
161
  assert.ok(openWorkspaceTool?.outputSchema?.properties?.capabilityCatalog);
160
162
  assert.ok(openWorkspaceTool?.outputSchema?.properties?.capabilityGuides);
161
- const templateUri = bashTool?._meta?.ui?.resourceUri;
163
+ const capabilityTool = tools.find((tool) => tool.name === "capability");
164
+ assert.deepEqual(capabilityTool?._meta?.["openai/fileParams"], ["file"]);
165
+ const templateUri = capabilityTool?._meta?.ui?.resourceUri;
162
166
  assert.match(
163
167
  templateUri ?? "",
164
168
  /^ui:\/\/forgerelay\/workspace-app-[0-9a-f]{12}\.html$/,
165
169
  JSON.stringify(bashTool ?? {}),
166
170
  );
167
- assert.deepEqual(bashTool?._meta?.ui?.visibility, ["model", "app"]);
168
- assert.equal(bashTool?._meta?.["openai/outputTemplate"], templateUri);
171
+ assert.deepEqual(capabilityTool?._meta?.ui?.visibility, ["model", "app"]);
172
+ assert.equal(capabilityTool?._meta?.["openai/outputTemplate"], templateUri);
169
173
  pass("MCP tools/list", `${toolNames.length} tools: ${toolNames.join(", ")}`);
170
174
 
171
175
  const resources = mcpRequest(oauth.accessToken, sessionId, {
@@ -234,15 +238,20 @@ try {
234
238
  "worktree.managed",
235
239
  "filesystem.rename-move",
236
240
  "filesystem.delete",
237
- "process.write-stdin",
241
+ "process.lifecycle",
238
242
  "hooks.lifecycle",
239
243
  "capability-guides.read",
240
- "inspection.search-tools",
244
+ ...(process.platform === "linux" ? ["artifact.native-download"] : []),
241
245
  "ui.mcp-app",
246
+ "review.changes",
242
247
  ],
243
248
  });
244
249
  const capabilityCatalog = opened.structuredContent.capabilityCatalog;
245
- assert.deepEqual(capabilityCatalog.map((entry) => entry.name), ["hooks.check"]);
250
+ assert.deepEqual(capabilityCatalog.map((entry) => entry.name), [
251
+ "hooks.check",
252
+ "review.changes",
253
+ ...(process.platform === "linux" ? ["artifact.download"] : []),
254
+ ]);
246
255
  assert.equal(capabilityCatalog[0].available, true);
247
256
  assert.equal(capabilityCatalog[0].guide.name, "lifecycle-hooks");
248
257
  const directCapability = callTool(oauth.accessToken, sessionId, 79, "capability", {
@@ -261,6 +270,18 @@ try {
261
270
  assert.equal(describedCapability.isError, undefined);
262
271
  assert.equal(describedCapability.structuredContent.capability.guide.name, "lifecycle-hooks");
263
272
  assert.equal(describedCapability.structuredContent.capability.inputSchema.type, "object");
273
+ if (process.platform === "linux") {
274
+ const describedArtifact = callTool(oauth.accessToken, sessionId, 82, "capability", {
275
+ workspaceId,
276
+ name: "artifact.download",
277
+ action: "describe",
278
+ });
279
+ assert.equal(describedArtifact.isError, undefined);
280
+ assert.deepEqual(describedArtifact.structuredContent.capability.transport, {
281
+ nativeFileArgument: "file",
282
+ gatewayParameter: "file",
283
+ });
284
+ }
264
285
  const unknownCapability = callTool(oauth.accessToken, sessionId, 81, "capability", {
265
286
  workspaceId,
266
287
  name: "unknown.capability",
@@ -273,6 +294,7 @@ try {
273
294
  assert.deepEqual(capabilityGuides.map((guide) => guide.name), [
274
295
  "lifecycle-hooks",
275
296
  "managed-worktrees",
297
+ "artifacts-review",
276
298
  "host-integration",
277
299
  "shell-processes",
278
300
  ]);
@@ -298,6 +320,19 @@ try {
298
320
  assert.match(read.structuredContent.result, /forgerelay 7677 acceptance/);
299
321
  pass("write + read", JSON.stringify(read.structuredContent));
300
322
 
323
+ const reviewed = callTool(oauth.accessToken, sessionId, 83, "capability", {
324
+ workspaceId,
325
+ name: "review.changes",
326
+ action: "run",
327
+ arguments: {},
328
+ });
329
+ assert.equal(reviewed.isError, undefined);
330
+ assert.match(reviewed.structuredContent.result.result, /Changed 1 file/);
331
+ assert.equal(reviewed._meta?.tool, "capability");
332
+ assert.equal(reviewed._meta?.card?.capabilityName, "review.changes");
333
+ assert.match(reviewed._meta?.card?.payload?.patch ?? "", /acceptance\.txt/);
334
+ pass("review.changes", "Capability Gateway produced the aggregate review card");
335
+
301
336
  const shell = callTool(oauth.accessToken, sessionId, 6, "bash", {
302
337
  workspaceId,
303
338
  command: "printf debug-bash-ok",
@@ -306,6 +341,25 @@ try {
306
341
  assert.equal(shell.structuredContent.running, false);
307
342
  pass("bash", "foreground command completed through ProcessManager");
308
343
 
344
+ const background = callTool(oauth.accessToken, sessionId, 61, "bash", {
345
+ workspaceId,
346
+ action: "run",
347
+ command: `${JSON.stringify(process.execPath)} -e "setTimeout(() => console.log('debug-process-ok'), 100)"`,
348
+ yieldTimeMs: 0,
349
+ });
350
+ assert.equal(background.structuredContent.running, true);
351
+ assert.equal(typeof background.structuredContent.processId, "number");
352
+ const polled = callTool(oauth.accessToken, sessionId, 62, "bash", {
353
+ workspaceId,
354
+ action: "process",
355
+ processId: background.structuredContent.processId,
356
+ yieldTimeMs: 5_000,
357
+ });
358
+ assert.equal(polled.structuredContent.running, false);
359
+ assert.equal(polled.structuredContent.exitCode, 0);
360
+ assert.match(polled.structuredContent.result, /debug-process-ok/);
361
+ pass("bash process", "action=run -> processId -> action=process completed through one MCP tool");
362
+
309
363
  const failedEdit = callTool(oauth.accessToken, sessionId, 7, "edit", {
310
364
  workspaceId,
311
365
  path: "acceptance.txt",
@@ -390,7 +444,7 @@ try {
390
444
  path: "feature.txt",
391
445
  content: "debug worktree acceptance\n",
392
446
  });
393
- const closed = callTool(oauth.accessToken, sessionId, 10, "close_worktree", {
447
+ const closed = callTool(oauth.accessToken, sessionId, 10, "close_workspace", {
394
448
  workspaceId: worktreeWorkspaceId,
395
449
  commitMessage: "test(debug): verify 7677 worktree lifecycle",
396
450
  });
@@ -401,7 +455,7 @@ try {
401
455
  "debug worktree acceptance\n",
402
456
  );
403
457
  pass(
404
- "managed worktree close",
458
+ "managed worktree workspace close",
405
459
  `${closed.structuredContent.branch} -> ${closed.structuredContent.targetBranch}`,
406
460
  );
407
461
 
@@ -734,6 +788,7 @@ function exerciseReleaseTagHooks(accessToken, sessionId) {
734
788
  assert.ok(existsSync(join(releaseProject, "release-ci-ran.txt")));
735
789
  assert.deepEqual(JSON.parse(readFileSync(join(releaseProject, "release-ci-ran.txt"), "utf8")), {
736
790
  tool: "bash",
791
+ action: "run",
737
792
  command: "git push origin v0.2.0",
738
793
  workingDirectory: ".",
739
794
  originalCommand: "git status --short && git push origin v0.2.0 && echo release-pushed",