@akira-tl/forgerelay 0.8.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "0.8.2",
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/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",
@@ -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,
@@ -265,6 +269,7 @@ try {
265
269
  "hooks.lifecycle",
266
270
  "capability-guides.read",
267
271
  "code.intelligence",
272
+ "workspace.tasks",
268
273
  "batch.execute",
269
274
  ...(process.platform === "linux" ? ["artifact.native-download"] : []),
270
275
  "ui.mcp-app",
@@ -276,12 +281,116 @@ try {
276
281
  "hooks.check",
277
282
  "review.changes",
278
283
  "code.intelligence",
284
+ "workspace.tasks",
279
285
  "batch.execute",
280
286
  ...(process.platform === "linux" ? ["artifact.download"] : []),
281
287
  ]);
282
288
  assert.equal(capabilityCatalog[0].available, true);
283
289
  assert.equal(capabilityCatalog[0].guide.name, "lifecycle-hooks");
284
290
 
291
+ const checkoutTaskStatePath = join(stateDir, "workspaces", workspaceId, "tasks.json");
292
+ assert.ok(existsSync(checkoutTaskStatePath));
293
+ const checkoutTaskList = callTool(oauth.accessToken, sessionId, 130, "capability", {
294
+ workspaceId,
295
+ name: "workspace.tasks",
296
+ action: "run",
297
+ arguments: { operation: "list.create", name: "7677 release tasks" },
298
+ });
299
+ assert.equal(checkoutTaskList.isError, undefined);
300
+ const checkoutListId = checkoutTaskList.structuredContent.result.lists[0].id;
301
+ const checkoutTask = callTool(oauth.accessToken, sessionId, 131, "capability", {
302
+ workspaceId,
303
+ name: "workspace.tasks",
304
+ action: "run",
305
+ arguments: {
306
+ operation: "task.create",
307
+ listId: checkoutListId,
308
+ subject: "Verify v0.8.3 Task persistence",
309
+ content: "created through real MCP",
310
+ status: "in_progress",
311
+ },
312
+ });
313
+ assert.equal(checkoutTask.isError, undefined);
314
+ const checkoutTaskId = checkoutTask.structuredContent.result.lists[0].tasks[0].id;
315
+ const taskFingerprintBeforeExternal = checkoutTask.structuredContent.result.fingerprint;
316
+ const externalTaskState = JSON.parse(readFileSync(checkoutTaskStatePath, "utf8"));
317
+ externalTaskState.lists[0].tasks[0].content = "reloaded external task edit";
318
+ writeFileSync(checkoutTaskStatePath, `${JSON.stringify(externalTaskState, null, 2)}\n`);
319
+ const reloadedCheckoutTasks = callTool(oauth.accessToken, sessionId, 132, "capability", {
320
+ workspaceId,
321
+ name: "workspace.tasks",
322
+ action: "run",
323
+ arguments: {
324
+ operation: "get",
325
+ level: "detail",
326
+ listId: checkoutListId,
327
+ taskId: checkoutTaskId,
328
+ },
329
+ });
330
+ assert.equal(reloadedCheckoutTasks.isError, undefined);
331
+ assert.equal(reloadedCheckoutTasks.structuredContent.result.task.id, checkoutTaskId);
332
+ assert.equal(reloadedCheckoutTasks.structuredContent.result.task.content, "reloaded external task edit");
333
+ assert.notEqual(reloadedCheckoutTasks.structuredContent.result.fingerprint, taskFingerprintBeforeExternal);
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
+
285
394
  const repeatedOpen = callTool(oauth.accessToken, sessionId, 84, "open_workspace", {
286
395
  path: checkoutWorkspace,
287
396
  newWorkspace: true,
@@ -312,6 +421,13 @@ try {
312
421
  assert.equal(closedWorkspace.isError, undefined);
313
422
  assert.equal(closedWorkspace.structuredContent.workspaceId, workspaceId);
314
423
  assert.equal(closedWorkspace.structuredContent.action, "close");
424
+ const closedCheckoutTasks = callTool(oauth.accessToken, sessionId, 133, "capability", {
425
+ workspaceId,
426
+ name: "workspace.tasks",
427
+ action: "run",
428
+ arguments: { operation: "get" },
429
+ });
430
+ assert.equal(closedCheckoutTasks.isError, true);
315
431
 
316
432
  const closedInventory = callTool(oauth.accessToken, sessionId, 87, "open_workspace", {
317
433
  action: "list",
@@ -336,12 +452,101 @@ try {
336
452
  resumedOriginal.structuredContent.contextFingerprint,
337
453
  opened.structuredContent.contextFingerprint,
338
454
  );
455
+ const resumedCheckoutTasks = callTool(oauth.accessToken, sessionId, 134, "capability", {
456
+ workspaceId,
457
+ name: "workspace.tasks",
458
+ action: "run",
459
+ arguments: {
460
+ operation: "get",
461
+ level: "detail",
462
+ listId: checkoutListId,
463
+ taskId: checkoutTaskId,
464
+ },
465
+ });
466
+ assert.equal(resumedCheckoutTasks.isError, undefined);
467
+ assert.equal(resumedCheckoutTasks.structuredContent.result.task.id, checkoutTaskId);
468
+ assert.equal(resumedCheckoutTasks.structuredContent.result.task.content, "reloaded external task edit");
469
+ assert.equal(resumedOriginal.structuredContent.contextFingerprint, opened.structuredContent.contextFingerprint);
339
470
 
340
471
  const deleteOpened = callTool(oauth.accessToken, sessionId, 91, "open_workspace", {
341
472
  path: lifecycleDeleteWorkspace,
342
473
  context: "none",
343
474
  }, { "openai/session": "acceptance-workspace-delete" });
344
475
  const deleteWorkspaceId = deleteOpened.structuredContent.workspaceId;
476
+ const deleteTaskStatePath = join(stateDir, "workspaces", deleteWorkspaceId, "tasks.json");
477
+ const deleteTaskList = callTool(oauth.accessToken, sessionId, 135, "capability", {
478
+ workspaceId: deleteWorkspaceId,
479
+ name: "workspace.tasks",
480
+ action: "run",
481
+ arguments: { operation: "list.create", name: "delete with workspace" },
482
+ });
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);
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
+
345
550
  const deleteClosed = callTool(oauth.accessToken, sessionId, 92, "close_workspace", {
346
551
  workspaceId: deleteWorkspaceId,
347
552
  });
@@ -353,12 +558,17 @@ try {
353
558
  assert.equal(deletedWorkspace.isError, undefined);
354
559
  assert.equal(deletedWorkspace.structuredContent.workspaceId, deleteWorkspaceId);
355
560
  assert.equal(deletedWorkspace.structuredContent.action, "delete");
561
+ assert.equal(existsSync(deleteTaskStatePath), false);
356
562
  assert.equal(readFileSync(join(lifecycleDeleteWorkspace, "keep.txt"), "utf8"), "keep checkout files\n");
357
563
  const deletedInventory = callTool(oauth.accessToken, sessionId, 94, "open_workspace", {
358
564
  action: "list",
359
565
  workspaceId: deleteWorkspaceId,
360
566
  });
361
567
  assert.equal(deletedInventory.structuredContent.workspaces.length, 0);
568
+ pass(
569
+ "workspace tasks",
570
+ `${workspaceId} create -> external reload -> close/reopen; explicit delete removed ${deleteWorkspaceId} Task state`,
571
+ );
362
572
 
363
573
  pass(
364
574
  "workspace lifecycle + inventory",
@@ -440,6 +650,7 @@ try {
440
650
  "host-integration",
441
651
  "shell-processes",
442
652
  "code-intelligence",
653
+ "workspace-tasks",
443
654
  "batch-execution",
444
655
  ]);
445
656
  const hooksGuide = callTool(oauth.accessToken, sessionId, 78, "read", {
@@ -650,6 +861,18 @@ try {
650
861
  });
651
862
  assert.equal(closedWorktreeInventory.structuredContent.workspaces.length, 1);
652
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);
653
876
 
654
877
  const reopenedWorktree = callTool(oauth.accessToken, sessionId, 111, "open_workspace", {
655
878
  workspaceId: worktreeWorkspaceId,
@@ -697,6 +920,33 @@ try {
697
920
  context: "none",
698
921
  });
699
922
  const compositeWorkspaceId = compositeOpened.structuredContent.workspaceId;
923
+ assert.deepEqual(
924
+ compositeOpened.structuredContent.capabilityCatalog.map((entry) => entry.name),
925
+ ["workspace.tasks"],
926
+ );
927
+ const compositeTaskStatePath = join(stateDir, "workspaces", compositeWorkspaceId, "tasks.json");
928
+ assert.ok(existsSync(compositeTaskStatePath));
929
+ const compositeTaskList = callTool(oauth.accessToken, sessionId, 136, "capability", {
930
+ workspaceId: compositeWorkspaceId,
931
+ name: "workspace.tasks",
932
+ action: "run",
933
+ arguments: { operation: "list.create", name: "Composite release tasks" },
934
+ });
935
+ assert.equal(compositeTaskList.isError, undefined);
936
+ const compositeTaskListId = compositeTaskList.structuredContent.result.lists[0].id;
937
+ const compositeTask = callTool(oauth.accessToken, sessionId, 137, "capability", {
938
+ workspaceId: compositeWorkspaceId,
939
+ name: "workspace.tasks",
940
+ action: "run",
941
+ arguments: {
942
+ operation: "task.create",
943
+ listId: compositeTaskListId,
944
+ subject: "Preserve Composite Task state",
945
+ content: "Composite-owned state",
946
+ },
947
+ });
948
+ assert.equal(compositeTask.isError, undefined);
949
+ const compositeTaskId = compositeTask.structuredContent.result.lists[0].tasks[0].id;
700
950
  callTool(oauth.accessToken, sessionId, 117, "open_workspace", {
701
951
  action: "member",
702
952
  workspaceId: compositeWorkspaceId,
@@ -707,6 +957,14 @@ try {
707
957
  workspaceId,
708
958
  },
709
959
  });
960
+ const memberScopedCompositeTasks = callTool(oauth.accessToken, sessionId, 138, "capability", {
961
+ workspaceId: compositeWorkspaceId,
962
+ member: "code",
963
+ name: "workspace.tasks",
964
+ action: "run",
965
+ arguments: { operation: "get" },
966
+ });
967
+ assert.equal(memberScopedCompositeTasks.isError, true);
710
968
  const closedComposite = callTool(oauth.accessToken, sessionId, 118, "close_workspace", {
711
969
  workspaceId: compositeWorkspaceId,
712
970
  });
@@ -727,6 +985,27 @@ try {
727
985
  path: "composite-sentinel.txt",
728
986
  });
729
987
  assert.equal(closedCompositeRead.isError, true);
988
+ const closedCompositeTasks = callTool(oauth.accessToken, sessionId, 139, "capability", {
989
+ workspaceId: compositeWorkspaceId,
990
+ name: "workspace.tasks",
991
+ action: "run",
992
+ arguments: { operation: "get" },
993
+ });
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
+ );
730
1009
 
731
1010
  const reopenedComposite = callTool(oauth.accessToken, sessionId, 121, "open_workspace", {
732
1011
  workspaceId: compositeWorkspaceId,
@@ -735,6 +1014,20 @@ try {
735
1014
  assert.equal(reopenedComposite.structuredContent.workspaceId, compositeWorkspaceId);
736
1015
  assert.equal(reopenedComposite.structuredContent.status, "active");
737
1016
  assert.equal(reopenedComposite.structuredContent.members[0].workspaceId, workspaceId);
1017
+ const reopenedCompositeTasks = callTool(oauth.accessToken, sessionId, 140, "capability", {
1018
+ workspaceId: compositeWorkspaceId,
1019
+ name: "workspace.tasks",
1020
+ action: "run",
1021
+ arguments: {
1022
+ operation: "get",
1023
+ level: "detail",
1024
+ listId: compositeTaskListId,
1025
+ taskId: compositeTaskId,
1026
+ },
1027
+ });
1028
+ assert.equal(reopenedCompositeTasks.isError, undefined);
1029
+ assert.equal(reopenedCompositeTasks.structuredContent.result.task.id, compositeTaskId);
1030
+ assert.equal(reopenedCompositeTasks.structuredContent.result.task.content, "Composite-owned state");
738
1031
  const reopenedCompositeRead = callTool(oauth.accessToken, sessionId, 122, "read", {
739
1032
  workspaceId: compositeWorkspaceId,
740
1033
  member: "code",
@@ -748,6 +1041,7 @@ try {
748
1041
  });
749
1042
  assert.equal(deletedComposite.structuredContent.action, "delete");
750
1043
  assert.equal(deletedComposite.structuredContent.dissolved, true);
1044
+ assert.equal(existsSync(compositeTaskStatePath), false);
751
1045
  const memberAfterCompositeDelete = callTool(oauth.accessToken, sessionId, 124, "read", {
752
1046
  workspaceId,
753
1047
  path: "composite-sentinel.txt",
@@ -759,6 +1053,10 @@ try {
759
1053
  workspaceId: compositeWorkspaceId,
760
1054
  });
761
1055
  assert.equal(deletedCompositeInventory.structuredContent.compositeWorkspaces.length, 0);
1056
+ pass(
1057
+ "Composite workspace tasks",
1058
+ `${compositeWorkspaceId} self-owned Task state -> close/reopen -> delete cleanup; member-scoped Task access rejected`,
1059
+ );
762
1060
  pass(
763
1061
  "Composite lifecycle",
764
1062
  `${compositeWorkspaceId} close -> closed/non-routable -> same-id reopen -> delete; member Workspace preserved`,