@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.
- package/CHANGELOG.md +24 -0
- package/README.md +2 -2
- package/capabilities/artifacts-review/GUIDE.md +2 -4
- package/capabilities/managed-worktrees/GUIDE.md +12 -7
- package/capabilities/shell-processes/GUIDE.md +10 -8
- package/dist/artifact-tools.js +0 -128
- package/dist/capabilities.js +4 -7
- package/dist/hooks.js +12 -0
- package/dist/mcp/server-instructions.js +7 -13
- package/dist/review-checkpoints.js +4 -4
- package/dist/server.js +217 -365
- package/dist/ui/.vite/manifest.json +280 -280
- package/dist/ui/assets/{angular-html-CQmCUfkv.js → angular-html-BT2Z6jee.js} +1 -1
- package/dist/ui/assets/{angular-ts-BSbfxpMS.js → angular-ts-BwPE8YDt.js} +1 -1
- package/dist/ui/assets/{apl-CWCaH-E3.js → apl-CGbONVvU.js} +1 -1
- package/dist/ui/assets/{astro-R_p4G0xA.js → astro-DIAPy7cj.js} +1 -1
- package/dist/ui/assets/{blade-irHensDY.js → blade-Cgtd19Ky.js} +1 -1
- package/dist/ui/assets/{c-_XEQUoq6.js → c-acnvZ8tm.js} +1 -1
- package/dist/ui/assets/{cobol-Css6VQjJ.js → cobol-G-7HLVKW.js} +1 -1
- package/dist/ui/assets/{coffee-CpXUkyDm.js → coffee-DQFyKxEB.js} +1 -1
- package/dist/ui/assets/{cpp-BQHsPWwI.js → cpp-UyrmHXan.js} +1 -1
- package/dist/ui/assets/{crystal-Cbv8J9Fm.js → crystal-DuiWhEO3.js} +1 -1
- package/dist/ui/assets/{css-D8DkrS3U.js → css-D_WCbY6m.js} +1 -1
- package/dist/ui/assets/{edge-bBL_XDAG.js → edge-COkalhFi.js} +1 -1
- package/dist/ui/assets/{elixir-CQjsM9sA.js → elixir-cdlzbYR1.js} +1 -1
- package/dist/ui/assets/{elm-DLFl-jEZ.js → elm-ZwDYuYpb.js} +1 -1
- package/dist/ui/assets/{erb-BJjsdB_Q.js → erb-C8WG9TtC.js} +1 -1
- package/dist/ui/assets/{git-rebase-CqP5A2s0.js → git-rebase-CMlDVJBT.js} +1 -1
- package/dist/ui/assets/{glimmer-js-B5Hh_7xe.js → glimmer-js-DeLOQlol.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-oaeSAC8s.js → glimmer-ts-DTEM04TK.js} +1 -1
- package/dist/ui/assets/{glsl-BylkOpcF.js → glsl-IJV4SB-M.js} +1 -1
- package/dist/ui/assets/{graphql-DsVRvP6S.js → graphql-PM-WDnoB.js} +1 -1
- package/dist/ui/assets/{hack-D4N5abT_.js → hack-C14sh6Lh.js} +1 -1
- package/dist/ui/assets/{haml-CIAJQIox.js → haml-B_5nS6Kx.js} +1 -1
- package/dist/ui/assets/{handlebars-DDEZyNXR.js → handlebars-eS-J0O-M.js} +1 -1
- package/dist/ui/assets/{heavy-payload-DafM8yjB.js → heavy-payload-BKB0QDOI.js} +1 -1
- package/dist/ui/assets/{html-zDy-3lvy.js → html-C3TPjrC5.js} +1 -1
- package/dist/ui/assets/{html-derivative-CZ91Y2d5.js → html-derivative-BlVZU2id.js} +1 -1
- package/dist/ui/assets/{http-ezaodZRf.js → http-BmbZzA5x.js} +1 -1
- package/dist/ui/assets/{hurl-BmL0o7TO.js → hurl-Dnd_YpOD.js} +1 -1
- package/dist/ui/assets/{java-Q2C0vwbH.js → java-ne9HdIVn.js} +1 -1
- package/dist/ui/assets/{javascript-UDhBkHE3.js → javascript-DpHT58rr.js} +1 -1
- package/dist/ui/assets/{jinja-BWRskcYS.js → jinja-DTvkZdHA.js} +1 -1
- package/dist/ui/assets/{jison-B18BY9Ds.js → jison-DUJG-tmh.js} +1 -1
- package/dist/ui/assets/{json-DNj7MpRr.js → json-DJmnBdxW.js} +1 -1
- package/dist/ui/assets/{jsx-CVpHJ6cr.js → jsx-BclJnZnN.js} +1 -1
- package/dist/ui/assets/{julia-CJBNBS9c.js → julia-P5B4pHFa.js} +1 -1
- package/dist/ui/assets/{just-Bmu2JCQT.js → just-DYaoh7C6.js} +1 -1
- package/dist/ui/assets/{latex-BB7F93KY.js → latex-CI5lnkWV.js} +1 -1
- package/dist/ui/assets/{liquid-Btnomc6E.js → liquid-D3LnAs_z.js} +1 -1
- package/dist/ui/assets/{lua-D0FYT9Vo.js → lua-dZC7ZQIY.js} +1 -1
- package/dist/ui/assets/{marko-MHLw0ZlK.js → marko-CRDrjqfS.js} +1 -1
- package/dist/ui/assets/{mdc-CSr-suGy.js → mdc-ZzwgmJVn.js} +1 -1
- package/dist/ui/assets/{nginx-ur1l6E-g.js → nginx-BAvFXGSV.js} +1 -1
- package/dist/ui/assets/{nim-B2k0IugA.js → nim-BHIFawe9.js} +1 -1
- package/dist/ui/assets/{perl-BqOHxFDn.js → perl-C_f5AnLa.js} +1 -1
- package/dist/ui/assets/{php-DzPK6O1_.js → php-DDqpegL5.js} +1 -1
- package/dist/ui/assets/{pug-D169OZkr.js → pug-CSaAZXyd.js} +1 -1
- package/dist/ui/assets/{qml-BtemJTC4.js → qml-CNHvRs_z.js} +1 -1
- package/dist/ui/assets/{r-BGVHu0bd.js → r-CAfoQND2.js} +1 -1
- package/dist/ui/assets/{razor-Diisjg1d.js → razor-BDfAPy7I.js} +1 -1
- package/dist/ui/assets/{regexp-B2F5vMZ6.js → regexp-Mug5jltQ.js} +1 -1
- package/dist/ui/assets/{review-payload-B-3I3Oj_.js → review-payload-D7GIdYDj.js} +1 -1
- package/dist/ui/assets/{rst-BxpeZwuT.js → rst-BzPP2gle.js} +1 -1
- package/dist/ui/assets/{ruby-DrCq4riN.js → ruby-CQjzokzX.js} +1 -1
- package/dist/ui/assets/{sas-CI_2W-1n.js → sas-Bp37KGr7.js} +1 -1
- package/dist/ui/assets/{scrollbar-DHsDF5w-.js → scrollbar-HUMBdLLK.js} +3 -3
- package/dist/ui/assets/{scss-BxtyHZ1p.js → scss-C_Z1DYnw.js} +1 -1
- package/dist/ui/assets/{shellscript-BY_YYrHT.js → shellscript-CLfJpn3I.js} +1 -1
- package/dist/ui/assets/{shellsession-C6uBrLmA.js → shellsession-CNc62UhY.js} +1 -1
- package/dist/ui/assets/{soy-BHM9gdPh.js → soy-CF44SpBo.js} +1 -1
- package/dist/ui/assets/{sql-XmhfEGz_.js → sql-CYnzfiQ1.js} +1 -1
- package/dist/ui/assets/{stata-BbHY7NEo.js → stata-DVeMA68W.js} +1 -1
- package/dist/ui/assets/{surrealql-CD49DZ3E.js → surrealql-52sF96Yv.js} +1 -1
- package/dist/ui/assets/{svelte-CbYsEYJY.js → svelte-ydwT7UbF.js} +1 -1
- package/dist/ui/assets/{templ-B5JoUxEB.js → templ-BXcoVoGh.js} +1 -1
- package/dist/ui/assets/{tex-DZcOfTZK.js → tex-COsYFp7O.js} +1 -1
- package/dist/ui/assets/{ts-tags-BeU-xqc0.js → ts-tags-CF--LE1s.js} +1 -1
- package/dist/ui/assets/{tsx-V7W1xGl3.js → tsx-DLBt2-2f.js} +1 -1
- package/dist/ui/assets/{twig-MIaJo5BU.js → twig-YoM-DiI5.js} +1 -1
- package/dist/ui/assets/{typescript-C0WESuVc.js → typescript-Ce9JGkNI.js} +1 -1
- package/dist/ui/assets/{vue-eJHwc9ra.js → vue-B5LDEy4l.js} +1 -1
- package/dist/ui/assets/{vue-html-CLmBujTU.js → vue-html-CPdGGk4T.js} +1 -1
- package/dist/ui/assets/{vue-vine-BP5kjIGb.js → vue-vine-B5bjgk0a.js} +1 -1
- package/dist/ui/assets/{workspace-app-C1qR5cAY.js → workspace-app-Cm9EX5x4.js} +3 -3
- package/dist/ui/assets/{xml-j7pmyP48.js → xml-Cun8xb0T.js} +1 -1
- package/dist/ui/assets/{xsl-5VFHbg6o.js → xsl-wls_CEK_.js} +1 -1
- package/dist/ui/assets/{yaml-DxFAt-KU.js → yaml-BxuXn9OL.js} +1 -1
- package/dist/ui/workspace-app.html +1 -1
- package/docs/artifact-exchange.md +4 -3
- package/docs/chatgpt-coding-workflow.md +31 -27
- package/docs/configuration.md +27 -20
- package/docs/debugging.md +3 -3
- package/docs/gotchas.md +9 -8
- package/docs/security.md +6 -5
- package/package.json +1 -1
- package/scripts/debug/accept.mjs +80 -25
package/docs/configuration.md
CHANGED
|
@@ -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` |
|
|
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
|
|
119
|
-
| `full` |
|
|
120
|
-
| `codex` | Experimental Codex-shaped
|
|
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
|
-
|
|
127
|
-
|
|
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 `
|
|
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
|
|
146
|
-
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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
|
|
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`
|
|
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 `
|
|
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
|
-
## `
|
|
168
|
+
## `close_workspace` refuses to finalize a managed worktree
|
|
169
169
|
|
|
170
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 `
|
|
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
|
|
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.
|
|
145
|
-
poll, wait,
|
|
146
|
-
|
|
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, `
|
|
183
|
-
provided by the MCP host
|
|
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
package/scripts/debug/accept.mjs
CHANGED
|
@@ -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: "
|
|
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:
|
|
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
|
-
|
|
133
|
-
|
|
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 ?? "", /
|
|
143
|
-
assert.
|
|
144
|
-
|
|
145
|
-
assert.
|
|
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
|
|
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(
|
|
168
|
-
assert.equal(
|
|
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.
|
|
241
|
+
"process.lifecycle",
|
|
238
242
|
"hooks.lifecycle",
|
|
239
243
|
"capability-guides.read",
|
|
240
|
-
"
|
|
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), [
|
|
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, "
|
|
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",
|