@akira-tl/forgerelay 0.8.0 → 0.8.2

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.
@@ -32,6 +32,7 @@ const stateDir = resolve(acceptanceRoot, "state");
32
32
  const worktreeRoot = resolve(acceptanceRoot, "worktrees");
33
33
  const hookLog = resolve(acceptanceRoot, "hooks.jsonl");
34
34
  const checkoutWorkspace = resolve(acceptanceRoot, "workspace");
35
+ const lifecycleDeleteWorkspace = resolve(acceptanceRoot, "delete-workspace");
35
36
  const codeIntelligenceLog = resolve(acceptanceRoot, "code-intelligence-lsp.jsonl");
36
37
  const fakeLanguageServer = resolve(repoRoot, "src", "lsp", "test-fixtures", "fake-lsp-server.mjs");
37
38
  const gitProject = resolve(acceptanceRoot, "git-project");
@@ -45,6 +46,8 @@ await assertDebugPortFree();
45
46
  rmSync(acceptanceRoot, { recursive: true, force: true });
46
47
  mkdirSync(acceptanceRoot, { recursive: true });
47
48
  setupGitProject(checkoutWorkspace);
49
+ setupGitProject(lifecycleDeleteWorkspace);
50
+ writeFileSync(join(lifecycleDeleteWorkspace, "keep.txt"), "keep checkout files\n");
48
51
  setupCodeIntelligenceAcceptanceProject({
49
52
  root: checkoutWorkspace,
50
53
  fakeLanguageServer,
@@ -279,42 +282,52 @@ try {
279
282
  assert.equal(capabilityCatalog[0].available, true);
280
283
  assert.equal(capabilityCatalog[0].guide.name, "lifecycle-hooks");
281
284
 
282
- const freshLogical = callTool(oauth.accessToken, sessionId, 84, "open_workspace", {
285
+ const repeatedOpen = callTool(oauth.accessToken, sessionId, 84, "open_workspace", {
283
286
  path: checkoutWorkspace,
284
287
  newWorkspace: true,
285
288
  }, workspaceConversationMeta);
286
- const freshLogicalId = freshLogical.structuredContent.workspaceId;
287
- assert.notEqual(freshLogicalId, workspaceId);
288
- assert.equal(freshLogical.structuredContent.action, "open");
289
+ assert.equal(repeatedOpen.structuredContent.workspaceId, workspaceId);
290
+ assert.equal(repeatedOpen.structuredContent.action, "open");
289
291
  assert.equal(
290
- freshLogical.structuredContent.contextFingerprint,
292
+ repeatedOpen.structuredContent.contextFingerprint,
291
293
  opened.structuredContent.contextFingerprint,
292
294
  );
293
- assert.equal(freshLogical.structuredContent.agentsFiles, undefined);
294
- assert.equal(freshLogical.structuredContent.capabilityGuides, undefined);
295
+ assert.equal(repeatedOpen.structuredContent.agentsFiles, undefined);
296
+ assert.equal(repeatedOpen.structuredContent.capabilityGuides, undefined);
295
297
 
296
298
  const workspaceInventory = callTool(oauth.accessToken, sessionId, 85, "open_workspace", {
297
299
  action: "list",
298
300
  root: checkoutWorkspace,
299
301
  }, workspaceConversationMeta);
300
302
  assert.equal(workspaceInventory.structuredContent.action, "list");
301
- assert.equal(workspaceInventory.structuredContent.summary.matching, 2);
303
+ assert.equal(workspaceInventory.structuredContent.summary.matching, 1);
302
304
  const inventoryEntries = workspaceInventory.structuredContent.workspaces;
303
- assert.equal(inventoryEntries.length, 2);
304
- assert.equal(
305
- inventoryEntries.find((entry) => entry.workspaceId === freshLogicalId)?.current,
306
- true,
307
- );
308
- assert.equal(
309
- inventoryEntries.find((entry) => entry.workspaceId === workspaceId)?.current,
310
- false,
311
- );
305
+ assert.equal(inventoryEntries.length, 1);
306
+ assert.equal(inventoryEntries[0].workspaceId, workspaceId);
307
+ assert.equal(inventoryEntries[0].current, true);
312
308
 
313
- const closedFreshLogical = callTool(oauth.accessToken, sessionId, 86, "close_workspace", {
314
- workspaceId: freshLogicalId,
309
+ const closedWorkspace = callTool(oauth.accessToken, sessionId, 86, "close_workspace", {
310
+ workspaceId,
315
311
  });
316
- assert.equal(closedFreshLogical.isError, undefined);
317
- const resumedOriginal = callTool(oauth.accessToken, sessionId, 87, "open_workspace", {
312
+ assert.equal(closedWorkspace.isError, undefined);
313
+ assert.equal(closedWorkspace.structuredContent.workspaceId, workspaceId);
314
+ assert.equal(closedWorkspace.structuredContent.action, "close");
315
+
316
+ const closedInventory = callTool(oauth.accessToken, sessionId, 87, "open_workspace", {
317
+ action: "list",
318
+ workspaceId,
319
+ }, workspaceConversationMeta);
320
+ assert.equal(closedInventory.structuredContent.workspaces.length, 1);
321
+ assert.equal(closedInventory.structuredContent.workspaces[0].state, "closed");
322
+ assert.equal(closedInventory.structuredContent.workspaces[0].current, false);
323
+
324
+ const closedRead = callTool(oauth.accessToken, sessionId, 89, "read", {
325
+ workspaceId,
326
+ path: "AGENTS.md",
327
+ });
328
+ assert.equal(closedRead.isError, true);
329
+
330
+ const resumedOriginal = callTool(oauth.accessToken, sessionId, 90, "open_workspace", {
318
331
  workspaceId,
319
332
  }, workspaceConversationMeta);
320
333
  assert.equal(resumedOriginal.structuredContent.workspaceId, workspaceId);
@@ -323,9 +336,33 @@ try {
323
336
  resumedOriginal.structuredContent.contextFingerprint,
324
337
  opened.structuredContent.contextFingerprint,
325
338
  );
339
+
340
+ const deleteOpened = callTool(oauth.accessToken, sessionId, 91, "open_workspace", {
341
+ path: lifecycleDeleteWorkspace,
342
+ context: "none",
343
+ }, { "openai/session": "acceptance-workspace-delete" });
344
+ const deleteWorkspaceId = deleteOpened.structuredContent.workspaceId;
345
+ const deleteClosed = callTool(oauth.accessToken, sessionId, 92, "close_workspace", {
346
+ workspaceId: deleteWorkspaceId,
347
+ });
348
+ assert.equal(deleteClosed.structuredContent.action, "close");
349
+ const deletedWorkspace = callTool(oauth.accessToken, sessionId, 93, "close_workspace", {
350
+ workspaceId: deleteWorkspaceId,
351
+ action: "delete",
352
+ });
353
+ assert.equal(deletedWorkspace.isError, undefined);
354
+ assert.equal(deletedWorkspace.structuredContent.workspaceId, deleteWorkspaceId);
355
+ assert.equal(deletedWorkspace.structuredContent.action, "delete");
356
+ assert.equal(readFileSync(join(lifecycleDeleteWorkspace, "keep.txt"), "utf8"), "keep checkout files\n");
357
+ const deletedInventory = callTool(oauth.accessToken, sessionId, 94, "open_workspace", {
358
+ action: "list",
359
+ workspaceId: deleteWorkspaceId,
360
+ });
361
+ assert.equal(deletedInventory.structuredContent.workspaces.length, 0);
362
+
326
363
  pass(
327
- "workspace context + inventory",
328
- `${workspaceId} -> ${freshLogicalId} -> list -> close -> resume without bootstrap replay`,
364
+ "workspace lifecycle + inventory",
365
+ `${workspaceId} canonical reuse -> close -> list -> reopen; explicit delete preserves checkout files`,
329
366
  );
330
367
 
331
368
  const directCapability = callTool(oauth.accessToken, sessionId, 79, "capability", {
@@ -607,9 +644,124 @@ try {
607
644
  readFileSync(join(gitProject, "feature.txt"), "utf8").replace(/\r\n/g, "\n"),
608
645
  "debug worktree acceptance\n",
609
646
  );
647
+ const closedWorktreeInventory = callTool(oauth.accessToken, sessionId, 110, "open_workspace", {
648
+ action: "list",
649
+ workspaceId: worktreeWorkspaceId,
650
+ });
651
+ assert.equal(closedWorktreeInventory.structuredContent.workspaces.length, 1);
652
+ assert.equal(closedWorktreeInventory.structuredContent.workspaces[0].state, "closed");
653
+
654
+ const reopenedWorktree = callTool(oauth.accessToken, sessionId, 111, "open_workspace", {
655
+ workspaceId: worktreeWorkspaceId,
656
+ context: "none",
657
+ });
658
+ assert.equal(reopenedWorktree.structuredContent.workspaceId, worktreeWorkspaceId);
659
+ const reopenedWorktreePath = reopenedWorktree.structuredContent.worktree.path;
660
+ assert.notEqual(reopenedWorktreePath, managedWorktreePath);
661
+ assert.ok(existsSync(reopenedWorktreePath));
662
+
663
+ callTool(oauth.accessToken, sessionId, 112, "write", {
664
+ workspaceId: worktreeWorkspaceId,
665
+ path: "delete-feature.txt",
666
+ content: "debug worktree delete acceptance\n",
667
+ });
668
+ const deletedWorktree = callTool(oauth.accessToken, sessionId, 113, "close_workspace", {
669
+ workspaceId: worktreeWorkspaceId,
670
+ action: "delete",
671
+ commitMessage: "test(debug): verify 7677 worktree delete lifecycle",
672
+ });
673
+ assert.equal(deletedWorktree.structuredContent.action, "delete");
674
+ assert.equal(existsSync(reopenedWorktreePath), false);
675
+ assert.equal(
676
+ readFileSync(join(gitProject, "delete-feature.txt"), "utf8").replace(/\r\n/g, "\n"),
677
+ "debug worktree delete acceptance\n",
678
+ );
679
+ const deletedWorktreeInventory = callTool(oauth.accessToken, sessionId, 114, "open_workspace", {
680
+ action: "list",
681
+ workspaceId: worktreeWorkspaceId,
682
+ });
683
+ assert.equal(deletedWorktreeInventory.structuredContent.workspaces.length, 0);
684
+ pass(
685
+ "managed worktree lifecycle",
686
+ `${worktreeWorkspaceId} close -> closed inventory -> same-id reopen with fresh backing -> safe delete`,
687
+ );
688
+
689
+ callTool(oauth.accessToken, sessionId, 115, "write", {
690
+ workspaceId,
691
+ path: "composite-sentinel.txt",
692
+ content: "debug composite member acceptance\n",
693
+ });
694
+ const compositeOpened = callTool(oauth.accessToken, sessionId, 116, "open_workspace", {
695
+ kind: "composite",
696
+ name: "debug-lifecycle-composite",
697
+ context: "none",
698
+ });
699
+ const compositeWorkspaceId = compositeOpened.structuredContent.workspaceId;
700
+ callTool(oauth.accessToken, sessionId, 117, "open_workspace", {
701
+ action: "member",
702
+ workspaceId: compositeWorkspaceId,
703
+ memberAction: "add",
704
+ member: {
705
+ name: "code",
706
+ purpose: "Debug lifecycle member",
707
+ workspaceId,
708
+ },
709
+ });
710
+ const closedComposite = callTool(oauth.accessToken, sessionId, 118, "close_workspace", {
711
+ workspaceId: compositeWorkspaceId,
712
+ });
713
+ assert.equal(closedComposite.structuredContent.action, "close");
714
+ assert.equal(closedComposite.structuredContent.status, "closed");
715
+ assert.equal(closedComposite.structuredContent.dissolved, false);
716
+ const closedCompositeInventory = callTool(oauth.accessToken, sessionId, 119, "open_workspace", {
717
+ action: "list",
718
+ kind: "composite",
719
+ workspaceId: compositeWorkspaceId,
720
+ status: "closed",
721
+ });
722
+ assert.equal(closedCompositeInventory.structuredContent.compositeWorkspaces.length, 1);
723
+ assert.equal(closedCompositeInventory.structuredContent.compositeWorkspaces[0].state, "closed");
724
+ const closedCompositeRead = callTool(oauth.accessToken, sessionId, 120, "read", {
725
+ workspaceId: compositeWorkspaceId,
726
+ member: "code",
727
+ path: "composite-sentinel.txt",
728
+ });
729
+ assert.equal(closedCompositeRead.isError, true);
730
+
731
+ const reopenedComposite = callTool(oauth.accessToken, sessionId, 121, "open_workspace", {
732
+ workspaceId: compositeWorkspaceId,
733
+ context: "none",
734
+ });
735
+ assert.equal(reopenedComposite.structuredContent.workspaceId, compositeWorkspaceId);
736
+ assert.equal(reopenedComposite.structuredContent.status, "active");
737
+ assert.equal(reopenedComposite.structuredContent.members[0].workspaceId, workspaceId);
738
+ const reopenedCompositeRead = callTool(oauth.accessToken, sessionId, 122, "read", {
739
+ workspaceId: compositeWorkspaceId,
740
+ member: "code",
741
+ path: "composite-sentinel.txt",
742
+ });
743
+ assert.match(reopenedCompositeRead.structuredContent.result, /debug composite member acceptance/);
744
+
745
+ const deletedComposite = callTool(oauth.accessToken, sessionId, 123, "close_workspace", {
746
+ workspaceId: compositeWorkspaceId,
747
+ action: "delete",
748
+ });
749
+ assert.equal(deletedComposite.structuredContent.action, "delete");
750
+ assert.equal(deletedComposite.structuredContent.dissolved, true);
751
+ const memberAfterCompositeDelete = callTool(oauth.accessToken, sessionId, 124, "read", {
752
+ workspaceId,
753
+ path: "composite-sentinel.txt",
754
+ });
755
+ assert.match(memberAfterCompositeDelete.structuredContent.result, /debug composite member acceptance/);
756
+ const deletedCompositeInventory = callTool(oauth.accessToken, sessionId, 125, "open_workspace", {
757
+ action: "list",
758
+ kind: "composite",
759
+ workspaceId: compositeWorkspaceId,
760
+ });
761
+ assert.equal(deletedCompositeInventory.structuredContent.compositeWorkspaces.length, 0);
610
762
  pass(
611
- "managed worktree workspace close",
612
- `${closed.structuredContent.branch} -> ${closed.structuredContent.targetBranch}`,
763
+ "Composite lifecycle",
764
+ `${compositeWorkspaceId} close -> closed/non-routable -> same-id reopen -> delete; member Workspace preserved`,
613
765
  );
614
766
 
615
767
  exerciseReleaseTagHooks(oauth.accessToken, sessionId);
@@ -972,13 +1124,14 @@ function exerciseReleaseTagHooks(accessToken, sessionId) {
972
1124
 
973
1125
  function exerciseSubagentHooks(runtimeEnv, debugStateDir, workspaceId, workspaceRoot) {
974
1126
  const seedScript = [
975
- 'import { LocalAgentStore } from "./src/local-agent-store.js";',
976
- 'const store = new LocalAgentStore(process.env.FORGERELAY_STATE_DIR);',
1127
+ 'import { SubagentSessionStore } from "./src/subagents/sessions/store.js";',
1128
+ 'const store = new SubagentSessionStore(process.env.FORGERELAY_STATE_DIR);',
977
1129
  'const record = store.create({',
978
1130
  ` workspaceId: ${JSON.stringify(workspaceId)},`,
979
1131
  ` workspaceRoot: ${JSON.stringify(workspaceRoot)},`,
980
1132
  ' profileName: "debug-missing-profile",',
981
1133
  ' provider: "codex",',
1134
+ ' activeRun: { id: "run_debug_acceptance", startedAt: new Date().toISOString() },',
982
1135
  '});',
983
1136
  'store.close();',
984
1137
  'process.stdout.write(record.id);',
@@ -1006,11 +1159,13 @@ function exerciseSubagentHooks(runtimeEnv, debugStateDir, workspaceId, workspace
1006
1159
  }
1007
1160
 
1008
1161
  const inspectScript = [
1009
- 'import { LocalAgentStore } from "./src/local-agent-store.js";',
1010
- 'const store = new LocalAgentStore(process.env.FORGERELAY_STATE_DIR);',
1162
+ 'import { SubagentSessionStore } from "./src/subagents/sessions/store.js";',
1163
+ 'import { SubagentDeliveryMailbox } from "./src/subagents/sessions/delivery-mailbox.js";',
1164
+ 'const store = new SubagentSessionStore(process.env.FORGERELAY_STATE_DIR);',
1011
1165
  `const record = store.get(${JSON.stringify(agentId)});`,
1166
+ `const deliveries = new SubagentDeliveryMailbox(process.env.FORGERELAY_STATE_DIR).claimSession(${JSON.stringify(workspaceId)}, ${JSON.stringify(agentId)});`,
1012
1167
  'store.close();',
1013
- 'process.stdout.write(JSON.stringify({ status: record?.status, error: record?.error }));',
1168
+ 'process.stdout.write(JSON.stringify({ status: record?.status, latestRun: record?.latestRun, delivery: deliveries[0] }));',
1014
1169
  ].join("\n");
1015
1170
  const inspected = spawnSync(process.execPath, ["--import", "tsx", "--input-type=module", "-e", inspectScript], {
1016
1171
  cwd: repoRoot,
@@ -1021,9 +1176,11 @@ function exerciseSubagentHooks(runtimeEnv, debugStateDir, workspaceId, workspace
1021
1176
  throw new Error(`unable to inspect debug subagent: ${inspected.stderr.trim()}`);
1022
1177
  }
1023
1178
  const record = JSON.parse(inspected.stdout);
1024
- assert.equal(record.status, "error");
1025
- assert.match(record.error, /Subagent profile not found/);
1026
- pass("subagent hook path", `${agentId} stopped in deterministic error path without calling a provider`);
1179
+ assert.equal(record.status, "idle");
1180
+ assert.equal(record.latestRun?.status, "failed");
1181
+ assert.equal(record.delivery?.outcome, "failed");
1182
+ assert.match(record.delivery?.error ?? "", /Subagent profile not found/);
1183
+ pass("subagent hook path", `${agentId} stopped in deterministic failed Run without calling a provider`);
1027
1184
  }
1028
1185
 
1029
1186
  function toolText(result) {