@akira-tl/forgerelay 0.8.1 → 0.8.3
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 +13 -0
- package/README.md +4 -2
- package/capabilities/workspace-tasks/GUIDE.md +29 -0
- package/dist/capabilities.js +6 -0
- package/dist/capability-registry.js +82 -21
- package/dist/composite-activity.js +1 -1
- package/dist/composite-workspaces.js +31 -10
- package/dist/git-worktrees.js +22 -0
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/server.js +290 -60
- package/dist/subagents/sessions/capability.js +3 -0
- package/dist/workspace-store.js +25 -0
- package/dist/workspace-tasks.js +351 -0
- package/dist/workspaces.js +95 -13
- package/docs/chatgpt-coding-workflow.md +17 -2
- package/docs/configuration.md +21 -6
- package/package.json +2 -2
- package/scripts/debug/accept.mjs +255 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.8.3] - 2026-08-31
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added `workspace.tasks` with private file-backed Task Lists per persistent Workspace, preserving Task state across restart, close/reopen, managed-worktree backing replacement, and Composite close while keeping Task data out of project Git contents.
|
|
12
|
+
|
|
13
|
+
## [0.8.2] - 2026-08-31
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Managed-worktree Workspace close now preserves the same identity for later reopen with fresh backing; explicit delete safely finalizes active work before removing ForgeRelay-owned state.
|
|
18
|
+
- Composite Workspace close now preserves identity and member topology for later reopen; explicit delete dissolves only the Composite relationship and leaves member Workspaces untouched.
|
|
19
|
+
|
|
7
20
|
## [0.8.1] - 2026-08-30
|
|
8
21
|
|
|
9
22
|
### Added
|
package/README.md
CHANGED
|
@@ -136,8 +136,10 @@ ForgeRelay does not merge member filesystems, Git state, Hooks, Skills, processe
|
|
|
136
136
|
or audit facts, and it never infers a member from the tool type or purpose text.
|
|
137
137
|
The Composite Activity Panel presents member operations in one Host Turn while the
|
|
138
138
|
actual facts remain owned by the member Workspace. `close_workspace` on a Composite
|
|
139
|
-
Workspace
|
|
140
|
-
|
|
139
|
+
Workspace now preserves the Composite identity and member topology as `closed`; a
|
|
140
|
+
later `open_workspace` restores the same `cws_...` identity. `action="delete"` is the
|
|
141
|
+
explicit dissolve operation. Neither close nor delete closes member Workspaces,
|
|
142
|
+
finalizes their worktrees, stops their jobs, changes their files, or removes relay routes.
|
|
141
143
|
|
|
142
144
|
### Progressive MCP context
|
|
143
145
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Workspace Tasks
|
|
2
|
+
|
|
3
|
+
`workspace.tasks` 维护当前 Workspace 自己的持久 Task Lists。它是轻量工作续接状态,不是执行队列、Subagent Session、Activity 或依赖图。
|
|
4
|
+
|
|
5
|
+
## 使用边界
|
|
6
|
+
|
|
7
|
+
- 只操作当前调用上下文中的 Workspace;参数中没有目标 `workspaceId`。
|
|
8
|
+
- Task state 由 ForgeRelay 保存在私有 state directory,不写入 checkout 或 managed worktree。
|
|
9
|
+
- 一个 Workspace 可以有多个 Task List;List 可为 `active` 或 `archived`。
|
|
10
|
+
- Task 状态只有 `pending`、`in_progress`、`completed`。`content` 保存继续工作真正需要的要求、阻塞点、结论或下一步,而不是日志或对话转录。
|
|
11
|
+
- Task/List ID 创建后保持稳定。完成 Task 不会删除它;删除必须显式执行。
|
|
12
|
+
|
|
13
|
+
## 操作
|
|
14
|
+
|
|
15
|
+
先用 `operation="get"` 读取当前 Task state。
|
|
16
|
+
|
|
17
|
+
List 操作:
|
|
18
|
+
|
|
19
|
+
- `list.create`:创建具名 List,可指定 `position`。
|
|
20
|
+
- `list.update`:修改 `name`、`state` 或 `position`;`state="archived"` 用于归档,改回 `active` 即重新激活。
|
|
21
|
+
- `list.delete`:显式删除 List 及其 Tasks。
|
|
22
|
+
|
|
23
|
+
Task 操作:
|
|
24
|
+
|
|
25
|
+
- `task.create`:在一个 List 中创建 Task;需要 `subject`,可提供 `content`、`status`、`position`。
|
|
26
|
+
- `task.update`:修改 `subject`、`content`、`status` 或 `position`;至少提供一个变更字段。
|
|
27
|
+
- `task.delete`:显式删除 Task。
|
|
28
|
+
|
|
29
|
+
Task 修改使用独立的 revision/fingerprint 域;它不改变 Workspace bootstrap `contextFingerprint`。
|
package/dist/capabilities.js
CHANGED
|
@@ -39,6 +39,11 @@ const CAPABILITY_GUIDE_DEFINITIONS = [
|
|
|
39
39
|
description: "Read-only semantic code navigation backed by external Language servers.",
|
|
40
40
|
whenToRead: "Read before using code.intelligence or configuring Language servers.",
|
|
41
41
|
},
|
|
42
|
+
{
|
|
43
|
+
name: "workspace-tasks",
|
|
44
|
+
description: "Persistent Task Lists owned by the current Workspace.",
|
|
45
|
+
whenToRead: "Read before creating or maintaining Workspace Tasks.",
|
|
46
|
+
},
|
|
42
47
|
{
|
|
43
48
|
name: "batch-execution",
|
|
44
49
|
description: "One-call execution of multiple independent ForgeRelay core operations.",
|
|
@@ -87,6 +92,7 @@ export function buildCapabilityFingerprint(config, version, context = {}) {
|
|
|
87
92
|
"hooks.lifecycle",
|
|
88
93
|
"capability-guides.read",
|
|
89
94
|
"code.intelligence",
|
|
95
|
+
"workspace.tasks",
|
|
90
96
|
];
|
|
91
97
|
if (config.toolMode !== "codex") {
|
|
92
98
|
capabilities.push("batch.execute");
|
|
@@ -130,6 +130,52 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
130
130
|
line: z.number().int(),
|
|
131
131
|
column: z.number().int(),
|
|
132
132
|
};
|
|
133
|
+
const workspaceTaskStatus = z.enum(["pending", "in_progress", "completed"]);
|
|
134
|
+
const workspaceTaskListState = z.enum(["active", "archived"]);
|
|
135
|
+
const workspaceTasksInput = z.union([
|
|
136
|
+
z.object({ operation: z.literal("get") }).strict(),
|
|
137
|
+
z.object({
|
|
138
|
+
operation: z.literal("list.create"),
|
|
139
|
+
name: z.string().trim().min(1),
|
|
140
|
+
position: z.number().int().min(0).optional(),
|
|
141
|
+
}).strict(),
|
|
142
|
+
z.object({
|
|
143
|
+
operation: z.literal("list.update"),
|
|
144
|
+
listId: z.string().min(1),
|
|
145
|
+
name: z.string().trim().min(1).optional(),
|
|
146
|
+
state: workspaceTaskListState.optional(),
|
|
147
|
+
position: z.number().int().min(0).optional(),
|
|
148
|
+
}).strict().refine((input) => input.name !== undefined || input.state !== undefined || input.position !== undefined, { message: "list.update requires at least one field to change" }),
|
|
149
|
+
z.object({
|
|
150
|
+
operation: z.literal("list.delete"),
|
|
151
|
+
listId: z.string().min(1),
|
|
152
|
+
}).strict(),
|
|
153
|
+
z.object({
|
|
154
|
+
operation: z.literal("task.create"),
|
|
155
|
+
listId: z.string().min(1),
|
|
156
|
+
subject: z.string().trim().min(1),
|
|
157
|
+
content: z.string().optional(),
|
|
158
|
+
status: workspaceTaskStatus.optional(),
|
|
159
|
+
position: z.number().int().min(0).optional(),
|
|
160
|
+
}).strict(),
|
|
161
|
+
z.object({
|
|
162
|
+
operation: z.literal("task.update"),
|
|
163
|
+
listId: z.string().min(1),
|
|
164
|
+
taskId: z.string().min(1),
|
|
165
|
+
subject: z.string().trim().min(1).optional(),
|
|
166
|
+
content: z.string().optional(),
|
|
167
|
+
status: workspaceTaskStatus.optional(),
|
|
168
|
+
position: z.number().int().min(0).optional(),
|
|
169
|
+
}).strict().refine((input) => input.subject !== undefined
|
|
170
|
+
|| input.content !== undefined
|
|
171
|
+
|| input.status !== undefined
|
|
172
|
+
|| input.position !== undefined, { message: "task.update requires at least one field to change" }),
|
|
173
|
+
z.object({
|
|
174
|
+
operation: z.literal("task.delete"),
|
|
175
|
+
listId: z.string().min(1),
|
|
176
|
+
taskId: z.string().min(1),
|
|
177
|
+
}).strict(),
|
|
178
|
+
]);
|
|
133
179
|
const subagentSessionInput = z.discriminatedUnion("operation", [
|
|
134
180
|
z.object({
|
|
135
181
|
operation: z.literal("start"),
|
|
@@ -190,11 +236,11 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
190
236
|
readGuideBeforeFirstUse: true,
|
|
191
237
|
batchPolicy: "parallel",
|
|
192
238
|
inputSchema: hooksCheckInput,
|
|
193
|
-
availability: () => (
|
|
239
|
+
availability: (context) => filesystemWorkspaceAvailability(context),
|
|
194
240
|
run: async (_input, context) => ({
|
|
195
241
|
value: {
|
|
196
242
|
ok: true,
|
|
197
|
-
...await dependencies.inspectHooks(context
|
|
243
|
+
...await dependencies.inspectHooks(requireWorkspaceRoot(context)),
|
|
198
244
|
},
|
|
199
245
|
}),
|
|
200
246
|
},
|
|
@@ -206,10 +252,7 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
206
252
|
readGuideBeforeFirstUse: true,
|
|
207
253
|
batchPolicy: "serial",
|
|
208
254
|
inputSchema: z.object({}).strict(),
|
|
209
|
-
availability: () => (
|
|
210
|
-
available: dependencies.reviewChanges?.available ?? false,
|
|
211
|
-
reason: dependencies.reviewChanges?.unavailableReason,
|
|
212
|
-
}),
|
|
255
|
+
availability: (context) => filesystemWorkspaceAvailability(context, dependencies.reviewChanges?.available ?? false, dependencies.reviewChanges?.unavailableReason),
|
|
213
256
|
run: async (_input, context) => dependencies.reviewChanges.run(context),
|
|
214
257
|
}]
|
|
215
258
|
: []),
|
|
@@ -221,11 +264,23 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
221
264
|
readGuideBeforeFirstUse: true,
|
|
222
265
|
batchPolicy: "parallel",
|
|
223
266
|
inputSchema: codeIntelligenceInput,
|
|
267
|
+
availability: (context) => filesystemWorkspaceAvailability(context, dependencies.codeIntelligence?.available ?? false, dependencies.codeIntelligence?.unavailableReason),
|
|
268
|
+
run: async (input, context, options) => dependencies.codeIntelligence.run(input, context, options),
|
|
269
|
+
}]
|
|
270
|
+
: []),
|
|
271
|
+
...(dependencies.workspaceTasks
|
|
272
|
+
? [{
|
|
273
|
+
name: "workspace.tasks",
|
|
274
|
+
description: "Maintain persistent Task Lists owned by the current Workspace.",
|
|
275
|
+
guideName: "workspace-tasks",
|
|
276
|
+
readGuideBeforeFirstUse: true,
|
|
277
|
+
batchPolicy: "serial",
|
|
278
|
+
inputSchema: workspaceTasksInput,
|
|
224
279
|
availability: () => ({
|
|
225
|
-
available: dependencies.
|
|
226
|
-
reason: dependencies.
|
|
280
|
+
available: dependencies.workspaceTasks?.available ?? false,
|
|
281
|
+
reason: dependencies.workspaceTasks?.unavailableReason,
|
|
227
282
|
}),
|
|
228
|
-
run: async (input, context, options) => dependencies.
|
|
283
|
+
run: async (input, context, options) => dependencies.workspaceTasks.run(input, context, options),
|
|
229
284
|
}]
|
|
230
285
|
: []),
|
|
231
286
|
...(dependencies.subagentSession
|
|
@@ -236,10 +291,7 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
236
291
|
readGuideBeforeFirstUse: true,
|
|
237
292
|
batchPolicy: "unsupported",
|
|
238
293
|
inputSchema: subagentSessionInput,
|
|
239
|
-
availability: () => (
|
|
240
|
-
available: dependencies.subagentSession?.available ?? false,
|
|
241
|
-
reason: dependencies.subagentSession?.unavailableReason,
|
|
242
|
-
}),
|
|
294
|
+
availability: (context) => filesystemWorkspaceAvailability(context, dependencies.subagentSession?.available ?? false, dependencies.subagentSession?.unavailableReason),
|
|
243
295
|
run: async (input, context, options) => dependencies.subagentSession.run(input, context, options),
|
|
244
296
|
}]
|
|
245
297
|
: []),
|
|
@@ -251,10 +303,7 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
251
303
|
readGuideBeforeFirstUse: true,
|
|
252
304
|
batchPolicy: "unsupported",
|
|
253
305
|
inputSchema: batchExecuteInputSchema,
|
|
254
|
-
availability: () => (
|
|
255
|
-
available: dependencies.batchExecute?.available ?? false,
|
|
256
|
-
reason: dependencies.batchExecute?.unavailableReason,
|
|
257
|
-
}),
|
|
306
|
+
availability: (context) => filesystemWorkspaceAvailability(context, dependencies.batchExecute?.available ?? false, dependencies.batchExecute?.unavailableReason),
|
|
258
307
|
run: async (input, context, options) => dependencies.batchExecute.run(input, context, options),
|
|
259
308
|
}]
|
|
260
309
|
: []),
|
|
@@ -277,15 +326,27 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
277
326
|
path: z.string().min(1),
|
|
278
327
|
}).strict(),
|
|
279
328
|
nativeFileArgument: "file",
|
|
280
|
-
availability: () => (
|
|
281
|
-
available: dependencies.downloadArtifact?.available ?? false,
|
|
282
|
-
reason: dependencies.downloadArtifact?.unavailableReason,
|
|
283
|
-
}),
|
|
329
|
+
availability: (context) => filesystemWorkspaceAvailability(context, dependencies.downloadArtifact?.available ?? false, dependencies.downloadArtifact?.unavailableReason),
|
|
284
330
|
run: async (input, context) => dependencies.downloadArtifact.run(input, context),
|
|
285
331
|
}]
|
|
286
332
|
: []),
|
|
287
333
|
]);
|
|
288
334
|
}
|
|
335
|
+
function filesystemWorkspaceAvailability(context, available = true, reason) {
|
|
336
|
+
if (context.workspaceKind !== "workspace" || !context.workspaceRoot) {
|
|
337
|
+
return {
|
|
338
|
+
available: false,
|
|
339
|
+
reason: "This capability requires a filesystem-backed Workspace.",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return { available, ...(reason ? { reason } : {}) };
|
|
343
|
+
}
|
|
344
|
+
function requireWorkspaceRoot(context) {
|
|
345
|
+
if (context.workspaceKind !== "workspace" || !context.workspaceRoot) {
|
|
346
|
+
throw new CapabilityError("capability_unavailable", "This capability requires a filesystem-backed Workspace.");
|
|
347
|
+
}
|
|
348
|
+
return context.workspaceRoot;
|
|
349
|
+
}
|
|
289
350
|
function isRecord(value) {
|
|
290
351
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
291
352
|
}
|
|
@@ -12,7 +12,7 @@ export class CompositeActivityCoordinator {
|
|
|
12
12
|
return this.composites.has(workspaceId);
|
|
13
13
|
}
|
|
14
14
|
beginPanel(workspaceId, conversationScopeId) {
|
|
15
|
-
this.composites.
|
|
15
|
+
this.composites.touchActive(workspaceId);
|
|
16
16
|
const snapshot = this.queries.beginTurn(conversationScopeId, workspaceId);
|
|
17
17
|
this.turns.set(snapshot.turnId, {
|
|
18
18
|
compositeWorkspaceId: workspaceId,
|
|
@@ -13,16 +13,20 @@ export class CompositeWorkspaceRegistry {
|
|
|
13
13
|
has(workspaceId) {
|
|
14
14
|
return this.records.has(workspaceId);
|
|
15
15
|
}
|
|
16
|
+
isActive(workspaceId) {
|
|
17
|
+
return this.records.get(workspaceId)?.status === "active";
|
|
18
|
+
}
|
|
16
19
|
create(name) {
|
|
17
20
|
const normalized = normalizeName(name);
|
|
18
21
|
const existing = [...this.records.values()].find((record) => record.name === normalized);
|
|
19
22
|
if (existing)
|
|
20
|
-
return this.
|
|
23
|
+
return this.open(existing.id);
|
|
21
24
|
const now = new Date().toISOString();
|
|
22
25
|
const record = {
|
|
23
26
|
id: `cws_${randomBytes(5).toString("hex")}`,
|
|
24
27
|
kind: "composite",
|
|
25
28
|
name: normalized,
|
|
29
|
+
status: "active",
|
|
26
30
|
members: [],
|
|
27
31
|
createdAt: now,
|
|
28
32
|
lastUsedAt: now,
|
|
@@ -38,7 +42,17 @@ export class CompositeWorkspaceRegistry {
|
|
|
38
42
|
return cloneRecord(record);
|
|
39
43
|
}
|
|
40
44
|
open(workspaceId) {
|
|
41
|
-
|
|
45
|
+
const record = this.requireRecord(workspaceId);
|
|
46
|
+
record.status = "active";
|
|
47
|
+
return this.touchRecord(record);
|
|
48
|
+
}
|
|
49
|
+
close(workspaceId) {
|
|
50
|
+
const record = this.requireActive(workspaceId);
|
|
51
|
+
record.status = "closed";
|
|
52
|
+
return this.touchRecord(record);
|
|
53
|
+
}
|
|
54
|
+
touchActive(workspaceId) {
|
|
55
|
+
return this.touchRecord(this.requireActive(workspaceId));
|
|
42
56
|
}
|
|
43
57
|
list() {
|
|
44
58
|
return [...this.records.values()]
|
|
@@ -46,7 +60,7 @@ export class CompositeWorkspaceRegistry {
|
|
|
46
60
|
.sort((left, right) => right.lastUsedAt.localeCompare(left.lastUsedAt));
|
|
47
61
|
}
|
|
48
62
|
addMember(workspaceId, input) {
|
|
49
|
-
const record = this.
|
|
63
|
+
const record = this.requireActive(workspaceId);
|
|
50
64
|
const name = normalizeMemberName(input.name);
|
|
51
65
|
const purpose = input.purpose.trim();
|
|
52
66
|
if (!purpose)
|
|
@@ -64,7 +78,7 @@ export class CompositeWorkspaceRegistry {
|
|
|
64
78
|
return cloneRecord(record);
|
|
65
79
|
}
|
|
66
80
|
updateMember(workspaceId, memberName, input) {
|
|
67
|
-
const record = this.
|
|
81
|
+
const record = this.requireActive(workspaceId);
|
|
68
82
|
const currentName = normalizeMemberName(memberName);
|
|
69
83
|
const index = record.members.findIndex((member) => member.name === currentName);
|
|
70
84
|
if (index < 0)
|
|
@@ -94,7 +108,7 @@ export class CompositeWorkspaceRegistry {
|
|
|
94
108
|
return cloneRecord(record);
|
|
95
109
|
}
|
|
96
110
|
removeMember(workspaceId, memberName) {
|
|
97
|
-
const record = this.
|
|
111
|
+
const record = this.requireActive(workspaceId);
|
|
98
112
|
const name = normalizeMemberName(memberName);
|
|
99
113
|
const index = record.members.findIndex((member) => member.name === name);
|
|
100
114
|
if (index < 0)
|
|
@@ -105,7 +119,7 @@ export class CompositeWorkspaceRegistry {
|
|
|
105
119
|
return cloneRecord(record);
|
|
106
120
|
}
|
|
107
121
|
member(workspaceId, memberName) {
|
|
108
|
-
const record = this.
|
|
122
|
+
const record = this.requireActive(workspaceId);
|
|
109
123
|
const name = normalizeMemberName(memberName);
|
|
110
124
|
const member = record.members.find((entry) => entry.name === name);
|
|
111
125
|
if (!member)
|
|
@@ -118,12 +132,18 @@ export class CompositeWorkspaceRegistry {
|
|
|
118
132
|
this.persist();
|
|
119
133
|
return cloneRecord(record);
|
|
120
134
|
}
|
|
121
|
-
|
|
122
|
-
const record = this.requireRecord(workspaceId);
|
|
135
|
+
touchRecord(record) {
|
|
123
136
|
record.lastUsedAt = new Date().toISOString();
|
|
124
137
|
this.persist();
|
|
125
138
|
return cloneRecord(record);
|
|
126
139
|
}
|
|
140
|
+
requireActive(workspaceId) {
|
|
141
|
+
const record = this.requireRecord(workspaceId);
|
|
142
|
+
if (record.status !== "active") {
|
|
143
|
+
throw new Error(`Composite Workspace ${workspaceId} is closed. Reopen it with open_workspace before use.`);
|
|
144
|
+
}
|
|
145
|
+
return record;
|
|
146
|
+
}
|
|
127
147
|
requireRecord(workspaceId) {
|
|
128
148
|
const record = this.records.get(workspaceId);
|
|
129
149
|
if (!record)
|
|
@@ -140,7 +160,7 @@ export class CompositeWorkspaceRegistry {
|
|
|
140
160
|
return;
|
|
141
161
|
throw new Error(`Failed to load Composite Workspace state: ${errorMessage(error)}`);
|
|
142
162
|
}
|
|
143
|
-
if (parsed?.version !== 1 || !Array.isArray(parsed.workspaces)) {
|
|
163
|
+
if ((parsed?.version !== 1 && parsed?.version !== 2) || !Array.isArray(parsed.workspaces)) {
|
|
144
164
|
throw new Error("Composite Workspace state has an unsupported format.");
|
|
145
165
|
}
|
|
146
166
|
for (const record of parsed.workspaces) {
|
|
@@ -148,6 +168,7 @@ export class CompositeWorkspaceRegistry {
|
|
|
148
168
|
continue;
|
|
149
169
|
this.records.set(record.id, {
|
|
150
170
|
...record,
|
|
171
|
+
status: record.status === "closed" ? "closed" : "active",
|
|
151
172
|
members: Array.isArray(record.members) ? record.members.map((member) => ({ ...member })) : [],
|
|
152
173
|
});
|
|
153
174
|
}
|
|
@@ -155,7 +176,7 @@ export class CompositeWorkspaceRegistry {
|
|
|
155
176
|
persist() {
|
|
156
177
|
mkdirSync(this.stateDir, { recursive: true });
|
|
157
178
|
const state = {
|
|
158
|
-
version:
|
|
179
|
+
version: 2,
|
|
159
180
|
workspaces: [...this.records.values()].map(cloneRecord),
|
|
160
181
|
};
|
|
161
182
|
const tempPath = `${this.statePath}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
package/dist/git-worktrees.js
CHANGED
|
@@ -57,6 +57,28 @@ export async function createManagedWorktree(input) {
|
|
|
57
57
|
managed: true,
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
export async function discardFreshManagedWorktree(input) {
|
|
61
|
+
const sourceRoot = assertAllowedPath(input.worktree.sourceRoot, input.config.allowedRoots);
|
|
62
|
+
const worktreePath = assertAllowedPath(input.worktree.path, [input.config.worktreeRoot]);
|
|
63
|
+
const worktreeBranch = await currentBranch(worktreePath);
|
|
64
|
+
if (worktreeBranch !== input.worktree.branch) {
|
|
65
|
+
throw new GitWorktreeError("GIT_WORKTREE_CLOSE_FAILED", `Cannot roll back reopened worktree because it is on branch ${JSON.stringify(worktreeBranch)} instead of ${JSON.stringify(input.worktree.branch)}.`);
|
|
66
|
+
}
|
|
67
|
+
if ((await git(["status", "--porcelain=v1"], worktreePath)).trim().length > 0) {
|
|
68
|
+
throw new GitWorktreeError("GIT_WORKTREE_CLOSE_FAILED", "Cannot roll back reopened worktree because it acquired uncommitted changes during reopen.");
|
|
69
|
+
}
|
|
70
|
+
const worktreeHead = (await git(["rev-parse", "HEAD"], worktreePath)).trim();
|
|
71
|
+
if (worktreeHead !== input.worktree.baseSha) {
|
|
72
|
+
throw new GitWorktreeError("GIT_WORKTREE_CLOSE_FAILED", "Cannot roll back reopened worktree because its branch advanced during reopen.");
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
await git(["worktree", "remove", worktreePath], sourceRoot);
|
|
76
|
+
await git(["branch", "-D", input.worktree.branch], sourceRoot);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
throw new GitWorktreeError("GIT_WORKTREE_CLOSE_FAILED", `Git failed to remove the temporary managed worktree created for a failed reopen. ${error instanceof Error ? error.message : String(error)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
60
82
|
export async function closeManagedWorktree(input) {
|
|
61
83
|
const sourceRoot = assertAllowedPath(input.worktree.sourceRoot, input.config.allowedRoots);
|
|
62
84
|
const worktreePath = assertAllowedPath(input.worktree.path, [input.config.worktreeRoot]);
|
|
@@ -38,7 +38,7 @@ function capabilityContractInstructions(config) {
|
|
|
38
38
|
const staleWorkspacePolicy = config.toolMode === "codex"
|
|
39
39
|
? ""
|
|
40
40
|
: ` If ${toolNames.openWorkspace} reports stale workspaces, let the user choose resume or ${toolNames.closeWorkspace}; never auto-close.`;
|
|
41
|
-
const workspaceLifecycle = `
|
|
41
|
+
const workspaceLifecycle = `Default to the user's existing checkout. Reuse workspaceId from ${toolNames.openWorkspace}; change it only when asked.${staleWorkspacePolicy} Only open mode=\"worktree\" when the user explicitly asks for isolated or parallel Git work. ${toolNames.closeWorkspace} preserves Workspace identity. Managed close finalizes backing and needs commitMessage. Composite close preserves members; delete removes only Composite state. Active worktree delete still finalizes safely; checkout files are never deleted.`;
|
|
42
42
|
const activityPanel = `Project-work order: ${toolNames.openWorkspace} if needed → activity_panel(workspaceId) once → work tools. activity_panel is the single ForgeRelay UI render tool: Workspace above Activity. A new workspaceId creates a new card. Never call activity_panel before needed ${toolNames.openWorkspace}.`;
|
|
43
43
|
const agents = `Follow instructions returned by ${toolNames.openWorkspace}. Read an availableAgentsFiles path before working under it.`;
|
|
44
44
|
const capabilityGuides = `For optional capabilities from ${toolNames.openWorkspace}, use ${toolNames.capability}; if unfamiliar, describe first and read its advertised capability guide with ${toolNames.read}.`;
|