@akira-tl/forgerelay 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +2 -0
- package/capabilities/workspace-tasks/GUIDE.md +23 -2
- package/dist/capability-registry.js +15 -1
- package/dist/config.js +11 -0
- package/dist/remote-workspace-relay.js +160 -17
- package/dist/server.js +334 -51
- package/dist/workspace-task-reminders.js +42 -0
- package/dist/workspace-tasks.js +69 -0
- package/dist/workspaces.js +62 -30
- package/docs/chatgpt-coding-workflow.md +13 -3
- package/docs/configuration.md +46 -11
- package/package.json +5 -2
- package/scripts/debug/accept.mjs +178 -18
- package/scripts/debug/relay-accept.mjs +890 -0
- package/scripts/wiki/sync.mjs +246 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.8.5] - 2026-08-31
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added source-controlled GitHub Wiki pages with validation and publishing tooling.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Relayed checkout and managed-worktree Workspaces now preserve stable Gateway identity and Execution-owned Task/lifecycle semantics across close, reopen, delete, inspection, and Composite member routing.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Task reminders and relayed/Composite runtime state now survive independent MCP transport sessions instead of resetting or presenting stale session-local state.
|
|
20
|
+
- Managed-worktree Relay acceptance now tolerates Git-native LF/CRLF checkout normalization, keeping the release matrix portable across Linux, macOS, and Windows.
|
|
21
|
+
|
|
22
|
+
## [0.8.4] - 2026-08-31
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Added `open_workspace(action="inspect", workspaceId=...)` for bounded read-only Workspace metadata, including safe Task summaries without opening, rebinding, or exposing bootstrap/remote-auth details.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `workspace.tasks` now discloses Task state progressively from List summaries to headers and one Task detail, with configurable forgotten-update reminders after meaningful Workspace work.
|
|
31
|
+
|
|
7
32
|
## [0.8.3] - 2026-08-31
|
|
8
33
|
|
|
9
34
|
### Added
|
package/README.md
CHANGED
|
@@ -348,6 +348,7 @@ Trusted Publishing setup.
|
|
|
348
348
|
|
|
349
349
|
## Documentation
|
|
350
350
|
|
|
351
|
+
- [GitHub Wiki](https://github.com/Akira-TL/forgerelay/wiki) — 面向用户的使用指南、概念说明与故障排查入口。
|
|
351
352
|
- [Setup Guide](docs/setup.md)
|
|
352
353
|
- [Local Debugging and 7677 Acceptance](docs/debugging.md)
|
|
353
354
|
- [ChatGPT Coding Workflow](docs/chatgpt-coding-workflow.md)
|
|
@@ -358,6 +359,7 @@ Trusted Publishing setup.
|
|
|
358
359
|
- [Troubleshooting](docs/gotchas.md)
|
|
359
360
|
- [Roadmap](docs/roadmap.md)
|
|
360
361
|
- [Versioning and Release Management](docs/versioning.md)
|
|
362
|
+
- [GitHub Wiki Maintenance](docs/maintenance/wiki.md)
|
|
361
363
|
- [Changelog](CHANGELOG.md)
|
|
362
364
|
- [Attribution Notice](NOTICE.md)
|
|
363
365
|
|
|
@@ -10,9 +10,19 @@
|
|
|
10
10
|
- Task 状态只有 `pending`、`in_progress`、`completed`。`content` 保存继续工作真正需要的要求、阻塞点、结论或下一步,而不是日志或对话转录。
|
|
11
11
|
- Task/List ID 创建后保持稳定。完成 Task 不会删除它;删除必须显式执行。
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## 渐进式读取
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Task 读取默认使用渐进式披露,不会一次返回所有 `content`:
|
|
16
|
+
|
|
17
|
+
- `operation="get"` 或 `level="summary"`:返回 List identity、状态、revision、Task 数量和未完成数量,不返回 Task headers/body。
|
|
18
|
+
- `operation="get", level="headers"`:返回 Task `id`、`status`、`subject`,不返回 `content`;可传 `listId` 只看一个 List。
|
|
19
|
+
- `operation="get", level="detail", listId=..., taskId=...`:只返回指定 Task 的完整 `content`。
|
|
20
|
+
|
|
21
|
+
先从 summary 定位 List,再按需读取 headers/detail;不要把所有 Task body 当作默认上下文。
|
|
22
|
+
|
|
23
|
+
## 修改操作
|
|
24
|
+
|
|
25
|
+
修改响应同样保持有界:List 修改返回 summary;Task 修改返回对应 List 的 headers,不会顺带回传所有 Task body。
|
|
16
26
|
|
|
17
27
|
List 操作:
|
|
18
28
|
|
|
@@ -27,3 +37,14 @@ Task 操作:
|
|
|
27
37
|
- `task.delete`:显式删除 Task。
|
|
28
38
|
|
|
29
39
|
Task 修改使用独立的 revision/fingerprint 域;它不改变 Workspace bootstrap `contextFingerprint`。
|
|
40
|
+
|
|
41
|
+
## 忘记更新提醒
|
|
42
|
+
|
|
43
|
+
ForgeRelay 会按当前 Workspace 统计成功的语义工作调用。默认连续 30 次语义工作没有 Task mutation、且仍存在 active List 中的未完成 Task 时,在工作结果后追加一条简短提醒;提醒只提示检查 `workspace.tasks`,不会携带 Task `content`。
|
|
44
|
+
|
|
45
|
+
- 任意 List/Task create/update/delete 都会重置计数。
|
|
46
|
+
- `open_workspace` inventory/open/close、Activity/UI 查询、Capability describe、Task get,以及 `bash action="process"|"output"` / Codex `write_stdin` 等已有进程 follow-up 不单独计数。
|
|
47
|
+
- `batch.execute` 作为一次顶层语义工作调用计数,而不是按 child operation 重复计数。
|
|
48
|
+
- 只有 active List 中仍有 `pending` / `in_progress` Task 才会提醒;归档 List 或全部完成会 suppress 提醒。
|
|
49
|
+
- 计数器只保存在当前 ForgeRelay 进程内,server restart 可以重置;Task durable state 本身不受影响。
|
|
50
|
+
- `FORGERELAY_TASK_REMINDER_INTERVAL=0` 可禁用提醒;其他非负整数设置间隔。
|
|
@@ -133,7 +133,21 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
133
133
|
const workspaceTaskStatus = z.enum(["pending", "in_progress", "completed"]);
|
|
134
134
|
const workspaceTaskListState = z.enum(["active", "archived"]);
|
|
135
135
|
const workspaceTasksInput = z.union([
|
|
136
|
-
z.object({
|
|
136
|
+
z.object({
|
|
137
|
+
operation: z.literal("get"),
|
|
138
|
+
level: z.literal("summary").optional(),
|
|
139
|
+
}).strict(),
|
|
140
|
+
z.object({
|
|
141
|
+
operation: z.literal("get"),
|
|
142
|
+
level: z.literal("headers"),
|
|
143
|
+
listId: z.string().min(1).optional(),
|
|
144
|
+
}).strict(),
|
|
145
|
+
z.object({
|
|
146
|
+
operation: z.literal("get"),
|
|
147
|
+
level: z.literal("detail"),
|
|
148
|
+
listId: z.string().min(1),
|
|
149
|
+
taskId: z.string().min(1),
|
|
150
|
+
}).strict(),
|
|
137
151
|
z.object({
|
|
138
152
|
operation: z.literal("list.create"),
|
|
139
153
|
name: z.string().trim().min(1),
|
package/dist/config.js
CHANGED
|
@@ -7,6 +7,7 @@ import { forgerelayAgentsDir, forgerelaySkillsDir, generateInstanceId, loadForge
|
|
|
7
7
|
const DEFAULT_OAUTH_ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
|
|
8
8
|
const DEFAULT_OAUTH_REFRESH_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
9
9
|
const DEFAULT_ARTIFACT_MAX_FILE_BYTES = 100 * 1024 * 1024;
|
|
10
|
+
const DEFAULT_TASK_REMINDER_INTERVAL = 30;
|
|
10
11
|
function parsePort(value) {
|
|
11
12
|
if (value === undefined || value === "")
|
|
12
13
|
return 7676;
|
|
@@ -109,6 +110,15 @@ function parsePositiveInteger(value, fallback, name, max = Number.MAX_SAFE_INTEG
|
|
|
109
110
|
}
|
|
110
111
|
return parsed;
|
|
111
112
|
}
|
|
113
|
+
function parseNonNegativeInteger(value, fallback, name) {
|
|
114
|
+
if (value === undefined || value === "")
|
|
115
|
+
return fallback;
|
|
116
|
+
const parsed = Number(value);
|
|
117
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > Number.MAX_SAFE_INTEGER) {
|
|
118
|
+
throw new Error(`Invalid ${name}: ${value}`);
|
|
119
|
+
}
|
|
120
|
+
return parsed;
|
|
121
|
+
}
|
|
112
122
|
function parseLoggingConfig(env, trustProxyDefault) {
|
|
113
123
|
const format = parseLogFormat(productEnv(env, "LOG_FORMAT"));
|
|
114
124
|
const requests = productEnv(env, "LOG_REQUESTS");
|
|
@@ -247,6 +257,7 @@ export function loadConfig(env = process.env) {
|
|
|
247
257
|
? files.config.artifactsEnabled === true
|
|
248
258
|
: parseBoolean(productEnv(env, "ARTIFACTS")),
|
|
249
259
|
artifactMaxFileBytes: parsePositiveInteger(productEnv(env, "ARTIFACT_MAX_FILE_BYTES") ?? numberConfigValue(files.config.artifactMaxFileBytes), DEFAULT_ARTIFACT_MAX_FILE_BYTES, "FORGERELAY_ARTIFACT_MAX_FILE_BYTES"),
|
|
260
|
+
taskReminderInterval: parseNonNegativeInteger(productEnv(env, "TASK_REMINDER_INTERVAL") ?? numberConfigValue(files.config.taskReminderInterval), DEFAULT_TASK_REMINDER_INTERVAL, "FORGERELAY_TASK_REMINDER_INTERVAL"),
|
|
250
261
|
skillsEnabled: productEnv(env, "SKILLS") === undefined ? true : parseBoolean(productEnv(env, "SKILLS")),
|
|
251
262
|
skillPaths: parsePathList(productEnv(env, "SKILL_PATHS")),
|
|
252
263
|
devspaceSkillsDir: forgerelaySkillsDir(env),
|
|
@@ -27,6 +27,58 @@ export class RemoteWorkspaceRelay {
|
|
|
27
27
|
this.loadRoutes();
|
|
28
28
|
return this.routes.has(workspaceId);
|
|
29
29
|
}
|
|
30
|
+
async inspectWorkspace(gatewayWorkspaceId) {
|
|
31
|
+
const route = this.requireRoute(gatewayWorkspaceId);
|
|
32
|
+
const resolved = this.remoteByInstance(route.remoteInstanceId);
|
|
33
|
+
let result;
|
|
34
|
+
try {
|
|
35
|
+
result = await this.callRemoteTool(resolved.alias, resolved.remote, "open_workspace", {
|
|
36
|
+
action: "inspect",
|
|
37
|
+
workspaceId: route.remoteWorkspaceId,
|
|
38
|
+
});
|
|
39
|
+
assertRemoteToolSucceeded(resolved.alias, "open_workspace", result);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw sanitizedRemoteError(error, route.remoteWorkspaceId, gatewayWorkspaceId);
|
|
43
|
+
}
|
|
44
|
+
const structured = result.structuredContent;
|
|
45
|
+
const remoteInspection = structured?.inspection;
|
|
46
|
+
if (!remoteInspection || remoteInspection.kind !== "workspace") {
|
|
47
|
+
throw new Error(`Remote ForgeRelay ${resolved.alias} inspection did not return a Workspace projection.`);
|
|
48
|
+
}
|
|
49
|
+
const root = stringField(remoteInspection, "root", "Remote Workspace inspection");
|
|
50
|
+
const mode = remoteInspection.mode;
|
|
51
|
+
if (mode !== "checkout" && mode !== "worktree") {
|
|
52
|
+
throw new Error(`Remote ForgeRelay ${resolved.alias} inspection did not return a valid Workspace mode.`);
|
|
53
|
+
}
|
|
54
|
+
const projection = {
|
|
55
|
+
workspaceId: route.gatewayWorkspaceId,
|
|
56
|
+
kind: "workspace",
|
|
57
|
+
location: "relay",
|
|
58
|
+
root,
|
|
59
|
+
routeState: "known",
|
|
60
|
+
mode,
|
|
61
|
+
relay: resolved.alias,
|
|
62
|
+
executionLocation: `remote:${resolved.alias}`,
|
|
63
|
+
};
|
|
64
|
+
copyStringField(remoteInspection, projection, "status");
|
|
65
|
+
const state = remoteInspection.state;
|
|
66
|
+
if (state === "active" || state === "stale" || state === "invalid" || state === "closed") {
|
|
67
|
+
projection.state = state;
|
|
68
|
+
}
|
|
69
|
+
copyStringField(remoteInspection, projection, "sourceRoot");
|
|
70
|
+
copyStringField(remoteInspection, projection, "branch");
|
|
71
|
+
copyStringField(remoteInspection, projection, "targetBranch");
|
|
72
|
+
copyBooleanField(remoteInspection, projection, "managed");
|
|
73
|
+
copyStringField(remoteInspection, projection, "createdAt");
|
|
74
|
+
copyStringField(remoteInspection, projection, "lastUsedAt");
|
|
75
|
+
copyNumberField(remoteInspection, projection, "idleMs");
|
|
76
|
+
copyBooleanField(remoteInspection, projection, "rootValid");
|
|
77
|
+
const taskSummary = safeTaskSummary(remoteInspection.taskSummary);
|
|
78
|
+
if (taskSummary)
|
|
79
|
+
projection.taskSummary = taskSummary;
|
|
80
|
+
return projection;
|
|
81
|
+
}
|
|
30
82
|
async openWorkspace(alias, input, conversationScopeId) {
|
|
31
83
|
const resolved = this.remoteByAlias(alias);
|
|
32
84
|
let result;
|
|
@@ -51,18 +103,15 @@ export class RemoteWorkspaceRelay {
|
|
|
51
103
|
if (mode !== "checkout" && mode !== "worktree") {
|
|
52
104
|
throw new Error("Remote open_workspace response did not include a valid workspace mode.");
|
|
53
105
|
}
|
|
54
|
-
const gatewayWorkspaceId = this.allocateGatewayWorkspaceId();
|
|
55
106
|
const sourceRoot = typeof structured?.sourceRoot === "string" ? structured.sourceRoot : undefined;
|
|
56
|
-
const route = {
|
|
57
|
-
gatewayWorkspaceId,
|
|
107
|
+
const route = this.findOrCreateRoute({
|
|
58
108
|
remoteInstanceId: resolved.remote.instanceId,
|
|
59
109
|
remoteWorkspaceId,
|
|
60
110
|
root,
|
|
61
111
|
mode,
|
|
62
112
|
...(sourceRoot ? { sourceRoot } : {}),
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
this.persistRoute(route);
|
|
113
|
+
});
|
|
114
|
+
const gatewayWorkspaceId = route.gatewayWorkspaceId;
|
|
66
115
|
const remapContext = (value) => replaceExactWorkspaceId(value, remoteWorkspaceId, gatewayWorkspaceId);
|
|
67
116
|
const remoteInstruction = typeof structured?.instruction === "string"
|
|
68
117
|
? String(remapContext(structured.instruction))
|
|
@@ -102,10 +151,24 @@ export class RemoteWorkspaceRelay {
|
|
|
102
151
|
};
|
|
103
152
|
}
|
|
104
153
|
async resumeWorkspace(gatewayWorkspaceId, context = "auto", conversationScopeId) {
|
|
154
|
+
const route = this.requireRoute(gatewayWorkspaceId);
|
|
105
155
|
const result = await this.callWorkspaceTool(gatewayWorkspaceId, "open_workspace", { context }, conversationScopeId);
|
|
106
156
|
if (result.isError === true) {
|
|
107
157
|
throw new Error(`Remote open_workspace failed: ${toolResultText(result)}`);
|
|
108
158
|
}
|
|
159
|
+
const structured = result.structuredContent;
|
|
160
|
+
const root = typeof structured?.root === "string" ? structured.root : route.root;
|
|
161
|
+
const mode = structured?.mode === "checkout" || structured?.mode === "worktree"
|
|
162
|
+
? structured.mode
|
|
163
|
+
: route.mode;
|
|
164
|
+
const sourceRoot = typeof structured?.sourceRoot === "string" ? structured.sourceRoot : route.sourceRoot;
|
|
165
|
+
this.findOrCreateRoute({
|
|
166
|
+
remoteInstanceId: route.remoteInstanceId,
|
|
167
|
+
remoteWorkspaceId: route.remoteWorkspaceId,
|
|
168
|
+
root,
|
|
169
|
+
mode,
|
|
170
|
+
...(sourceRoot ? { sourceRoot } : {}),
|
|
171
|
+
});
|
|
109
172
|
return result;
|
|
110
173
|
}
|
|
111
174
|
async read(gatewayWorkspaceId, input, conversationScopeId) {
|
|
@@ -206,14 +269,16 @@ export class RemoteWorkspaceRelay {
|
|
|
206
269
|
throw sanitizedRemoteError(error, route.remoteWorkspaceId, gatewayWorkspaceId);
|
|
207
270
|
}
|
|
208
271
|
}
|
|
209
|
-
async closeWorkspace(gatewayWorkspaceId,
|
|
272
|
+
async closeWorkspace(gatewayWorkspaceId, input = {}, conversationScopeId) {
|
|
210
273
|
const route = this.requireRoute(gatewayWorkspaceId);
|
|
211
274
|
const resolved = this.remoteByInstance(route.remoteInstanceId);
|
|
275
|
+
const action = input.action ?? "close";
|
|
212
276
|
let result;
|
|
213
277
|
try {
|
|
214
278
|
result = await this.callRemoteTool(resolved.alias, resolved.remote, "close_workspace", {
|
|
215
279
|
workspaceId: route.remoteWorkspaceId,
|
|
216
|
-
|
|
280
|
+
action,
|
|
281
|
+
...(input.commitMessage !== undefined ? { commitMessage: input.commitMessage } : {}),
|
|
217
282
|
}, conversationScopeId);
|
|
218
283
|
assertRemoteToolSucceeded(resolved.alias, "close_workspace", result);
|
|
219
284
|
}
|
|
@@ -221,21 +286,26 @@ export class RemoteWorkspaceRelay {
|
|
|
221
286
|
throw sanitizedRemoteError(error, route.remoteWorkspaceId, gatewayWorkspaceId);
|
|
222
287
|
}
|
|
223
288
|
const remoteStructured = result.structuredContent;
|
|
224
|
-
|
|
225
|
-
|
|
289
|
+
if (action === "delete") {
|
|
290
|
+
this.routes.delete(gatewayWorkspaceId);
|
|
291
|
+
this.deletePersistedRoute(gatewayWorkspaceId);
|
|
292
|
+
}
|
|
226
293
|
for (const [turnId, routedWorkspaceId] of this.turnRoutes) {
|
|
227
294
|
if (routedWorkspaceId === gatewayWorkspaceId)
|
|
228
295
|
this.turnRoutes.delete(turnId);
|
|
229
296
|
}
|
|
297
|
+
const actionText = action === "delete" ? "Deleted" : "Closed";
|
|
230
298
|
const text = route.mode === "worktree"
|
|
231
|
-
?
|
|
232
|
-
:
|
|
299
|
+
? `${actionText} relayed worktree workspace ${gatewayWorkspaceId} on remote ${resolved.alias}.`
|
|
300
|
+
: `${actionText} relayed checkout workspace ${gatewayWorkspaceId} on remote ${resolved.alias}.`;
|
|
233
301
|
const structuredContent = {
|
|
234
302
|
result: text,
|
|
235
303
|
workspaceId: gatewayWorkspaceId,
|
|
304
|
+
action,
|
|
236
305
|
mode: route.mode,
|
|
237
306
|
};
|
|
238
307
|
for (const field of [
|
|
308
|
+
"status",
|
|
239
309
|
"sourceRoot",
|
|
240
310
|
"branch",
|
|
241
311
|
"targetBranch",
|
|
@@ -255,6 +325,7 @@ export class RemoteWorkspaceRelay {
|
|
|
255
325
|
tool: "close_workspace",
|
|
256
326
|
card: {
|
|
257
327
|
workspaceId: gatewayWorkspaceId,
|
|
328
|
+
action,
|
|
258
329
|
mode: route.mode,
|
|
259
330
|
payload: { content: [{ type: "text", text }] },
|
|
260
331
|
},
|
|
@@ -277,10 +348,26 @@ export class RemoteWorkspaceRelay {
|
|
|
277
348
|
this.routes.set(workspaceId, route);
|
|
278
349
|
}
|
|
279
350
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
351
|
+
findOrCreateRoute(input) {
|
|
352
|
+
let selected;
|
|
353
|
+
mkdirSync(this.routeStateDir, { recursive: true });
|
|
354
|
+
this.withRouteFileLock(() => {
|
|
355
|
+
const routes = this.readRoutesFromDisk();
|
|
356
|
+
const existing = [...routes.values()]
|
|
357
|
+
.filter((route) => route.remoteInstanceId === input.remoteInstanceId &&
|
|
358
|
+
route.remoteWorkspaceId === input.remoteWorkspaceId)
|
|
359
|
+
.sort((left, right) => left.gatewayWorkspaceId.localeCompare(right.gatewayWorkspaceId))[0];
|
|
360
|
+
selected = {
|
|
361
|
+
gatewayWorkspaceId: existing?.gatewayWorkspaceId ?? this.allocateGatewayWorkspaceId(routes),
|
|
362
|
+
...input,
|
|
363
|
+
};
|
|
364
|
+
routes.set(selected.gatewayWorkspaceId, selected);
|
|
365
|
+
this.writeRoutesToDisk(routes);
|
|
283
366
|
});
|
|
367
|
+
if (!selected)
|
|
368
|
+
throw new Error("Failed to persist relayed Workspace route.");
|
|
369
|
+
this.routes.set(selected.gatewayWorkspaceId, selected);
|
|
370
|
+
return selected;
|
|
284
371
|
}
|
|
285
372
|
deletePersistedRoute(workspaceId) {
|
|
286
373
|
this.updatePersistedRoutes((routes) => {
|
|
@@ -408,11 +495,11 @@ export class RemoteWorkspaceRelay {
|
|
|
408
495
|
writeForgeRelayRemote(alias, refreshed, this.authEnv);
|
|
409
496
|
return refreshed;
|
|
410
497
|
}
|
|
411
|
-
allocateGatewayWorkspaceId() {
|
|
498
|
+
allocateGatewayWorkspaceId(routes = this.routes) {
|
|
412
499
|
let workspaceId;
|
|
413
500
|
do {
|
|
414
501
|
workspaceId = `rws_${randomBytes(5).toString("hex")}`;
|
|
415
|
-
} while (
|
|
502
|
+
} while (routes.has(workspaceId));
|
|
416
503
|
return workspaceId;
|
|
417
504
|
}
|
|
418
505
|
}
|
|
@@ -428,6 +515,62 @@ function stringField(structured, field, label) {
|
|
|
428
515
|
}
|
|
429
516
|
return value;
|
|
430
517
|
}
|
|
518
|
+
function copyStringField(source, target, field) {
|
|
519
|
+
const value = source[field];
|
|
520
|
+
if (typeof value === "string")
|
|
521
|
+
target[field] = value;
|
|
522
|
+
}
|
|
523
|
+
function copyBooleanField(source, target, field) {
|
|
524
|
+
const value = source[field];
|
|
525
|
+
if (typeof value === "boolean")
|
|
526
|
+
target[field] = value;
|
|
527
|
+
}
|
|
528
|
+
function copyNumberField(source, target, field) {
|
|
529
|
+
const value = source[field];
|
|
530
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
531
|
+
target[field] = value;
|
|
532
|
+
}
|
|
533
|
+
function safeTaskSummary(value) {
|
|
534
|
+
if (!value || typeof value !== "object")
|
|
535
|
+
return undefined;
|
|
536
|
+
const summary = value;
|
|
537
|
+
if (summary.level !== "summary" ||
|
|
538
|
+
summary.version !== 1 ||
|
|
539
|
+
typeof summary.revision !== "number" ||
|
|
540
|
+
!Number.isInteger(summary.revision) ||
|
|
541
|
+
summary.revision < 0 ||
|
|
542
|
+
!Array.isArray(summary.lists)) {
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
const lists = [];
|
|
546
|
+
for (const value of summary.lists) {
|
|
547
|
+
if (!value || typeof value !== "object")
|
|
548
|
+
return undefined;
|
|
549
|
+
const list = value;
|
|
550
|
+
if (typeof list.id !== "string" ||
|
|
551
|
+
typeof list.name !== "string" ||
|
|
552
|
+
(list.state !== "active" && list.state !== "archived") ||
|
|
553
|
+
typeof list.revision !== "number" || !Number.isInteger(list.revision) || list.revision <= 0 ||
|
|
554
|
+
typeof list.taskCount !== "number" || !Number.isInteger(list.taskCount) || list.taskCount < 0 ||
|
|
555
|
+
typeof list.unfinishedTaskCount !== "number" || !Number.isInteger(list.unfinishedTaskCount) || list.unfinishedTaskCount < 0) {
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
lists.push({
|
|
559
|
+
id: list.id,
|
|
560
|
+
name: list.name,
|
|
561
|
+
state: list.state,
|
|
562
|
+
revision: list.revision,
|
|
563
|
+
taskCount: list.taskCount,
|
|
564
|
+
unfinishedTaskCount: list.unfinishedTaskCount,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
level: "summary",
|
|
569
|
+
version: 1,
|
|
570
|
+
revision: summary.revision,
|
|
571
|
+
lists,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
431
574
|
function toolResultText(result) {
|
|
432
575
|
return (result.content ?? [])
|
|
433
576
|
.filter((entry) => entry.type === "text")
|