@akira-tl/forgerelay 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/capabilities/artifacts-review/GUIDE.md +2 -4
- package/dist/artifact-tools.js +0 -128
- package/dist/capabilities.js +2 -5
- package/dist/db/migrations.js +19 -0
- package/dist/db/schema.js +9 -0
- package/dist/hooks.js +12 -0
- package/dist/mcp/server-instructions.js +4 -9
- package/dist/review-checkpoints.js +4 -4
- package/dist/server.js +154 -280
- 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/dist/workspace-store.js +52 -1
- package/dist/workspaces.js +209 -29
- package/docs/artifact-exchange.md +4 -3
- package/docs/chatgpt-coding-workflow.md +70 -11
- package/docs/configuration.md +41 -16
- package/docs/debugging.md +2 -2
- package/docs/gotchas.md +3 -3
- package/docs/roadmap.md +12 -1
- package/docs/security.md +3 -2
- package/package.json +1 -1
- package/scripts/debug/accept.mjs +113 -15
package/dist/workspaces.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { mkdir, opendir, readFile, realpath, stat } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
@@ -27,8 +27,9 @@ export class WorkspaceRegistry {
|
|
|
27
27
|
async openWorkspace(input, openOptions = {}) {
|
|
28
28
|
this.pruneIdleWorkspaceSessions(openOptions.protectedWorkspaceIds ?? new Set());
|
|
29
29
|
const workspaceInput = typeof input === "string" ? { path: input } : input;
|
|
30
|
+
const bootstrapContext = workspaceInput.context ?? "auto";
|
|
30
31
|
if (workspaceInput.workspaceId) {
|
|
31
|
-
return this.resumeWorkspace(workspaceInput.workspaceId, openOptions.conversationScopeId);
|
|
32
|
+
return this.resumeWorkspace(workspaceInput.workspaceId, openOptions.conversationScopeId, bootstrapContext);
|
|
32
33
|
}
|
|
33
34
|
if (!workspaceInput.path) {
|
|
34
35
|
throw new Error("open_workspace requires either path or workspaceId.");
|
|
@@ -37,24 +38,138 @@ export class WorkspaceRegistry {
|
|
|
37
38
|
if (mode === "worktree") {
|
|
38
39
|
return this.openReusableWorktree(workspaceInput, openOptions.conversationScopeId);
|
|
39
40
|
}
|
|
40
|
-
return this.openReusableCheckout(workspaceInput.path, openOptions.conversationScopeId, workspaceInput.newWorkspace ?? false);
|
|
41
|
+
return this.openReusableCheckout(workspaceInput.path, openOptions.conversationScopeId, workspaceInput.newWorkspace ?? false, bootstrapContext);
|
|
41
42
|
}
|
|
42
|
-
async
|
|
43
|
+
async listWorkspaces(input = {}, openOptions = {}) {
|
|
44
|
+
this.pruneIdleWorkspaceSessions(openOptions.protectedWorkspaceIds ?? new Set());
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const sessions = this.store
|
|
47
|
+
? this.store.listSessions()
|
|
48
|
+
: [...this.workspaces.values()].map((workspace) => ({
|
|
49
|
+
id: workspace.id,
|
|
50
|
+
root: workspace.root,
|
|
51
|
+
status: "active",
|
|
52
|
+
mode: workspace.mode,
|
|
53
|
+
sourceRoot: workspace.sourceRoot,
|
|
54
|
+
baseRef: workspace.worktree?.baseRef,
|
|
55
|
+
baseSha: workspace.worktree?.baseSha,
|
|
56
|
+
branch: workspace.worktree?.branch,
|
|
57
|
+
targetBranch: workspace.worktree?.targetBranch,
|
|
58
|
+
managed: workspace.worktree?.managed ?? false,
|
|
59
|
+
createdAt: "",
|
|
60
|
+
lastUsedAt: "",
|
|
61
|
+
}));
|
|
62
|
+
const currentWorkspaceIds = new Set(openOptions.conversationScopeId && this.store
|
|
63
|
+
? this.store
|
|
64
|
+
.listConversationBindings()
|
|
65
|
+
.filter((binding) => binding.conversationScopeId === openOptions.conversationScopeId)
|
|
66
|
+
.map((binding) => binding.workspaceSessionId)
|
|
67
|
+
: []);
|
|
68
|
+
const rootKey = input.root
|
|
69
|
+
? await canonicalPath(assertAllowedPath(input.root, [...this.config.allowedRoots, this.config.worktreeRoot]))
|
|
70
|
+
: undefined;
|
|
71
|
+
const entries = await Promise.all(sessions.map(async (session) => {
|
|
72
|
+
const rootValid = await this.validSessionRoot(session) !== undefined;
|
|
73
|
+
const lastUsedAt = Date.parse(session.lastUsedAt);
|
|
74
|
+
const idleMs = Number.isFinite(lastUsedAt) ? Math.max(0, now - lastUsedAt) : 0;
|
|
75
|
+
const state = session.status !== "active"
|
|
76
|
+
? "closed"
|
|
77
|
+
: !rootValid
|
|
78
|
+
? "invalid"
|
|
79
|
+
: idleMs >= WORKSPACE_STALE_REMINDER_MS
|
|
80
|
+
? "stale"
|
|
81
|
+
: "active";
|
|
82
|
+
const projectRoot = session.sourceRoot ?? session.root;
|
|
83
|
+
return {
|
|
84
|
+
label: `${basename(resolve(projectRoot)) || "workspace"}/${session.id}`,
|
|
85
|
+
workspaceId: session.id,
|
|
86
|
+
root: session.root,
|
|
87
|
+
status: session.status,
|
|
88
|
+
state,
|
|
89
|
+
mode: session.mode,
|
|
90
|
+
sourceRoot: session.sourceRoot,
|
|
91
|
+
branch: session.branch,
|
|
92
|
+
targetBranch: session.targetBranch,
|
|
93
|
+
managed: session.managed,
|
|
94
|
+
createdAt: session.createdAt,
|
|
95
|
+
lastUsedAt: session.lastUsedAt,
|
|
96
|
+
idleMs,
|
|
97
|
+
rootValid,
|
|
98
|
+
current: currentWorkspaceIds.has(session.id),
|
|
99
|
+
};
|
|
100
|
+
}));
|
|
101
|
+
const filtered = [];
|
|
102
|
+
for (let index = 0; index < sessions.length; index += 1) {
|
|
103
|
+
const session = sessions[index];
|
|
104
|
+
const entry = entries[index];
|
|
105
|
+
if (!session || !entry)
|
|
106
|
+
continue;
|
|
107
|
+
if (input.workspaceId && entry.workspaceId !== input.workspaceId)
|
|
108
|
+
continue;
|
|
109
|
+
if (input.status && entry.status !== input.status)
|
|
110
|
+
continue;
|
|
111
|
+
if (input.state && entry.state !== input.state)
|
|
112
|
+
continue;
|
|
113
|
+
if (input.mode && entry.mode !== input.mode)
|
|
114
|
+
continue;
|
|
115
|
+
if (input.staleOnly && entry.state !== "stale")
|
|
116
|
+
continue;
|
|
117
|
+
if (rootKey) {
|
|
118
|
+
const sessionRootKey = await canonicalPath(session.root);
|
|
119
|
+
const sourceRootKey = session.sourceRoot ? await canonicalPath(session.sourceRoot) : undefined;
|
|
120
|
+
if (sessionRootKey !== rootKey && sourceRootKey !== rootKey)
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
filtered.push(entry);
|
|
124
|
+
}
|
|
125
|
+
const summary = filtered.reduce((counts, entry) => {
|
|
126
|
+
counts[entry.state] += 1;
|
|
127
|
+
return counts;
|
|
128
|
+
}, { active: 0, stale: 0, invalid: 0, closed: 0 });
|
|
129
|
+
const offset = Math.max(0, input.offset ?? 0);
|
|
130
|
+
const limit = Math.min(100, Math.max(1, input.limit ?? 50));
|
|
131
|
+
return {
|
|
132
|
+
workspaces: filtered.slice(offset, offset + limit),
|
|
133
|
+
summary: {
|
|
134
|
+
total: entries.length,
|
|
135
|
+
matching: filtered.length,
|
|
136
|
+
...summary,
|
|
137
|
+
},
|
|
138
|
+
page: {
|
|
139
|
+
offset,
|
|
140
|
+
limit,
|
|
141
|
+
hasMore: offset + limit < filtered.length,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async resumeWorkspace(workspaceId, conversationScopeId, bootstrapContext = "auto") {
|
|
43
146
|
const workspace = this.getWorkspace(workspaceId);
|
|
44
147
|
const context = await this.reusedWorkspaceContext(workspace);
|
|
45
148
|
if (!conversationScopeId || !this.store) {
|
|
46
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
...context,
|
|
151
|
+
includeBootstrapContext: bootstrapContext !== "none",
|
|
152
|
+
};
|
|
47
153
|
}
|
|
48
154
|
const targetKeys = await this.workspaceTargetKeys(workspace);
|
|
49
|
-
const
|
|
155
|
+
const contextAlreadyDelivered = targetKeys.some((targetKey) => this.store?.getContextDelivery(conversationScopeId, targetKey)?.contextFingerprint ===
|
|
156
|
+
context.contextFingerprint);
|
|
157
|
+
const includeBootstrapContext = resolveBootstrapContextVisibility(bootstrapContext, contextAlreadyDelivered);
|
|
50
158
|
for (const targetKey of targetKeys) {
|
|
51
159
|
this.store.setConversationBinding({
|
|
52
160
|
conversationScopeId,
|
|
53
161
|
targetKey,
|
|
54
162
|
workspaceSessionId: workspace.id,
|
|
55
163
|
});
|
|
164
|
+
if (includeBootstrapContext) {
|
|
165
|
+
this.store.setContextDelivery({
|
|
166
|
+
conversationScopeId,
|
|
167
|
+
targetKey,
|
|
168
|
+
contextFingerprint: context.contextFingerprint,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
56
171
|
}
|
|
57
|
-
return { ...context, includeBootstrapContext
|
|
172
|
+
return { ...context, includeBootstrapContext };
|
|
58
173
|
}
|
|
59
174
|
async listStaleWorkspaces(workspace) {
|
|
60
175
|
if (!this.store)
|
|
@@ -206,12 +321,12 @@ export class WorkspaceRegistry {
|
|
|
206
321
|
}
|
|
207
322
|
return { ...result, hookReports };
|
|
208
323
|
}
|
|
209
|
-
async openReusableCheckout(path, conversationScopeId, newWorkspace) {
|
|
324
|
+
async openReusableCheckout(path, conversationScopeId, newWorkspace, bootstrapContext) {
|
|
210
325
|
const allowedPath = assertAllowedPath(path, this.config.allowedRoots);
|
|
211
326
|
const projectKey = await canonicalPath(allowedPath);
|
|
212
327
|
const targetKey = JSON.stringify(["checkout", projectKey, null]);
|
|
213
328
|
if (!newWorkspace) {
|
|
214
|
-
const boundContext = await this.boundConversationContext(conversationScopeId, targetKey, "checkout", async (session, root) => session.mode === "checkout" && await canonicalPath(root) === projectKey);
|
|
329
|
+
const boundContext = await this.boundConversationContext(conversationScopeId, targetKey, "checkout", async (session, root) => session.mode === "checkout" && await canonicalPath(root) === projectKey, bootstrapContext);
|
|
215
330
|
if (boundContext)
|
|
216
331
|
return boundContext;
|
|
217
332
|
}
|
|
@@ -220,7 +335,7 @@ export class WorkspaceRegistry {
|
|
|
220
335
|
const freshContext = reusableWorkspace
|
|
221
336
|
? await this.cloneWorkspaceContext(reusableWorkspace)
|
|
222
337
|
: await this.openCheckoutWorkspace(path);
|
|
223
|
-
return this.withConversationContext(freshContext, conversationScopeId, targetKey);
|
|
338
|
+
return this.withConversationContext(freshContext, conversationScopeId, targetKey, bootstrapContext);
|
|
224
339
|
}
|
|
225
340
|
const operationKey = this.conversationOpenKey(targetKey, conversationScopeId);
|
|
226
341
|
const context = await this.openOnce(operationKey, async () => {
|
|
@@ -231,18 +346,19 @@ export class WorkspaceRegistry {
|
|
|
231
346
|
? this.cloneWorkspaceContext(reusableWorkspace)
|
|
232
347
|
: this.reusedWorkspaceContext(reusableWorkspace);
|
|
233
348
|
});
|
|
234
|
-
return this.withConversationContext(context, conversationScopeId, targetKey);
|
|
349
|
+
return this.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
|
|
235
350
|
}
|
|
236
351
|
async openReusableWorktree(input, conversationScopeId) {
|
|
237
352
|
const path = input.path;
|
|
238
353
|
if (!path)
|
|
239
354
|
throw new Error("Worktree mode requires path.");
|
|
355
|
+
const bootstrapContext = input.context ?? "auto";
|
|
240
356
|
const managedPath = this.tryManagedWorktreePath(path);
|
|
241
357
|
if (managedPath) {
|
|
242
358
|
const worktreeKey = await canonicalPath(managedPath);
|
|
243
359
|
const targetKey = JSON.stringify(["worktree-path", worktreeKey]);
|
|
244
360
|
if (!input.newWorkspace) {
|
|
245
|
-
const boundContext = await this.boundConversationContext(conversationScopeId, targetKey, "worktree", async (session, root) => session.mode === "worktree" && await canonicalPath(root) === worktreeKey);
|
|
361
|
+
const boundContext = await this.boundConversationContext(conversationScopeId, targetKey, "worktree", async (session, root) => session.mode === "worktree" && await canonicalPath(root) === worktreeKey, bootstrapContext);
|
|
246
362
|
if (boundContext)
|
|
247
363
|
return boundContext;
|
|
248
364
|
}
|
|
@@ -251,7 +367,7 @@ export class WorkspaceRegistry {
|
|
|
251
367
|
if (!reusableWorkspace) {
|
|
252
368
|
throw new Error(`Managed worktree is not registered as an active ForgeRelay workspace: ${managedPath}. Open the source project in worktree mode to create or recover a managed worktree first.`);
|
|
253
369
|
}
|
|
254
|
-
return this.withConversationContext(await this.cloneWorkspaceContext(reusableWorkspace), conversationScopeId, targetKey);
|
|
370
|
+
return this.withConversationContext(await this.cloneWorkspaceContext(reusableWorkspace), conversationScopeId, targetKey, bootstrapContext);
|
|
255
371
|
}
|
|
256
372
|
const operationKey = this.conversationOpenKey(targetKey, conversationScopeId);
|
|
257
373
|
const context = await this.openOnce(operationKey, async () => {
|
|
@@ -263,7 +379,7 @@ export class WorkspaceRegistry {
|
|
|
263
379
|
? this.cloneWorkspaceContext(reusableWorkspace)
|
|
264
380
|
: this.reusedWorkspaceContext(reusableWorkspace);
|
|
265
381
|
});
|
|
266
|
-
return this.withConversationContext(context, conversationScopeId, targetKey);
|
|
382
|
+
return this.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
|
|
267
383
|
}
|
|
268
384
|
const resolvedBase = await resolveManagedWorktreeBase({
|
|
269
385
|
sourcePath: path,
|
|
@@ -274,13 +390,13 @@ export class WorkspaceRegistry {
|
|
|
274
390
|
const targetKey = JSON.stringify(["worktree", sourceKey, resolvedBase.targetBranch]);
|
|
275
391
|
if (input.newWorktree) {
|
|
276
392
|
const context = await this.openWorktreeWorkspace(path, input.baseRef);
|
|
277
|
-
return this.withConversationContext(context, conversationScopeId, targetKey);
|
|
393
|
+
return this.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
|
|
278
394
|
}
|
|
279
395
|
if (!input.newWorkspace) {
|
|
280
396
|
const boundContext = await this.boundConversationContext(conversationScopeId, targetKey, "worktree", async (session) => session.mode === "worktree" &&
|
|
281
397
|
session.sourceRoot !== undefined &&
|
|
282
398
|
await canonicalPath(session.sourceRoot) === sourceKey &&
|
|
283
|
-
session.targetBranch === resolvedBase.targetBranch);
|
|
399
|
+
session.targetBranch === resolvedBase.targetBranch, bootstrapContext);
|
|
284
400
|
if (boundContext)
|
|
285
401
|
return boundContext;
|
|
286
402
|
}
|
|
@@ -289,7 +405,7 @@ export class WorkspaceRegistry {
|
|
|
289
405
|
const freshContext = reusableWorkspace
|
|
290
406
|
? await this.cloneWorkspaceContext(reusableWorkspace)
|
|
291
407
|
: await this.openWorktreeWorkspace(path, input.baseRef);
|
|
292
|
-
return this.withConversationContext(freshContext, conversationScopeId, targetKey);
|
|
408
|
+
return this.withConversationContext(freshContext, conversationScopeId, targetKey, bootstrapContext);
|
|
293
409
|
}
|
|
294
410
|
const operationKey = this.conversationOpenKey(targetKey, conversationScopeId);
|
|
295
411
|
const context = await this.openOnce(operationKey, async () => {
|
|
@@ -300,7 +416,7 @@ export class WorkspaceRegistry {
|
|
|
300
416
|
? this.cloneWorkspaceContext(reusableWorkspace)
|
|
301
417
|
: this.reusedWorkspaceContext(reusableWorkspace);
|
|
302
418
|
});
|
|
303
|
-
return this.withConversationContext(context, conversationScopeId, targetKey);
|
|
419
|
+
return this.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
|
|
304
420
|
}
|
|
305
421
|
async openOnce(operationKey, open) {
|
|
306
422
|
const pending = this.pendingOpens.get(operationKey);
|
|
@@ -338,7 +454,7 @@ export class WorkspaceRegistry {
|
|
|
338
454
|
? JSON.stringify(["conversation", conversationScopeId, targetKey])
|
|
339
455
|
: targetKey;
|
|
340
456
|
}
|
|
341
|
-
async boundConversationContext(conversationScopeId, targetKey, mode, matches) {
|
|
457
|
+
async boundConversationContext(conversationScopeId, targetKey, mode, matches, bootstrapContext) {
|
|
342
458
|
if (!conversationScopeId || !this.store)
|
|
343
459
|
return undefined;
|
|
344
460
|
const binding = this.store.getConversationBinding(conversationScopeId, targetKey);
|
|
@@ -359,24 +475,30 @@ export class WorkspaceRegistry {
|
|
|
359
475
|
return undefined;
|
|
360
476
|
}
|
|
361
477
|
const context = await this.reusedWorkspaceContext(this.getWorkspace(session.id));
|
|
362
|
-
this.
|
|
363
|
-
return { ...context, includeBootstrapContext: false };
|
|
478
|
+
return this.withConversationContext(context, conversationScopeId, targetKey, bootstrapContext);
|
|
364
479
|
}
|
|
365
|
-
withConversationContext(context, conversationScopeId, targetKey) {
|
|
480
|
+
withConversationContext(context, conversationScopeId, targetKey, bootstrapContext) {
|
|
366
481
|
if (!conversationScopeId || !this.store) {
|
|
367
|
-
return {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
this.store.touchConversationBinding(conversationScopeId, targetKey);
|
|
372
|
-
return { ...context, includeBootstrapContext: false };
|
|
482
|
+
return {
|
|
483
|
+
...context,
|
|
484
|
+
includeBootstrapContext: bootstrapContext !== "none",
|
|
485
|
+
};
|
|
373
486
|
}
|
|
487
|
+
const delivery = this.store.getContextDelivery(conversationScopeId, targetKey);
|
|
488
|
+
const includeBootstrapContext = resolveBootstrapContextVisibility(bootstrapContext, delivery?.contextFingerprint === context.contextFingerprint);
|
|
374
489
|
this.store.setConversationBinding({
|
|
375
490
|
conversationScopeId,
|
|
376
491
|
targetKey,
|
|
377
492
|
workspaceSessionId: context.workspace.id,
|
|
378
493
|
});
|
|
379
|
-
|
|
494
|
+
if (includeBootstrapContext) {
|
|
495
|
+
this.store.setContextDelivery({
|
|
496
|
+
conversationScopeId,
|
|
497
|
+
targetKey,
|
|
498
|
+
contextFingerprint: context.contextFingerprint,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return { ...context, includeBootstrapContext };
|
|
380
502
|
}
|
|
381
503
|
pruneIdleWorkspaceSessions(protectedWorkspaceIds, force = false) {
|
|
382
504
|
if (!this.store)
|
|
@@ -397,6 +519,13 @@ export class WorkspaceRegistry {
|
|
|
397
519
|
this.store.deleteConversationBinding(binding.conversationScopeId, binding.targetKey);
|
|
398
520
|
}
|
|
399
521
|
}
|
|
522
|
+
for (const delivery of this.store.listContextDeliveries()) {
|
|
523
|
+
const deliveredAt = Date.parse(delivery.deliveredAt);
|
|
524
|
+
if (Number.isFinite(deliveredAt) &&
|
|
525
|
+
now - deliveredAt >= WORKSPACE_SESSION_IDLE_TTL_MS) {
|
|
526
|
+
this.store.deleteContextDelivery(delivery.conversationScopeId, delivery.targetKey);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
400
529
|
const boundWorkspaceIds = new Set(this.store.listConversationBindings().map((binding) => binding.workspaceSessionId));
|
|
401
530
|
const worktreeAnchors = new Map();
|
|
402
531
|
for (const session of activeSessions) {
|
|
@@ -499,13 +628,17 @@ export class WorkspaceRegistry {
|
|
|
499
628
|
});
|
|
500
629
|
}
|
|
501
630
|
async reusedWorkspaceContext(workspace) {
|
|
631
|
+
Object.assign(workspace, this.loadSkillsForWorkspace(workspace.root));
|
|
632
|
+
workspace.capabilityGuides = loadCapabilityGuides(this.config);
|
|
502
633
|
workspace.agentProfiles = await loadLocalAgentProfiles(this.config, workspace.root);
|
|
503
634
|
const agentsFiles = await this.loadInitialAgentsFiles(workspace.root);
|
|
504
635
|
const availableAgentsFiles = await this.findAvailableAgentsFiles(workspace.root, agentsFiles);
|
|
636
|
+
const contextFingerprint = bootstrapContextFingerprint(workspace, agentsFiles, availableAgentsFiles);
|
|
505
637
|
return {
|
|
506
638
|
workspace,
|
|
507
639
|
agentsFiles,
|
|
508
640
|
availableAgentsFiles,
|
|
641
|
+
contextFingerprint,
|
|
509
642
|
hookReports: [],
|
|
510
643
|
workspaceReused: true,
|
|
511
644
|
includeBootstrapContext: true,
|
|
@@ -676,10 +809,12 @@ export class WorkspaceRegistry {
|
|
|
676
809
|
});
|
|
677
810
|
const agentsFiles = await this.loadInitialAgentsFiles(workspace.root);
|
|
678
811
|
const availableAgentsFiles = await this.findAvailableAgentsFiles(workspace.root, agentsFiles);
|
|
812
|
+
const contextFingerprint = bootstrapContextFingerprint(workspace, agentsFiles, availableAgentsFiles);
|
|
679
813
|
return {
|
|
680
814
|
workspace,
|
|
681
815
|
agentsFiles,
|
|
682
816
|
availableAgentsFiles,
|
|
817
|
+
contextFingerprint,
|
|
683
818
|
hookReports,
|
|
684
819
|
workspaceReused: false,
|
|
685
820
|
includeBootstrapContext: true,
|
|
@@ -761,6 +896,51 @@ export class WorkspaceRegistry {
|
|
|
761
896
|
return discovered.sort((a, b) => a.path.localeCompare(b.path));
|
|
762
897
|
}
|
|
763
898
|
}
|
|
899
|
+
function resolveBootstrapContextVisibility(mode, contextAlreadyDelivered) {
|
|
900
|
+
if (mode === "full")
|
|
901
|
+
return true;
|
|
902
|
+
if (mode === "none")
|
|
903
|
+
return false;
|
|
904
|
+
return !contextAlreadyDelivered;
|
|
905
|
+
}
|
|
906
|
+
function bootstrapContextFingerprint(workspace, agentsFiles, availableAgentsFiles) {
|
|
907
|
+
const payload = {
|
|
908
|
+
agentsFiles: agentsFiles
|
|
909
|
+
.map((file) => ({ path: resolve(file.path), content: file.content }))
|
|
910
|
+
.sort((left, right) => left.path.localeCompare(right.path)),
|
|
911
|
+
availableAgentsFiles: availableAgentsFiles
|
|
912
|
+
.map((file) => resolve(file.path))
|
|
913
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
914
|
+
skills: workspace.skills
|
|
915
|
+
.map((skill) => ({
|
|
916
|
+
name: skill.name,
|
|
917
|
+
description: skill.description,
|
|
918
|
+
filePath: resolve(skill.filePath),
|
|
919
|
+
disableModelInvocation: skill.disableModelInvocation ?? false,
|
|
920
|
+
}))
|
|
921
|
+
.sort((left, right) => left.name.localeCompare(right.name) || left.filePath.localeCompare(right.filePath)),
|
|
922
|
+
skillDiagnostics: workspace.skillDiagnostics,
|
|
923
|
+
capabilityGuides: workspace.capabilityGuides
|
|
924
|
+
.map((guide) => ({
|
|
925
|
+
name: guide.name,
|
|
926
|
+
description: guide.description,
|
|
927
|
+
whenToRead: guide.whenToRead,
|
|
928
|
+
filePath: resolve(guide.filePath),
|
|
929
|
+
}))
|
|
930
|
+
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
931
|
+
agentProfiles: workspace.agentProfiles
|
|
932
|
+
.map((profile) => ({
|
|
933
|
+
name: profile.name,
|
|
934
|
+
description: profile.description,
|
|
935
|
+
provider: profile.provider,
|
|
936
|
+
model: profile.model,
|
|
937
|
+
thinking: profile.thinking,
|
|
938
|
+
filePath: resolve(profile.filePath),
|
|
939
|
+
}))
|
|
940
|
+
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
941
|
+
};
|
|
942
|
+
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
943
|
+
}
|
|
764
944
|
async function canonicalPath(path) {
|
|
765
945
|
const missingSegments = [];
|
|
766
946
|
let candidate = path;
|
|
@@ -14,11 +14,12 @@ The legacy `DEVSPACE_ARTIFACTS` variable remains a fallback during migration.
|
|
|
14
14
|
## Workflow
|
|
15
15
|
|
|
16
16
|
1. Open the destination project with `open_workspace`.
|
|
17
|
-
2. Call `
|
|
18
|
-
the existing `workspaceId`,
|
|
17
|
+
2. Call the `capability` Gateway with `name="artifact.download"`, `action="run"`,
|
|
18
|
+
the existing `workspaceId`, the native file value in the Gateway's top-level
|
|
19
|
+
`file` slot, and a workspace-relative destination in `arguments.path`.
|
|
19
20
|
3. Use the returned normalized path with ordinary ForgeRelay file tools.
|
|
20
21
|
|
|
21
|
-
The
|
|
22
|
+
The capability creates missing parent directories and refuses to overwrite an existing
|
|
22
23
|
destination.
|
|
23
24
|
|
|
24
25
|
## Input boundary
|
|
@@ -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:
|
|
@@ -131,8 +185,9 @@ worktrees, subagents, artifact/change-review workflows, Host/OAuth/MCP App
|
|
|
131
185
|
integration, and long-running shell/PTY/process behavior. Optional guides are
|
|
132
186
|
advertised only when their feature is enabled; for example, disabled subagents
|
|
133
187
|
and artifact/change-review features do not add those descriptors to bootstrap
|
|
134
|
-
context.
|
|
135
|
-
|
|
188
|
+
context. Bootstrap replay follows the conversation/canonical-target context
|
|
189
|
+
fingerprint described above, so changing logical `workspaceId` alone does not
|
|
190
|
+
repeat unchanged descriptors; previously advertised guides remain valid.
|
|
136
191
|
|
|
137
192
|
The fingerprint is also a stale-Host-schema diagnostic. If `open_workspace`
|
|
138
193
|
reports a capability such as `filesystem.rename-move` but the Host's current
|
|
@@ -209,7 +264,9 @@ killing it. Reuse `bash(action="process", processId=...)` to poll/wait, send inp
|
|
|
209
264
|
resize a PTY, or interrupt the existing process; or continue other work and consume
|
|
210
265
|
the one-shot completion notice from a later result in the same workspace.
|
|
211
266
|
|
|
212
|
-
`FORGERELAY_TOOL_MODE=full`
|
|
267
|
+
`FORGERELAY_TOOL_MODE=full` is retained as a compatibility value and exposes the
|
|
268
|
+
same canonical 9-tool surface as `minimal`; use `bash` for search and directory
|
|
269
|
+
inspection.
|
|
213
270
|
|
|
214
271
|
Experimental `FORGERELAY_TOOL_MODE=codex` keeps its Codex-shaped compatibility
|
|
215
272
|
surface, including direct `rename`/`delete` path mutations alongside `apply_patch`,
|
|
@@ -221,12 +278,14 @@ Workspace IDs are logical conversation handles rather than physical-directory
|
|
|
221
278
|
identities. The same conversation keeps a stable ID for a project, while another
|
|
222
279
|
conversation normally receives a different ID pointing at the same checkout or
|
|
223
280
|
worktree. `open_workspace` can explicitly resume a known `workspaceId`, and a
|
|
224
|
-
fresh logical ID is created only when the user asks for one.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
281
|
+
fresh logical ID is created only when the user asks for one. The normal open path
|
|
282
|
+
may still include `staleWorkspaces` as a passive reminder for same-target handles
|
|
283
|
+
idle for more than two days; use `open_workspace(action="list")` for complete,
|
|
284
|
+
filtered inventory when continuation or cleanup actually requires it.
|
|
285
|
+
`close_workspace` is the single public close operation: checkout-backed workspaces
|
|
286
|
+
release the logical handle, while managed-worktree-backed workspaces require
|
|
287
|
+
`commitMessage` and run the safe commit / fast-forward-only integration / cleanup
|
|
288
|
+
lifecycle.
|
|
230
289
|
|
|
231
290
|
Shell commands are allowed to modify ordinary project files when that is a
|
|
232
291
|
natural part of the user's requested development task; ForgeRelay does not apply
|
|
@@ -247,8 +306,8 @@ an explicit user request before changing configuration files through `bash` or
|
|
|
247
306
|
By default `FORGERELAY_WIDGETS=full` attaches ChatGPT Apps-compatible UI to the
|
|
248
307
|
normal workspace/file/edit/shell tools.
|
|
249
308
|
|
|
250
|
-
`FORGERELAY_WIDGETS=changes`
|
|
251
|
-
widget usage focused on workspace/change review.
|
|
309
|
+
`FORGERELAY_WIDGETS=changes` enables aggregate `review.changes` Capability results
|
|
310
|
+
and keeps widget usage focused on workspace/change review.
|
|
252
311
|
|
|
253
312
|
`FORGERELAY_WIDGETS=off` disables widget UI.
|
|
254
313
|
|
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 compatibility
|
|
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
|
|
@@ -161,12 +163,35 @@ receives a different ID for the same physical checkout/worktree. Pass
|
|
|
161
163
|
`workspaceId` to `open_workspace` to explicitly resume an existing handle in the
|
|
162
164
|
current conversation. `newWorkspace: true` allocates a new logical handle without
|
|
163
165
|
creating another checkout or Git worktree and should be used only on explicit user
|
|
164
|
-
request.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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.
|
|
170
195
|
|
|
171
196
|
Regular `bash` has no execution-timeout input. `action="run"` (the default) waits
|
|
172
197
|
in the foreground for at most 300 seconds; if the process is still alive, the
|
|
@@ -186,7 +211,7 @@ regular Agent workflows should use the single `bash` process lifecycle.
|
|
|
186
211
|
| Value | Behavior |
|
|
187
212
|
| --- | --- |
|
|
188
213
|
| `full` | Default. Attach UI to exposed workspace/file/edit/shell tools. |
|
|
189
|
-
| `changes` | Attach UI to `open_workspace` and
|
|
214
|
+
| `changes` | Attach UI to `open_workspace` and Capability Gateway review results from `review.changes`. |
|
|
190
215
|
| `off` | Disable widget UI. |
|
|
191
216
|
|
|
192
217
|
## Lifecycle hooks
|
package/docs/debugging.md
CHANGED
|
@@ -59,9 +59,9 @@ 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`, `bash` long-process `run` → `processId` → `process`, 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
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`;
|