@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.
- package/CHANGELOG.md +17 -0
- package/README.md +4 -2
- package/dist/composite-activity.js +1 -1
- package/dist/composite-workspaces.js +31 -10
- package/dist/git-worktrees.js +22 -0
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/server.js +181 -52
- package/dist/workspace-store.js +25 -0
- package/dist/workspaces.js +169 -25
- package/docs/chatgpt-coding-workflow.md +24 -5
- package/docs/configuration.md +26 -9
- package/package.json +1 -1
- package/scripts/debug/accept.mjs +190 -33
package/scripts/debug/accept.mjs
CHANGED
|
@@ -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
|
|
285
|
+
const repeatedOpen = callTool(oauth.accessToken, sessionId, 84, "open_workspace", {
|
|
283
286
|
path: checkoutWorkspace,
|
|
284
287
|
newWorkspace: true,
|
|
285
288
|
}, workspaceConversationMeta);
|
|
286
|
-
|
|
287
|
-
assert.
|
|
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
|
-
|
|
292
|
+
repeatedOpen.structuredContent.contextFingerprint,
|
|
291
293
|
opened.structuredContent.contextFingerprint,
|
|
292
294
|
);
|
|
293
|
-
assert.equal(
|
|
294
|
-
assert.equal(
|
|
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,
|
|
303
|
+
assert.equal(workspaceInventory.structuredContent.summary.matching, 1);
|
|
302
304
|
const inventoryEntries = workspaceInventory.structuredContent.workspaces;
|
|
303
|
-
assert.equal(inventoryEntries.length,
|
|
304
|
-
assert.equal(
|
|
305
|
-
|
|
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
|
|
314
|
-
workspaceId
|
|
309
|
+
const closedWorkspace = callTool(oauth.accessToken, sessionId, 86, "close_workspace", {
|
|
310
|
+
workspaceId,
|
|
315
311
|
});
|
|
316
|
-
assert.equal(
|
|
317
|
-
|
|
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
|
|
328
|
-
`${workspaceId} ->
|
|
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
|
-
"
|
|
612
|
-
`${
|
|
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 {
|
|
976
|
-
'const store = new
|
|
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 {
|
|
1010
|
-
'
|
|
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,
|
|
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, "
|
|
1025
|
-
assert.
|
|
1026
|
-
|
|
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) {
|