@akira-tl/forgerelay 0.3.5 → 0.3.7
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 +29 -0
- package/capabilities/shell-processes/GUIDE.md +2 -2
- package/dist/db/migrations.js +19 -0
- package/dist/db/schema.js +9 -0
- package/dist/logger.js +16 -0
- package/dist/mcp-sessions.js +30 -0
- package/dist/oauth-provider.js +9 -0
- package/dist/process-sessions.js +98 -16
- package/dist/review-checkpoints.js +36 -1
- package/dist/server.js +234 -26
- package/dist/workspace-store.js +148 -20
- package/dist/workspaces.js +339 -111
- package/docs/chatgpt-coding-workflow.md +75 -12
- package/docs/configuration.md +47 -9
- package/docs/roadmap.md +23 -1
- package/package.json +2 -2
- package/scripts/debug/accept.mjs +60 -3
|
@@ -18,6 +18,60 @@ existing ID explicitly when the user wants to resume that logical workspace.
|
|
|
18
18
|
A Git worktree directory is a separate physical workspace target from its source
|
|
19
19
|
checkout, and each conversation can still have its own logical handle for it.
|
|
20
20
|
|
|
21
|
+
### Bootstrap context and workspace inventory
|
|
22
|
+
|
|
23
|
+
Normal coding still starts with the shortest path:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
open_workspace(path="~/project")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The default `context="auto"` keeps the first useful bootstrap while avoiding
|
|
30
|
+
replay. ForgeRelay tracks delivered context by conversation plus canonical
|
|
31
|
+
workspace target and a content fingerprint, not by logical `workspaceId`. If the
|
|
32
|
+
same conversation opens or resumes another logical handle for the same physical
|
|
33
|
+
project and the fingerprint is unchanged, the response keeps only lightweight
|
|
34
|
+
workspace metadata. If loaded instruction contents or relevant Skill, Capability
|
|
35
|
+
guide, profile, diagnostic, or nested-instruction metadata changes, the next
|
|
36
|
+
automatic open returns the refreshed bootstrap.
|
|
37
|
+
|
|
38
|
+
Two explicit controls are available for exceptional cases:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
open_workspace(workspaceId="ws_...", context="full")
|
|
42
|
+
open_workspace(workspaceId="ws_...", context="none")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`full` forces a bootstrap refresh. `none` opens/resumes the workspace without
|
|
46
|
+
returning the full project context and does not record the current fingerprint as
|
|
47
|
+
already delivered. Context-delivery state is independent from logical-workspace
|
|
48
|
+
selection, so closing or switching one handle does not make the conversation
|
|
49
|
+
forget unchanged project context it already received.
|
|
50
|
+
|
|
51
|
+
Do not enumerate workspace state on every normal open. When the user wants to
|
|
52
|
+
continue an earlier task, choose among logical workspaces, or clean up accumulated
|
|
53
|
+
state, use the same Core tool in inventory mode:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
open_workspace(action="list")
|
|
57
|
+
open_workspace(action="list", root="~/project")
|
|
58
|
+
open_workspace(action="list", staleOnly=true)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Inventory is paginated, defaults to 50 entries, and caps each page at 100. It can
|
|
62
|
+
filter by `workspaceId`, persisted `status`, derived `state`, `mode`, canonical
|
|
63
|
+
root/source root, or stale-only state. Entries include a compact label such as
|
|
64
|
+
`project/ws_...`, checkout/worktree backing metadata, timestamps, idle duration,
|
|
65
|
+
root validity, and whether that logical workspace is currently selected by this
|
|
66
|
+
conversation. Listing is observational and does not refresh `lastUsedAt`.
|
|
67
|
+
|
|
68
|
+
Treat persisted status and derived state separately. `status="active"` means the
|
|
69
|
+
record has not been explicitly closed. A valid recent record has `state="active"`;
|
|
70
|
+
a valid active record idle for more than two days has `state="stale"`; an active
|
|
71
|
+
record whose root is missing or unusable has `state="invalid"`; and an explicitly
|
|
72
|
+
finalized persisted record has `state="closed"`. Ask the user before cleanup, then
|
|
73
|
+
use the existing `close_workspace` lifecycle on the selected `workspaceId`.
|
|
74
|
+
|
|
21
75
|
## Checkout-first behavior
|
|
22
76
|
|
|
23
77
|
Checkout mode is the default:
|
|
@@ -103,10 +157,16 @@ CLAUDE.md
|
|
|
103
157
|
CLAUDE.MD
|
|
104
158
|
```
|
|
105
159
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
`
|
|
109
|
-
|
|
160
|
+
To keep broad workspaces such as `~` fast, initial nested-instruction discovery is
|
|
161
|
+
bounded to direct child directories instead of recursively walking the whole tree.
|
|
162
|
+
Deeper `AGENTS.md` / `CLAUDE.md` files are discovered lazily along a path the first
|
|
163
|
+
time the Agent accesses it, and already-scanned directories are cached for the life
|
|
164
|
+
of that workspace handle. A `read` result carries any newly discovered local
|
|
165
|
+
instructions before the requested file content. Side-effecting file tools and shell
|
|
166
|
+
commands discover instructions before execution; if new local instructions are
|
|
167
|
+
found, ForgeRelay returns them and requires the Agent to retry, so the side effect
|
|
168
|
+
does not occur before the relevant instructions are known. `FORGERELAY_AGENT_DIR`
|
|
169
|
+
is not an instruction source; it remains only a compatibility skill-discovery path.
|
|
110
170
|
|
|
111
171
|
## MCP capability loading
|
|
112
172
|
|
|
@@ -131,8 +191,9 @@ worktrees, subagents, artifact/change-review workflows, Host/OAuth/MCP App
|
|
|
131
191
|
integration, and long-running shell/PTY/process behavior. Optional guides are
|
|
132
192
|
advertised only when their feature is enabled; for example, disabled subagents
|
|
133
193
|
and artifact/change-review features do not add those descriptors to bootstrap
|
|
134
|
-
context.
|
|
135
|
-
|
|
194
|
+
context. Bootstrap replay follows the conversation/canonical-target context
|
|
195
|
+
fingerprint described above, so changing logical `workspaceId` alone does not
|
|
196
|
+
repeat unchanged descriptors; previously advertised guides remain valid.
|
|
136
197
|
|
|
137
198
|
The fingerprint is also a stale-Host-schema diagnostic. If `open_workspace`
|
|
138
199
|
reports a capability such as `filesystem.rename-move` but the Host's current
|
|
@@ -223,12 +284,14 @@ Workspace IDs are logical conversation handles rather than physical-directory
|
|
|
223
284
|
identities. The same conversation keeps a stable ID for a project, while another
|
|
224
285
|
conversation normally receives a different ID pointing at the same checkout or
|
|
225
286
|
worktree. `open_workspace` can explicitly resume a known `workspaceId`, and a
|
|
226
|
-
fresh logical ID is created only when the user asks for one.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
287
|
+
fresh logical ID is created only when the user asks for one. The normal open path
|
|
288
|
+
may still include `staleWorkspaces` as a passive reminder for same-target handles
|
|
289
|
+
idle for more than two days; use `open_workspace(action="list")` for complete,
|
|
290
|
+
filtered inventory when continuation or cleanup actually requires it.
|
|
291
|
+
`close_workspace` is the single public close operation: checkout-backed workspaces
|
|
292
|
+
release the logical handle, while managed-worktree-backed workspaces require
|
|
293
|
+
`commitMessage` and run the safe commit / fast-forward-only integration / cleanup
|
|
294
|
+
lifecycle.
|
|
232
295
|
|
|
233
296
|
Shell commands are allowed to modify ordinary project files when that is a
|
|
234
297
|
natural part of the user's requested development task; ForgeRelay does not apply
|
package/docs/configuration.md
CHANGED
|
@@ -163,12 +163,43 @@ receives a different ID for the same physical checkout/worktree. Pass
|
|
|
163
163
|
`workspaceId` to `open_workspace` to explicitly resume an existing handle in the
|
|
164
164
|
current conversation. `newWorkspace: true` allocates a new logical handle without
|
|
165
165
|
creating another checkout or Git worktree and should be used only on explicit user
|
|
166
|
-
request.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
166
|
+
request.
|
|
167
|
+
|
|
168
|
+
Bootstrap delivery is tracked separately from the selected logical workspace.
|
|
169
|
+
`open_workspace` defaults to `context="auto"`: ForgeRelay fingerprints the current
|
|
170
|
+
project context and returns the full AGENTS/Skills/Capability-guide/profile bootstrap
|
|
171
|
+
only when that conversation has not already received the current fingerprint for
|
|
172
|
+
the canonical workspace target. `context="full"` forces a refresh;
|
|
173
|
+
`context="none"` opens or resumes the logical workspace without returning the full
|
|
174
|
+
bootstrap and does not mark the current fingerprint as delivered. Closing or
|
|
175
|
+
switching a logical workspace therefore does not by itself cause unchanged project
|
|
176
|
+
context to be injected again, while changed context produces a new fingerprint and
|
|
177
|
+
is delivered on the next `auto` open.
|
|
178
|
+
|
|
179
|
+
Use `open_workspace(action="list")` only when the Agent needs to continue an older
|
|
180
|
+
logical workspace, choose among multiple handles, or organize workspace state. The
|
|
181
|
+
inventory is paginated (50 records by default, at most 100) and can filter by
|
|
182
|
+
workspace ID, persisted status, derived state, mode, canonical root/source root, or
|
|
183
|
+
stale-only state. Reading inventory does not refresh `lastUsedAt`. Persisted
|
|
184
|
+
`status="active"` means the record has not been explicitly closed; the derived
|
|
185
|
+
`state` distinguishes `active`, `stale`, `invalid`, and `closed`. A missing checkout
|
|
186
|
+
or externally removed managed-worktree root can therefore remain diagnostically
|
|
187
|
+
`status="active"` while appearing as `state="invalid"`. The existing
|
|
188
|
+
`staleWorkspaces` field remains a passive same-workspace reminder for old handles;
|
|
189
|
+
`action="list"` is the formal on-demand inventory path.
|
|
190
|
+
|
|
191
|
+
`close_workspace` removes a checkout-backed logical handle without deleting checkout
|
|
192
|
+
files. For a managed-worktree-backed workspace, `close_workspace` requires
|
|
193
|
+
`commitMessage` and runs the existing safe worktree finalize lifecycle: close Hooks,
|
|
194
|
+
commit when needed, fast-forward-only integration, cleanup, and alias invalidation.
|
|
195
|
+
|
|
196
|
+
Hot workspace/session activity timestamps are coalesced in memory and flushed to the
|
|
197
|
+
SQLite state database in a transaction at most every five minutes; normal shutdown
|
|
198
|
+
performs a final explicit flush. Reads within the running ForgeRelay process see the
|
|
199
|
+
latest in-memory timestamps immediately. Workspace creation, close/status changes,
|
|
200
|
+
context-delivery checkpoints, and other semantic state transitions remain immediate
|
|
201
|
+
persistent writes. A hard process crash may therefore lose only the most recent
|
|
202
|
+
activity timestamp window, not the existence or closed/open state of a workspace.
|
|
172
203
|
|
|
173
204
|
Regular `bash` has no execution-timeout input. `action="run"` (the default) waits
|
|
174
205
|
in the foreground for at most 300 seconds; if the process is still alive, the
|
|
@@ -176,7 +207,10 @@ result contains `running: true` and a canonical `processId`. Reuse the same `bas
|
|
|
176
207
|
with `action="process"` to poll/wait, send `input`, resize a PTY, or set
|
|
177
208
|
`interrupt:true`; each wait can be up to 300 seconds. ForgeRelay does not kill a
|
|
178
209
|
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.
|
|
210
|
+
delivered once with a later tool result for the same logical workspace ID. An
|
|
211
|
+
unconsumed completed-process notice is retained for at most five minutes, and
|
|
212
|
+
ForgeRelay also bounds active and completed process counts so repeated background
|
|
213
|
+
commands cannot grow server memory without limit.
|
|
180
214
|
|
|
181
215
|
Codex mode retains `write_stdin` only as an experimental compatibility adapter;
|
|
182
216
|
regular Agent workflows should use the single `bash` process lifecycle.
|
|
@@ -325,8 +359,12 @@ Arrays or empty values are not accepted. Symbolic links are followed, so the
|
|
|
325
359
|
runtime entry may point at a canonical source elsewhere on disk.
|
|
326
360
|
|
|
327
361
|
Project-root `AGENTS.md` / `CLAUDE.md` files remain project context and are
|
|
328
|
-
loaded separately.
|
|
329
|
-
|
|
362
|
+
loaded separately. Initial nested-instruction discovery checks only direct child
|
|
363
|
+
directories; deeper instruction files are discovered lazily when a workspace path
|
|
364
|
+
is first accessed. Reads surface newly discovered instructions inline, while
|
|
365
|
+
side-effecting file/shell operations stop before execution and require a retry if
|
|
366
|
+
that access discovers new local instructions. `FORGERELAY_AGENT_DIR` does not
|
|
367
|
+
select a global instruction file; it remains a compatibility path for Agent Skills.
|
|
330
368
|
|
|
331
369
|
## Skills and subagents
|
|
332
370
|
|
package/docs/roadmap.md
CHANGED
|
@@ -170,7 +170,29 @@ capability
|
|
|
170
170
|
- 删除已经完成迁移的 dedicated low-frequency tool aliases,确保新增 Capability 不再扩大常驻 tool count;
|
|
171
171
|
- 简化 fingerprint,使其用于版本/运行时能力摘要与 stale-Host 诊断,而不是重新枚举 tool implementation;
|
|
172
172
|
- 对 `open_workspace → catalog → capability describe/read/run`、managed worktree close、长进程 interaction、review/artifact capability、MCP App 与 stale-schema 情况做 7677 acceptance 和新 Host 会话验收;
|
|
173
|
-
- 0.3.5
|
|
173
|
+
- 0.3.5 完成 canonical MCP surface 与 fresh-Host 主验收后,0.3 的 progressive-disclosure 主体设计视为稳定;后续 0.3.x 只接收验收暴露出的兼容性或 lifecycle 补丁,0.4 仍回到原定 LSP code intelligence v1。
|
|
174
|
+
|
|
175
|
+
### 0.3.6 — Workspace bootstrap 与 inventory 补丁
|
|
176
|
+
|
|
177
|
+
0.3.6 处理 0.3.5 fresh-Host 验收后暴露的 Workspace 上下文与 logical-workspace 管理问题,不增加新的常驻 MCP tool,也不改变 0.4 的 LSP 主路线:
|
|
178
|
+
|
|
179
|
+
- `open_workspace` 的 bootstrap 去重从 logical `workspaceId` 身份提升为 conversation scope + canonical workspace target + context fingerprint;同一 conversation 切换到同一物理 checkout/worktree 的其他 logical workspace 时,只要 AGENTS、Skills、Capability guide/profile 等相关上下文没有变化,就不重复注入完整 bootstrap;
|
|
180
|
+
- `open_workspace` 提供 `context="auto" | "full" | "none"`。`auto` 默认只在当前 fingerprint 尚未交付或已变化时返回完整上下文,`full` 强制刷新,`none` 只取得 workspace handle/metadata 且不会把该 fingerprint 标记为已交付;
|
|
181
|
+
- context-delivery 状态与 conversation 当前绑定的 logical workspace 分离持久化,因此关闭或切换 logical handle 不会让 Agent 在同一 conversation 中忘记已经收到的项目上下文;delivery 状态继续服从有限生命周期清理,而不是永久缓存;
|
|
182
|
+
- `open_workspace(action="list")` 成为按需 logical-workspace inventory 入口,不增加 `list_workspaces`/`workspace.list` 第十个 Core tool;普通开发仍直接使用默认 `action="open"`,只有续接旧任务、选择 workspace 或整理状态时才读取 inventory;
|
|
183
|
+
- inventory 区分持久化 `status` 与派生 `state`:`status="active"` 表示尚未显式关闭,`state` 再区分 active、stale、invalid 与 closed;missing root 或外部删除的 managed worktree 可以保持可诊断的 active record,同时显示为 invalid;
|
|
184
|
+
- inventory 查看本身不刷新 workspace `lastUsedAt`,支持过滤与分页,并继续让现有 `close_workspace` 承担用户确认后的实际清理/finalize lifecycle。
|
|
185
|
+
|
|
186
|
+
### 0.3.7 — 资源生命周期与 Workspace I/O 性能补丁
|
|
187
|
+
|
|
188
|
+
0.3.7 处理长时间运行实例在高请求量、高输出 `bash`、大量 logical workspace 与宽根目录下的资源放大问题,不改变 canonical 9-tool surface:
|
|
189
|
+
|
|
190
|
+
- completed background process 的 Agent 可消费状态最多保留 5 分钟;底层 ChildProcess/PTY handle 在退出时立即释放,completed notice 数量与 active process 数量都有硬上限,并缩小单 process 输出驻留预算;
|
|
191
|
+
- 高输出 head/tail buffer 保留 Unicode code-point 语义,但不再通过 `Array.from(整段输出)` 构造巨型临时数组,降低 V8 heap 扩容与 GC 压力;
|
|
192
|
+
- MCP transport registry 与 review checkpoint state 加入容量边界;正常 transport close/workspace close 仍立即释放,异常遗弃对象不能再无限累积;OAuth 过期 authorization code 也会主动淘汰;
|
|
193
|
+
- `open_workspace` 的 instruction discovery 首轮只检查 root 与直接子目录,不再递归整棵 workspace。更深层 `AGENTS.md` / `CLAUDE.md` 在 Agent 首次访问对应路径时沿祖先目录惰性发现,并缓存已扫描目录;read 可直接携带新发现指令,write/edit/rename/delete/bash 等副作用调用则在执行前返回指令并要求重试;
|
|
194
|
+
- Workspace SQLite 继续作为本地持久化真源,不引入 Redis/PostgreSQL/Docker。高频 session/conversation `lastUsedAt` touch 进入内存 write-behind cache,最多每 5 分钟事务批量 flush,normal shutdown 再显式 flush;create/close/status 等语义性状态仍同步持久化;
|
|
195
|
+
- debug runtime telemetry 定期报告 RSS/heap、transport、process、workspace cache 与 review state 数量,为后续真实实例资源趋势提供可观测性。
|
|
174
196
|
|
|
175
197
|
必要安全语义始终留在 Core tool interface、Capability contract 或自动 Hook report 中;渐进式披露不能成为隐藏权限、隐式 autonomous workflow 或绕过 allowed roots/auth 的机制。`rename` 继续作为文件和目录 move/rename 的统一 primitive。
|
|
176
198
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
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",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"debug:accept": "node scripts/debug/accept.mjs",
|
|
43
43
|
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
|
|
44
44
|
"start": "node dist/cli.js serve",
|
|
45
|
-
"test": "tsx src/config.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
45
|
+
"test": "tsx src/config.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspace-store.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
46
46
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
47
47
|
"release:check": "node scripts/release-version.mjs check",
|
|
48
48
|
"release:tag-check": "node scripts/release-version.mjs tag",
|
package/scripts/debug/accept.mjs
CHANGED
|
@@ -223,13 +223,16 @@ try {
|
|
|
223
223
|
assert.equal(historicalTemplate.text, template.text);
|
|
224
224
|
pass("MCP app template", `${templateUri} + legacy/history compatibility -> ${scriptUrl}`);
|
|
225
225
|
|
|
226
|
+
const workspaceConversationMeta = { "openai/session": "acceptance-workspace" };
|
|
226
227
|
const opened = callTool(oauth.accessToken, sessionId, 3, "open_workspace", {
|
|
227
228
|
path: checkoutWorkspace,
|
|
228
|
-
});
|
|
229
|
+
}, workspaceConversationMeta);
|
|
229
230
|
const workspaceId = opened.structuredContent.workspaceId;
|
|
230
231
|
assert.match(workspaceId, /^ws_/);
|
|
232
|
+
assert.equal(opened.structuredContent.action, "open");
|
|
231
233
|
assert.equal(opened.structuredContent.root, checkoutWorkspace);
|
|
232
234
|
assert.equal(opened.structuredContent.mode, "checkout");
|
|
235
|
+
assert.equal(typeof opened.structuredContent.contextFingerprint, "string");
|
|
233
236
|
assert.deepEqual(opened.structuredContent.capabilityFingerprint, {
|
|
234
237
|
version: packageJson.version,
|
|
235
238
|
toolMode: "full",
|
|
@@ -254,6 +257,56 @@ try {
|
|
|
254
257
|
]);
|
|
255
258
|
assert.equal(capabilityCatalog[0].available, true);
|
|
256
259
|
assert.equal(capabilityCatalog[0].guide.name, "lifecycle-hooks");
|
|
260
|
+
|
|
261
|
+
const freshLogical = callTool(oauth.accessToken, sessionId, 84, "open_workspace", {
|
|
262
|
+
path: checkoutWorkspace,
|
|
263
|
+
newWorkspace: true,
|
|
264
|
+
}, workspaceConversationMeta);
|
|
265
|
+
const freshLogicalId = freshLogical.structuredContent.workspaceId;
|
|
266
|
+
assert.notEqual(freshLogicalId, workspaceId);
|
|
267
|
+
assert.equal(freshLogical.structuredContent.action, "open");
|
|
268
|
+
assert.equal(
|
|
269
|
+
freshLogical.structuredContent.contextFingerprint,
|
|
270
|
+
opened.structuredContent.contextFingerprint,
|
|
271
|
+
);
|
|
272
|
+
assert.equal(freshLogical.structuredContent.agentsFiles, undefined);
|
|
273
|
+
assert.equal(freshLogical.structuredContent.capabilityGuides, undefined);
|
|
274
|
+
|
|
275
|
+
const workspaceInventory = callTool(oauth.accessToken, sessionId, 85, "open_workspace", {
|
|
276
|
+
action: "list",
|
|
277
|
+
root: checkoutWorkspace,
|
|
278
|
+
}, workspaceConversationMeta);
|
|
279
|
+
assert.equal(workspaceInventory.structuredContent.action, "list");
|
|
280
|
+
assert.equal(workspaceInventory.structuredContent.summary.matching, 2);
|
|
281
|
+
const inventoryEntries = workspaceInventory.structuredContent.workspaces;
|
|
282
|
+
assert.equal(inventoryEntries.length, 2);
|
|
283
|
+
assert.equal(
|
|
284
|
+
inventoryEntries.find((entry) => entry.workspaceId === freshLogicalId)?.current,
|
|
285
|
+
true,
|
|
286
|
+
);
|
|
287
|
+
assert.equal(
|
|
288
|
+
inventoryEntries.find((entry) => entry.workspaceId === workspaceId)?.current,
|
|
289
|
+
false,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const closedFreshLogical = callTool(oauth.accessToken, sessionId, 86, "close_workspace", {
|
|
293
|
+
workspaceId: freshLogicalId,
|
|
294
|
+
});
|
|
295
|
+
assert.equal(closedFreshLogical.isError, undefined);
|
|
296
|
+
const resumedOriginal = callTool(oauth.accessToken, sessionId, 87, "open_workspace", {
|
|
297
|
+
workspaceId,
|
|
298
|
+
}, workspaceConversationMeta);
|
|
299
|
+
assert.equal(resumedOriginal.structuredContent.workspaceId, workspaceId);
|
|
300
|
+
assert.equal(resumedOriginal.structuredContent.agentsFiles, undefined);
|
|
301
|
+
assert.equal(
|
|
302
|
+
resumedOriginal.structuredContent.contextFingerprint,
|
|
303
|
+
opened.structuredContent.contextFingerprint,
|
|
304
|
+
);
|
|
305
|
+
pass(
|
|
306
|
+
"workspace context + inventory",
|
|
307
|
+
`${workspaceId} -> ${freshLogicalId} -> list -> close -> resume without bootstrap replay`,
|
|
308
|
+
);
|
|
309
|
+
|
|
257
310
|
const directCapability = callTool(oauth.accessToken, sessionId, 79, "capability", {
|
|
258
311
|
workspaceId,
|
|
259
312
|
name: "hooks.check",
|
|
@@ -590,12 +643,16 @@ function mcpRequest(accessToken, sessionId, request) {
|
|
|
590
643
|
return { response, message: parseMcpMessage(response.body, request.id) };
|
|
591
644
|
}
|
|
592
645
|
|
|
593
|
-
function callTool(accessToken, sessionId, id, name, args) {
|
|
646
|
+
function callTool(accessToken, sessionId, id, name, args, meta) {
|
|
594
647
|
const message = mcpRequest(accessToken, sessionId, {
|
|
595
648
|
jsonrpc: "2.0",
|
|
596
649
|
id,
|
|
597
650
|
method: "tools/call",
|
|
598
|
-
params: {
|
|
651
|
+
params: {
|
|
652
|
+
name,
|
|
653
|
+
arguments: args,
|
|
654
|
+
...(meta ? { _meta: meta } : {}),
|
|
655
|
+
},
|
|
599
656
|
}).message;
|
|
600
657
|
assert.equal(message.id, id);
|
|
601
658
|
assert.ok(message.result, `tool ${name} did not return a result`);
|