@akira-tl/forgerelay 0.8.3 → 0.8.4
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 +10 -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 +15 -0
- package/dist/server.js +284 -40
- 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 +2 -2
- package/scripts/debug/accept.mjs +178 -18
package/dist/workspace-tasks.js
CHANGED
|
@@ -80,6 +80,20 @@ export class WorkspaceTaskStore {
|
|
|
80
80
|
read(workspaceId) {
|
|
81
81
|
return this.ensureWorkspace(workspaceId);
|
|
82
82
|
}
|
|
83
|
+
readSummary(workspaceId) {
|
|
84
|
+
return taskSummary(this.ensureWorkspace(workspaceId));
|
|
85
|
+
}
|
|
86
|
+
inspectSummary(workspaceId) {
|
|
87
|
+
const id = normalizeWorkspaceId(workspaceId);
|
|
88
|
+
const loaded = this.tryReadState(id);
|
|
89
|
+
return loaded ? taskSummary(snapshot(loaded.state, loaded.fingerprint)) : undefined;
|
|
90
|
+
}
|
|
91
|
+
readHeaders(workspaceId, listId) {
|
|
92
|
+
return taskHeaders(this.ensureWorkspace(workspaceId), listId);
|
|
93
|
+
}
|
|
94
|
+
readTaskDetail(workspaceId, listId, taskId) {
|
|
95
|
+
return taskDetail(this.ensureWorkspace(workspaceId), listId, taskId);
|
|
96
|
+
}
|
|
83
97
|
createList(workspaceId, input) {
|
|
84
98
|
return this.mutate(workspaceId, (state) => {
|
|
85
99
|
if (state.lists.length >= MAX_TASK_LISTS) {
|
|
@@ -266,6 +280,61 @@ function snapshot(state, stateFingerprint) {
|
|
|
266
280
|
fingerprint: stateFingerprint,
|
|
267
281
|
};
|
|
268
282
|
}
|
|
283
|
+
function taskSummary(snapshotValue) {
|
|
284
|
+
return {
|
|
285
|
+
level: "summary",
|
|
286
|
+
version: snapshotValue.version,
|
|
287
|
+
revision: snapshotValue.revision,
|
|
288
|
+
fingerprint: snapshotValue.fingerprint,
|
|
289
|
+
lists: snapshotValue.lists.map((list) => ({
|
|
290
|
+
id: list.id,
|
|
291
|
+
name: list.name,
|
|
292
|
+
state: list.state,
|
|
293
|
+
revision: list.revision,
|
|
294
|
+
taskCount: list.tasks.length,
|
|
295
|
+
unfinishedTaskCount: list.tasks.filter((task) => task.status !== "completed").length,
|
|
296
|
+
})),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function taskHeaders(snapshotValue, listId) {
|
|
300
|
+
const lists = listId === undefined
|
|
301
|
+
? snapshotValue.lists
|
|
302
|
+
: [snapshotValue.lists[requireListIndex(snapshotValue, listId)]];
|
|
303
|
+
return {
|
|
304
|
+
level: "headers",
|
|
305
|
+
version: snapshotValue.version,
|
|
306
|
+
revision: snapshotValue.revision,
|
|
307
|
+
fingerprint: snapshotValue.fingerprint,
|
|
308
|
+
lists: lists.map((list) => ({
|
|
309
|
+
id: list.id,
|
|
310
|
+
name: list.name,
|
|
311
|
+
state: list.state,
|
|
312
|
+
revision: list.revision,
|
|
313
|
+
tasks: list.tasks.map((task) => ({
|
|
314
|
+
id: task.id,
|
|
315
|
+
status: task.status,
|
|
316
|
+
subject: task.subject,
|
|
317
|
+
})),
|
|
318
|
+
})),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function taskDetail(snapshotValue, listId, taskId) {
|
|
322
|
+
const list = requireList(snapshotValue, listId);
|
|
323
|
+
const task = list.tasks[requireTaskIndex(list, taskId)];
|
|
324
|
+
return {
|
|
325
|
+
level: "detail",
|
|
326
|
+
version: snapshotValue.version,
|
|
327
|
+
revision: snapshotValue.revision,
|
|
328
|
+
fingerprint: snapshotValue.fingerprint,
|
|
329
|
+
list: {
|
|
330
|
+
id: list.id,
|
|
331
|
+
name: list.name,
|
|
332
|
+
state: list.state,
|
|
333
|
+
revision: list.revision,
|
|
334
|
+
},
|
|
335
|
+
task: { ...task },
|
|
336
|
+
};
|
|
337
|
+
}
|
|
269
338
|
function cloneState(state) {
|
|
270
339
|
return {
|
|
271
340
|
version: state.version,
|
package/dist/workspaces.js
CHANGED
|
@@ -46,6 +46,37 @@ export class WorkspaceRegistry {
|
|
|
46
46
|
}
|
|
47
47
|
return this.openReusableCheckout(workspaceInput.path, openOptions.conversationScopeId, bootstrapContext);
|
|
48
48
|
}
|
|
49
|
+
async inspectWorkspace(workspaceId) {
|
|
50
|
+
let session = this.store?.getSession(workspaceId);
|
|
51
|
+
if (!session) {
|
|
52
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
53
|
+
if (workspace) {
|
|
54
|
+
session = {
|
|
55
|
+
id: workspace.id,
|
|
56
|
+
root: workspace.root,
|
|
57
|
+
status: "active",
|
|
58
|
+
mode: workspace.mode,
|
|
59
|
+
sourceRoot: workspace.sourceRoot,
|
|
60
|
+
baseRef: workspace.worktree?.baseRef,
|
|
61
|
+
baseSha: workspace.worktree?.baseSha,
|
|
62
|
+
branch: workspace.worktree?.branch,
|
|
63
|
+
targetBranch: workspace.worktree?.targetBranch,
|
|
64
|
+
managed: workspace.worktree?.managed ?? false,
|
|
65
|
+
createdAt: "",
|
|
66
|
+
lastUsedAt: "",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!session)
|
|
71
|
+
throw new Error(`Unknown workspaceId: ${workspaceId}.`);
|
|
72
|
+
const entry = await this.inventoryEntryForSession(session, Date.now(), false);
|
|
73
|
+
const { current: _current, ...inspection } = entry;
|
|
74
|
+
return {
|
|
75
|
+
kind: "workspace",
|
|
76
|
+
location: "local",
|
|
77
|
+
...inspection,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
49
80
|
async listWorkspaces(input = {}, openOptions = {}) {
|
|
50
81
|
this.pruneIdleWorkspaceSessions(openOptions.protectedWorkspaceIds ?? new Set());
|
|
51
82
|
const now = Date.now();
|
|
@@ -77,36 +108,7 @@ export class WorkspaceRegistry {
|
|
|
77
108
|
const rootKey = input.root
|
|
78
109
|
? await canonicalPath(assertAllowedPath(input.root, [...this.config.allowedRoots, this.config.worktreeRoot]))
|
|
79
110
|
: undefined;
|
|
80
|
-
const entries = await Promise.all(sessions.map(
|
|
81
|
-
const rootValid = await this.validSessionRoot(session) !== undefined;
|
|
82
|
-
const lastUsedAt = Date.parse(session.lastUsedAt);
|
|
83
|
-
const idleMs = Number.isFinite(lastUsedAt) ? Math.max(0, now - lastUsedAt) : 0;
|
|
84
|
-
const state = session.status !== "active"
|
|
85
|
-
? "closed"
|
|
86
|
-
: !rootValid
|
|
87
|
-
? "invalid"
|
|
88
|
-
: idleMs >= WORKSPACE_STALE_REMINDER_MS
|
|
89
|
-
? "stale"
|
|
90
|
-
: "active";
|
|
91
|
-
const projectRoot = session.sourceRoot ?? session.root;
|
|
92
|
-
return {
|
|
93
|
-
label: `${basename(resolve(projectRoot)) || "workspace"}/${session.id}`,
|
|
94
|
-
workspaceId: session.id,
|
|
95
|
-
root: session.root,
|
|
96
|
-
status: session.status,
|
|
97
|
-
state,
|
|
98
|
-
mode: session.mode,
|
|
99
|
-
sourceRoot: session.sourceRoot,
|
|
100
|
-
branch: session.branch,
|
|
101
|
-
targetBranch: session.targetBranch,
|
|
102
|
-
managed: session.managed,
|
|
103
|
-
createdAt: session.createdAt,
|
|
104
|
-
lastUsedAt: session.lastUsedAt,
|
|
105
|
-
idleMs,
|
|
106
|
-
rootValid,
|
|
107
|
-
current: currentWorkspaceIds.has(session.id),
|
|
108
|
-
};
|
|
109
|
-
}));
|
|
111
|
+
const entries = await Promise.all(sessions.map((session) => this.inventoryEntryForSession(session, now, currentWorkspaceIds.has(session.id))));
|
|
110
112
|
const filtered = [];
|
|
111
113
|
for (let index = 0; index < sessions.length; index += 1) {
|
|
112
114
|
const session = sessions[index];
|
|
@@ -151,6 +153,36 @@ export class WorkspaceRegistry {
|
|
|
151
153
|
},
|
|
152
154
|
};
|
|
153
155
|
}
|
|
156
|
+
async inventoryEntryForSession(session, now, current) {
|
|
157
|
+
const rootValid = await this.validSessionRoot(session) !== undefined;
|
|
158
|
+
const lastUsedAt = Date.parse(session.lastUsedAt);
|
|
159
|
+
const idleMs = Number.isFinite(lastUsedAt) ? Math.max(0, now - lastUsedAt) : 0;
|
|
160
|
+
const state = session.status !== "active"
|
|
161
|
+
? "closed"
|
|
162
|
+
: !rootValid
|
|
163
|
+
? "invalid"
|
|
164
|
+
: idleMs >= WORKSPACE_STALE_REMINDER_MS
|
|
165
|
+
? "stale"
|
|
166
|
+
: "active";
|
|
167
|
+
const projectRoot = session.sourceRoot ?? session.root;
|
|
168
|
+
return {
|
|
169
|
+
label: `${basename(resolve(projectRoot)) || "workspace"}/${session.id}`,
|
|
170
|
+
workspaceId: session.id,
|
|
171
|
+
root: session.root,
|
|
172
|
+
status: session.status,
|
|
173
|
+
state,
|
|
174
|
+
mode: session.mode,
|
|
175
|
+
sourceRoot: session.sourceRoot,
|
|
176
|
+
branch: session.branch,
|
|
177
|
+
targetBranch: session.targetBranch,
|
|
178
|
+
managed: session.managed,
|
|
179
|
+
createdAt: session.createdAt,
|
|
180
|
+
lastUsedAt: session.lastUsedAt,
|
|
181
|
+
idleMs,
|
|
182
|
+
rootValid,
|
|
183
|
+
current,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
154
186
|
async resumeWorkspace(workspaceId, conversationScopeId, bootstrapContext = "auto") {
|
|
155
187
|
const session = this.store?.getSession(workspaceId);
|
|
156
188
|
const context = session?.status === "closed" && session.mode === "worktree"
|
|
@@ -49,9 +49,9 @@ returning the full project context and does not record the current fingerprint a
|
|
|
49
49
|
already delivered. Context-delivery state remains conversation-scoped and does not
|
|
50
50
|
change the persistent Workspace identity.
|
|
51
51
|
|
|
52
|
-
Do not enumerate Workspace state on every normal open.
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
Do not enumerate Workspace state on every normal open. Use the same Core tool in
|
|
53
|
+
inventory mode only when the user wants to discover known Workspaces, continue earlier
|
|
54
|
+
work, or organize accumulated state:
|
|
55
55
|
|
|
56
56
|
```text
|
|
57
57
|
open_workspace(action="list")
|
|
@@ -66,6 +66,16 @@ root/source root, or stale-only state. Entries include a compact label such as
|
|
|
66
66
|
root validity, and whether that Workspace is currently selected by this
|
|
67
67
|
conversation. Listing is observational and does not refresh `lastUsedAt`.
|
|
68
68
|
|
|
69
|
+
When one known Workspace needs more bounded detail but must not be opened or resumed,
|
|
70
|
+
use `open_workspace(action="inspect", workspaceId="...")`. Inspection is a strict
|
|
71
|
+
allowlist projection: ordinary/worktree lifecycle metadata, Composite member
|
|
72
|
+
summaries, safe Relay presentation metadata, and an existing Task List summary may be
|
|
73
|
+
returned. It does not return bootstrap instructions, Skills, Capability guides,
|
|
74
|
+
Subagent bodies, files, process/Activity output, credentials, routes, or Task bodies;
|
|
75
|
+
it does not bind the conversation, mark bootstrap delivered, refresh `lastUsedAt`, or
|
|
76
|
+
grant execution authority. Explicitly open the target Workspace before mutating or
|
|
77
|
+
executing against it.
|
|
78
|
+
|
|
69
79
|
Treat persisted status and derived state separately. `status="active"` means the
|
|
70
80
|
record has not been explicitly closed. A valid recent record has `state="active"`;
|
|
71
81
|
a valid active record idle for more than two days has `state="stale"`; an active
|
package/docs/configuration.md
CHANGED
|
@@ -327,17 +327,52 @@ Hooks, Skills, language services, and Activity facts remain owned by the underly
|
|
|
327
327
|
Workspace. The Composite Activity Panel only aggregates their presentation into one
|
|
328
328
|
Host Turn.
|
|
329
329
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
`
|
|
336
|
-
`
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
330
|
+
## Workspace Tasks
|
|
331
|
+
|
|
332
|
+
`workspace.tasks` keeps lightweight Task Lists in ForgeRelay-owned private Workspace
|
|
333
|
+
state. Task data is not project bootstrap context and is not written into checkout or
|
|
334
|
+
managed-worktree Git contents. Reads use progressive disclosure: default `get` returns
|
|
335
|
+
List summaries and unfinished counts, `level="headers"` adds Task ID/status/subject,
|
|
336
|
+
and `level="detail"` returns full content for one explicitly selected Task.
|
|
337
|
+
|
|
338
|
+
ForgeRelay can append a bounded forgotten-update reminder after semantic Workspace
|
|
339
|
+
work when active Lists still contain unfinished Tasks. The counter defaults to 30
|
|
340
|
+
successful semantic work calls, resets on any Task mutation, treats one
|
|
341
|
+
`batch.execute` as one call, and does not count Workspace inventory/lifecycle UI,
|
|
342
|
+
Activity queries, Task reads, or Bash/process follow-up polling/input as new work.
|
|
343
|
+
The counter is process-local and may reset on server restart; Task state itself
|
|
344
|
+
remains durable.
|
|
345
|
+
|
|
346
|
+
| Variable | Default | Purpose |
|
|
347
|
+
| --- | --- | --- |
|
|
348
|
+
| `FORGERELAY_TASK_REMINDER_INTERVAL` | `30` | Successful semantic work calls between Task update reminders; `0` disables reminders. |
|
|
349
|
+
|
|
350
|
+
The same value may be persisted as `taskReminderInterval` in `config.json`. The
|
|
351
|
+
legacy-compatible `DEVSPACE_TASK_REMINDER_INTERVAL` environment name is also accepted.
|
|
352
|
+
|
|
353
|
+
Use `open_workspace(action="list")` only when the Agent needs lightweight inventory
|
|
354
|
+
to discover known Workspaces, continue earlier work, or organize Workspace state. The
|
|
355
|
+
inventory is paginated (50 records by default, at most 100) and can filter by Workspace
|
|
356
|
+
ID, persisted status, derived state, mode, canonical root/source root, or stale-only
|
|
357
|
+
state. Reading inventory does not refresh `lastUsedAt`. Persisted `status="active"`
|
|
358
|
+
means the record has not been explicitly closed; the derived `state` distinguishes
|
|
359
|
+
`active`, `stale`, `invalid`, and `closed`. A missing checkout or externally removed
|
|
360
|
+
managed-worktree root can therefore remain diagnostically `status="active"` while
|
|
361
|
+
appearing as `state="invalid"`. Canonical identity means ordinary same-target opens no
|
|
362
|
+
longer accumulate duplicate inventory rows; `action="list"` remains the formal
|
|
363
|
+
on-demand inventory path.
|
|
364
|
+
|
|
365
|
+
`open_workspace(action="inspect", workspaceId="...")` is the bounded read-only detail
|
|
366
|
+
path for one known Workspace. It uses an explicit allowlist and never opens/resumes the
|
|
367
|
+
target, changes conversation bindings or bootstrap-delivery records, refreshes
|
|
368
|
+
`lastUsedAt`, or grants file/process/Git/Capability authority. Safe projections include
|
|
369
|
+
ordinary/worktree lifecycle metadata, Composite member availability summaries, Relay
|
|
370
|
+
alias/execution-location presentation metadata, and an already-existing Task List
|
|
371
|
+
summary. Inspection never returns AGENTS/CLAUDE contents, Skills, Capability-guide
|
|
372
|
+
paths or contents, Subagent bodies/sessions, files, Git diffs, process/Activity output,
|
|
373
|
+
Hook/review artifacts, credentials, network/SSH routes, or Task bodies. Relay inspection
|
|
374
|
+
reports only that the Gateway route is known; it does not probe or claim the remote
|
|
375
|
+
Workspace lifecycle state.
|
|
341
376
|
|
|
342
377
|
For checkout-backed Workspaces, `close_workspace` defaults to `action="close"`:
|
|
343
378
|
it marks the persistent Workspace closed, removes current conversation bindings, and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"release:publish": "node scripts/release/publish.mjs",
|
|
48
48
|
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
|
|
49
49
|
"start": "node dist/cli.js serve",
|
|
50
|
-
"test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/release-version.test.mjs && tsx src/oauth/router.test.ts && tsx src/remote-auth-cli.test.ts && tsx src/remote-ssh-auth-cli.test.ts && tsx src/remote-workspace-relay.test.ts && tsx src/remote-workspace-relay-process.test.ts && tsx src/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.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/workspace-lifecycle-app.test.ts && tsx src/ui/activity/model.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/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/operations/edit-preflight.test.ts && tsx src/skills.test.ts && tsx src/db/migrations.test.ts && tsx src/workspace-store.test.ts && tsx src/workspace-tasks.test.ts && tsx src/activity/audit-store.test.ts && tsx src/activity/bash-output-store.test.ts && tsx src/activity/lifecycle.test.ts && tsx src/activity/query-service.test.ts && tsx src/operations/core-operation-executor.test.ts && tsx src/operations/bulk-mutation.test.ts && tsx src/operations/batch/scheduler.test.ts && tsx src/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
50
|
+
"test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/release-version.test.mjs && tsx src/oauth/router.test.ts && tsx src/remote-auth-cli.test.ts && tsx src/remote-ssh-auth-cli.test.ts && tsx src/remote-workspace-relay.test.ts && tsx src/remote-workspace-relay-process.test.ts && tsx src/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.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/workspace-lifecycle-app.test.ts && tsx src/ui/activity/model.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/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/operations/edit-preflight.test.ts && tsx src/skills.test.ts && tsx src/db/migrations.test.ts && tsx src/workspace-store.test.ts && tsx src/workspace-tasks.test.ts && tsx src/workspace-task-reminders.test.ts && tsx src/activity/audit-store.test.ts && tsx src/activity/bash-output-store.test.ts && tsx src/activity/lifecycle.test.ts && tsx src/activity/query-service.test.ts && tsx src/operations/core-operation-executor.test.ts && tsx src/operations/bulk-mutation.test.ts && tsx src/operations/batch/scheduler.test.ts && tsx src/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
51
51
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
52
52
|
"release:check": "node scripts/release-version.mjs check",
|
|
53
53
|
"release:tag-check": "node scripts/release-version.mjs tag",
|
package/scripts/debug/accept.mjs
CHANGED
|
@@ -48,6 +48,10 @@ mkdirSync(acceptanceRoot, { recursive: true });
|
|
|
48
48
|
setupGitProject(checkoutWorkspace);
|
|
49
49
|
setupGitProject(lifecycleDeleteWorkspace);
|
|
50
50
|
writeFileSync(join(lifecycleDeleteWorkspace, "keep.txt"), "keep checkout files\n");
|
|
51
|
+
writeFileSync(
|
|
52
|
+
join(lifecycleDeleteWorkspace, "AGENTS.md"),
|
|
53
|
+
"INSPECTION_ACCEPTANCE_BOOTSTRAP_SECRET\n",
|
|
54
|
+
);
|
|
51
55
|
setupCodeIntelligenceAcceptanceProject({
|
|
52
56
|
root: checkoutWorkspace,
|
|
53
57
|
fakeLanguageServer,
|
|
@@ -316,16 +320,77 @@ try {
|
|
|
316
320
|
workspaceId,
|
|
317
321
|
name: "workspace.tasks",
|
|
318
322
|
action: "run",
|
|
319
|
-
arguments: {
|
|
323
|
+
arguments: {
|
|
324
|
+
operation: "get",
|
|
325
|
+
level: "detail",
|
|
326
|
+
listId: checkoutListId,
|
|
327
|
+
taskId: checkoutTaskId,
|
|
328
|
+
},
|
|
320
329
|
});
|
|
321
330
|
assert.equal(reloadedCheckoutTasks.isError, undefined);
|
|
322
|
-
assert.equal(reloadedCheckoutTasks.structuredContent.result.
|
|
323
|
-
assert.equal(
|
|
324
|
-
reloadedCheckoutTasks.structuredContent.result.lists[0].tasks[0].content,
|
|
325
|
-
"reloaded external task edit",
|
|
326
|
-
);
|
|
331
|
+
assert.equal(reloadedCheckoutTasks.structuredContent.result.task.id, checkoutTaskId);
|
|
332
|
+
assert.equal(reloadedCheckoutTasks.structuredContent.result.task.content, "reloaded external task edit");
|
|
327
333
|
assert.notEqual(reloadedCheckoutTasks.structuredContent.result.fingerprint, taskFingerprintBeforeExternal);
|
|
328
334
|
|
|
335
|
+
const checkoutTaskSummary = callTool(oauth.accessToken, sessionId, 150, "capability", {
|
|
336
|
+
workspaceId,
|
|
337
|
+
name: "workspace.tasks",
|
|
338
|
+
action: "run",
|
|
339
|
+
arguments: { operation: "get" },
|
|
340
|
+
});
|
|
341
|
+
const summaryProjection = checkoutTaskSummary.structuredContent.result;
|
|
342
|
+
assert.equal(summaryProjection.level, "summary");
|
|
343
|
+
assert.equal(summaryProjection.lists[0].id, checkoutListId);
|
|
344
|
+
assert.equal(summaryProjection.lists[0].taskCount, 1);
|
|
345
|
+
assert.equal(summaryProjection.lists[0].unfinishedTaskCount, 1);
|
|
346
|
+
assert.equal(JSON.stringify(summaryProjection).includes(checkoutTaskId), false);
|
|
347
|
+
assert.equal(JSON.stringify(summaryProjection).includes("reloaded external task edit"), false);
|
|
348
|
+
|
|
349
|
+
const checkoutTaskHeaders = callTool(oauth.accessToken, sessionId, 151, "capability", {
|
|
350
|
+
workspaceId,
|
|
351
|
+
name: "workspace.tasks",
|
|
352
|
+
action: "run",
|
|
353
|
+
arguments: { operation: "get", level: "headers", listId: checkoutListId },
|
|
354
|
+
});
|
|
355
|
+
const headersProjection = checkoutTaskHeaders.structuredContent.result;
|
|
356
|
+
assert.equal(headersProjection.level, "headers");
|
|
357
|
+
assert.equal(headersProjection.lists.length, 1);
|
|
358
|
+
assert.equal(headersProjection.lists[0].tasks[0].id, checkoutTaskId);
|
|
359
|
+
assert.equal(headersProjection.lists[0].tasks[0].subject, "Verify v0.8.3 Task persistence");
|
|
360
|
+
assert.equal("content" in headersProjection.lists[0].tasks[0], false);
|
|
361
|
+
|
|
362
|
+
const checkoutTaskDetail = callTool(oauth.accessToken, sessionId, 152, "capability", {
|
|
363
|
+
workspaceId,
|
|
364
|
+
name: "workspace.tasks",
|
|
365
|
+
action: "run",
|
|
366
|
+
arguments: {
|
|
367
|
+
operation: "get",
|
|
368
|
+
level: "detail",
|
|
369
|
+
listId: checkoutListId,
|
|
370
|
+
taskId: checkoutTaskId,
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
const detailProjection = checkoutTaskDetail.structuredContent.result;
|
|
374
|
+
assert.equal(detailProjection.level, "detail");
|
|
375
|
+
assert.equal(detailProjection.task.id, checkoutTaskId);
|
|
376
|
+
assert.equal(detailProjection.task.content, "reloaded external task edit");
|
|
377
|
+
pass("workspace.tasks progressive disclosure", "summary -> headers -> one Task detail through real MCP");
|
|
378
|
+
|
|
379
|
+
for (let index = 0; index < 29; index += 1) {
|
|
380
|
+
const semanticWork = callTool(oauth.accessToken, sessionId, 160 + index, "read", {
|
|
381
|
+
workspaceId,
|
|
382
|
+
path: "README.md",
|
|
383
|
+
});
|
|
384
|
+
assert.doesNotMatch(toolText(semanticWork), /Reminder: this Workspace has unfinished active Tasks/);
|
|
385
|
+
}
|
|
386
|
+
const reminderWork = callTool(oauth.accessToken, sessionId, 189, "read", {
|
|
387
|
+
workspaceId,
|
|
388
|
+
path: "README.md",
|
|
389
|
+
});
|
|
390
|
+
assert.match(toolText(reminderWork), /Reminder: this Workspace has unfinished active Tasks/);
|
|
391
|
+
assert.equal(toolText(reminderWork).includes("reloaded external task edit"), false);
|
|
392
|
+
pass("workspace.tasks reminder", "default 30 semantic work calls emitted one body-free reminder");
|
|
393
|
+
|
|
329
394
|
const repeatedOpen = callTool(oauth.accessToken, sessionId, 84, "open_workspace", {
|
|
330
395
|
path: checkoutWorkspace,
|
|
331
396
|
newWorkspace: true,
|
|
@@ -391,14 +456,16 @@ try {
|
|
|
391
456
|
workspaceId,
|
|
392
457
|
name: "workspace.tasks",
|
|
393
458
|
action: "run",
|
|
394
|
-
arguments: {
|
|
459
|
+
arguments: {
|
|
460
|
+
operation: "get",
|
|
461
|
+
level: "detail",
|
|
462
|
+
listId: checkoutListId,
|
|
463
|
+
taskId: checkoutTaskId,
|
|
464
|
+
},
|
|
395
465
|
});
|
|
396
466
|
assert.equal(resumedCheckoutTasks.isError, undefined);
|
|
397
|
-
assert.equal(resumedCheckoutTasks.structuredContent.result.
|
|
398
|
-
assert.equal(
|
|
399
|
-
resumedCheckoutTasks.structuredContent.result.lists[0].tasks[0].content,
|
|
400
|
-
"reloaded external task edit",
|
|
401
|
-
);
|
|
467
|
+
assert.equal(resumedCheckoutTasks.structuredContent.result.task.id, checkoutTaskId);
|
|
468
|
+
assert.equal(resumedCheckoutTasks.structuredContent.result.task.content, "reloaded external task edit");
|
|
402
469
|
assert.equal(resumedOriginal.structuredContent.contextFingerprint, opened.structuredContent.contextFingerprint);
|
|
403
470
|
|
|
404
471
|
const deleteOpened = callTool(oauth.accessToken, sessionId, 91, "open_workspace", {
|
|
@@ -414,7 +481,72 @@ try {
|
|
|
414
481
|
arguments: { operation: "list.create", name: "delete with workspace" },
|
|
415
482
|
});
|
|
416
483
|
assert.equal(deleteTaskList.isError, undefined);
|
|
484
|
+
const deleteTaskListId = deleteTaskList.structuredContent.result.lists[0].id;
|
|
485
|
+
const inspectionTask = callTool(oauth.accessToken, sessionId, 190, "capability", {
|
|
486
|
+
workspaceId: deleteWorkspaceId,
|
|
487
|
+
name: "workspace.tasks",
|
|
488
|
+
action: "run",
|
|
489
|
+
arguments: {
|
|
490
|
+
operation: "task.create",
|
|
491
|
+
listId: deleteTaskListId,
|
|
492
|
+
subject: "Inspect safely",
|
|
493
|
+
content: "INSPECTION_ACCEPTANCE_TASK_BODY_SECRET",
|
|
494
|
+
status: "in_progress",
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
assert.equal(inspectionTask.isError, undefined);
|
|
417
498
|
assert.ok(existsSync(deleteTaskStatePath));
|
|
499
|
+
|
|
500
|
+
const inspectionBefore = callTool(oauth.accessToken, sessionId, 191, "open_workspace", {
|
|
501
|
+
action: "list",
|
|
502
|
+
workspaceId: deleteWorkspaceId,
|
|
503
|
+
}, workspaceConversationMeta);
|
|
504
|
+
const inspectionBeforeEntry = inspectionBefore.structuredContent.workspaces[0];
|
|
505
|
+
assert.equal(inspectionBeforeEntry.current, false);
|
|
506
|
+
const inspectedWorkspace = callTool(oauth.accessToken, sessionId, 192, "open_workspace", {
|
|
507
|
+
action: "inspect",
|
|
508
|
+
workspaceId: deleteWorkspaceId,
|
|
509
|
+
}, workspaceConversationMeta);
|
|
510
|
+
assert.equal(inspectedWorkspace.structuredContent.action, "inspect");
|
|
511
|
+
const inspectionProjection = inspectedWorkspace.structuredContent.inspection;
|
|
512
|
+
assert.equal(inspectionProjection.workspaceId, deleteWorkspaceId);
|
|
513
|
+
assert.equal(inspectionProjection.kind, "workspace");
|
|
514
|
+
assert.equal(inspectionProjection.location, "local");
|
|
515
|
+
assert.equal(inspectionProjection.root, lifecycleDeleteWorkspace);
|
|
516
|
+
assert.equal(inspectionProjection.taskSummary.level, "summary");
|
|
517
|
+
assert.equal(inspectionProjection.taskSummary.lists[0].taskCount, 1);
|
|
518
|
+
assert.equal(inspectionProjection.taskSummary.lists[0].unfinishedTaskCount, 1);
|
|
519
|
+
const inspectionJson = JSON.stringify(inspectedWorkspace);
|
|
520
|
+
for (const forbidden of [
|
|
521
|
+
"INSPECTION_ACCEPTANCE_BOOTSTRAP_SECRET",
|
|
522
|
+
"INSPECTION_ACCEPTANCE_TASK_BODY_SECRET",
|
|
523
|
+
"\"fingerprint\"",
|
|
524
|
+
"\"agentsFiles\"",
|
|
525
|
+
"\"availableAgentsFiles\"",
|
|
526
|
+
"\"capabilityGuides\"",
|
|
527
|
+
"\"skillDiagnostics\"",
|
|
528
|
+
"\"agentProviders\"",
|
|
529
|
+
"\"contextFingerprint\"",
|
|
530
|
+
"\"capabilityFingerprint\"",
|
|
531
|
+
"\"memberContext\"",
|
|
532
|
+
]) {
|
|
533
|
+
assert.equal(inspectionJson.includes(forbidden), false, `Workspace inspect leaked ${forbidden}`);
|
|
534
|
+
}
|
|
535
|
+
const inspectionAfter = callTool(oauth.accessToken, sessionId, 193, "open_workspace", {
|
|
536
|
+
action: "list",
|
|
537
|
+
workspaceId: deleteWorkspaceId,
|
|
538
|
+
}, workspaceConversationMeta);
|
|
539
|
+
assert.equal(inspectionAfter.structuredContent.workspaces[0].lastUsedAt, inspectionBeforeEntry.lastUsedAt);
|
|
540
|
+
assert.equal(inspectionAfter.structuredContent.workspaces[0].current, false);
|
|
541
|
+
const callerAfterInspection = callTool(oauth.accessToken, sessionId, 194, "open_workspace", {
|
|
542
|
+
workspaceId,
|
|
543
|
+
context: "auto",
|
|
544
|
+
}, workspaceConversationMeta);
|
|
545
|
+
assert.equal(callerAfterInspection.structuredContent.workspaceId, workspaceId);
|
|
546
|
+
assert.equal(callerAfterInspection.structuredContent.agentsFiles, undefined);
|
|
547
|
+
assert.equal(callerAfterInspection.structuredContent.capabilityGuides, undefined);
|
|
548
|
+
pass("Workspace inspection", "cross-Workspace metadata + Task summary stayed read-only and bootstrap-free");
|
|
549
|
+
|
|
418
550
|
const deleteClosed = callTool(oauth.accessToken, sessionId, 92, "close_workspace", {
|
|
419
551
|
workspaceId: deleteWorkspaceId,
|
|
420
552
|
});
|
|
@@ -729,6 +861,18 @@ try {
|
|
|
729
861
|
});
|
|
730
862
|
assert.equal(closedWorktreeInventory.structuredContent.workspaces.length, 1);
|
|
731
863
|
assert.equal(closedWorktreeInventory.structuredContent.workspaces[0].state, "closed");
|
|
864
|
+
const closedWorktreeInspection = callTool(oauth.accessToken, sessionId, 195, "open_workspace", {
|
|
865
|
+
action: "inspect",
|
|
866
|
+
workspaceId: worktreeWorkspaceId,
|
|
867
|
+
});
|
|
868
|
+
const closedWorktreeProjection = closedWorktreeInspection.structuredContent.inspection;
|
|
869
|
+
assert.equal(closedWorktreeProjection.kind, "workspace");
|
|
870
|
+
assert.equal(closedWorktreeProjection.mode, "worktree");
|
|
871
|
+
assert.equal(closedWorktreeProjection.managed, true);
|
|
872
|
+
assert.equal(closedWorktreeProjection.state, "closed");
|
|
873
|
+
assert.equal(closedWorktreeProjection.rootValid, false);
|
|
874
|
+
assert.equal(existsSync(managedWorktreePath), false);
|
|
875
|
+
assert.equal(JSON.stringify(closedWorktreeInspection).includes("\"fingerprint\""), false);
|
|
732
876
|
|
|
733
877
|
const reopenedWorktree = callTool(oauth.accessToken, sessionId, 111, "open_workspace", {
|
|
734
878
|
workspaceId: worktreeWorkspaceId,
|
|
@@ -848,6 +992,20 @@ try {
|
|
|
848
992
|
arguments: { operation: "get" },
|
|
849
993
|
});
|
|
850
994
|
assert.equal(closedCompositeTasks.isError, true);
|
|
995
|
+
const closedCompositeInspection = callTool(oauth.accessToken, sessionId, 196, "open_workspace", {
|
|
996
|
+
action: "inspect",
|
|
997
|
+
workspaceId: compositeWorkspaceId,
|
|
998
|
+
});
|
|
999
|
+
const closedCompositeProjection = closedCompositeInspection.structuredContent.inspection;
|
|
1000
|
+
assert.equal(closedCompositeProjection.kind, "composite");
|
|
1001
|
+
assert.equal(closedCompositeProjection.state, "closed");
|
|
1002
|
+
assert.equal(closedCompositeProjection.members[0].workspaceId, workspaceId);
|
|
1003
|
+
assert.equal(closedCompositeProjection.members[0].known, true);
|
|
1004
|
+
assert.equal(closedCompositeProjection.taskSummary.lists[0].taskCount, 1);
|
|
1005
|
+
assert.equal(
|
|
1006
|
+
JSON.stringify(closedCompositeInspection).includes("Composite-owned state"),
|
|
1007
|
+
false,
|
|
1008
|
+
);
|
|
851
1009
|
|
|
852
1010
|
const reopenedComposite = callTool(oauth.accessToken, sessionId, 121, "open_workspace", {
|
|
853
1011
|
workspaceId: compositeWorkspaceId,
|
|
@@ -860,14 +1018,16 @@ try {
|
|
|
860
1018
|
workspaceId: compositeWorkspaceId,
|
|
861
1019
|
name: "workspace.tasks",
|
|
862
1020
|
action: "run",
|
|
863
|
-
arguments: {
|
|
1021
|
+
arguments: {
|
|
1022
|
+
operation: "get",
|
|
1023
|
+
level: "detail",
|
|
1024
|
+
listId: compositeTaskListId,
|
|
1025
|
+
taskId: compositeTaskId,
|
|
1026
|
+
},
|
|
864
1027
|
});
|
|
865
1028
|
assert.equal(reopenedCompositeTasks.isError, undefined);
|
|
866
|
-
assert.equal(reopenedCompositeTasks.structuredContent.result.
|
|
867
|
-
assert.equal(
|
|
868
|
-
reopenedCompositeTasks.structuredContent.result.lists[0].tasks[0].content,
|
|
869
|
-
"Composite-owned state",
|
|
870
|
-
);
|
|
1029
|
+
assert.equal(reopenedCompositeTasks.structuredContent.result.task.id, compositeTaskId);
|
|
1030
|
+
assert.equal(reopenedCompositeTasks.structuredContent.result.task.content, "Composite-owned state");
|
|
871
1031
|
const reopenedCompositeRead = callTool(oauth.accessToken, sessionId, 122, "read", {
|
|
872
1032
|
workspaceId: compositeWorkspaceId,
|
|
873
1033
|
member: "code",
|