@opengsd/gsd-pi 1.0.2-dev.235ebf3 → 1.0.2-dev.2c204d3
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/README.md +63 -12
- package/dist/resource-loader.d.ts +7 -0
- package/dist/resource-loader.js +42 -9
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/context7/index.js +12 -2
- package/dist/resources/extensions/gsd/auto/loop.js +19 -0
- package/dist/resources/extensions/gsd/auto/phases.js +1 -1
- package/dist/resources/extensions/gsd/auto/session.js +3 -0
- package/dist/resources/extensions/gsd/auto-start.js +232 -49
- package/dist/resources/extensions/gsd/auto-worktree.js +2 -54
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +4 -3
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -15
- package/dist/resources/extensions/gsd/closeout-recovery.js +7 -1
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +9 -1
- package/dist/resources/extensions/gsd/commands-handlers.js +3 -0
- package/dist/resources/extensions/gsd/git-conflict-state.js +26 -1
- package/dist/resources/extensions/gsd/tools/complete-task.js +9 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +40 -1
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +24 -3
- package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
- package/dist/resources/extensions/search-the-web/native-search.js +57 -8
- package/dist/resources/shared/package-manager-detection.js +36 -0
- package/dist/update-check.d.ts +6 -2
- package/dist/update-check.js +7 -3
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
- package/dist/web/standalone/.next/server/chunks/1834.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/dist/web/standalone/package.json +0 -1
- package/dist/worktree-cli.d.ts +0 -2
- package/dist/worktree-cli.js +21 -9
- package/package.json +5 -2
- package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
- package/packages/cloud-mcp-gateway/package.json +4 -3
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/bin/gsd-mcp-server.js +14 -0
- package/packages/mcp-server/dist/workflow-tools.js +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +5 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +13 -13
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/bin/pi-ai.js +14 -0
- package/packages/pi-ai/dist/models.generated.d.ts +40 -17
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +49 -30
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +50 -0
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +2 -0
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/package.json +3 -2
- package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
- package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
- package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
- package/packages/pi-coding-agent/package.json +8 -8
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/scripts/install/deps.js +10 -0
- package/scripts/install/detect-existing.js +17 -3
- package/scripts/install/npm-global.js +103 -33
- package/scripts/install.js +1 -0
- package/src/resources/extensions/context7/index.ts +15 -2
- package/src/resources/extensions/gsd/auto/loop.ts +22 -0
- package/src/resources/extensions/gsd/auto/phases.ts +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +3 -0
- package/src/resources/extensions/gsd/auto-start.ts +307 -56
- package/src/resources/extensions/gsd/auto-worktree.ts +2 -56
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +4 -3
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -15
- package/src/resources/extensions/gsd/closeout-recovery.ts +6 -1
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -1
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -0
- package/src/resources/extensions/gsd/git-conflict-state.ts +25 -1
- package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +436 -0
- package/src/resources/extensions/gsd/tests/closeout-recovery.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/commands-dispatcher-workspace-git.test.ts +15 -2
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +70 -10
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +13 -2
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +24 -1
- package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +60 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +16 -1
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +141 -1
- package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +45 -1
- package/src/resources/extensions/gsd/tools/complete-task.ts +9 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +56 -4
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +37 -2
- package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
- package/src/resources/extensions/search-the-web/native-search.ts +60 -8
- package/src/resources/shared/package-manager-detection.ts +39 -0
- package/dist/tsconfig.extensions.tsbuildinfo +0 -1
- /package/dist/web/standalone/.next/static/{-P554bKh56nzavKUmvFM2 → mijI90BL1BdUcMUnhC0HU}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{-P554bKh56nzavKUmvFM2 → mijI90BL1BdUcMUnhC0HU}/_ssgManifest.js +0 -0
|
@@ -344,6 +344,70 @@ describe("Custom engine loop integration", () => {
|
|
|
344
344
|
);
|
|
345
345
|
});
|
|
346
346
|
|
|
347
|
+
it("step mode stops after one custom workflow step", async () => {
|
|
348
|
+
_resetPendingResolve();
|
|
349
|
+
|
|
350
|
+
const runDir = makeTmpDir();
|
|
351
|
+
const graph = makeGraph([
|
|
352
|
+
makeStep({ id: "step-a" }),
|
|
353
|
+
makeStep({ id: "step-b", dependsOn: ["step-a"] }),
|
|
354
|
+
makeStep({ id: "step-c", dependsOn: ["step-b"] }),
|
|
355
|
+
], "step-mode-custom");
|
|
356
|
+
writeGraph(runDir, graph);
|
|
357
|
+
writeDefinition(runDir, graph.steps, "step-mode-custom");
|
|
358
|
+
|
|
359
|
+
const ctx = makeMockCtx();
|
|
360
|
+
const pi = makeMockPi();
|
|
361
|
+
const s = makeLoopSession({
|
|
362
|
+
activeEngineId: "custom",
|
|
363
|
+
activeRunDir: runDir,
|
|
364
|
+
basePath: runDir,
|
|
365
|
+
stepMode: true,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const deps = makeMockDeps({
|
|
369
|
+
stopAuto: async (_ctx, _pi, reason) => {
|
|
370
|
+
deps.callLog.push(`stopAuto:${reason ?? "no-reason"}`);
|
|
371
|
+
s.active = false;
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
376
|
+
await resolveNextAgentEnd();
|
|
377
|
+
|
|
378
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
379
|
+
try {
|
|
380
|
+
await Promise.race([
|
|
381
|
+
loopPromise,
|
|
382
|
+
new Promise((_, reject) =>
|
|
383
|
+
timeout = setTimeout(() => {
|
|
384
|
+
s.active = false;
|
|
385
|
+
if (_hasPendingResolveForTest()) {
|
|
386
|
+
resolveAgentEnd({ messages: [{ role: "assistant" }] });
|
|
387
|
+
}
|
|
388
|
+
reject(new Error(
|
|
389
|
+
`step mode did not stop after one custom workflow step; calls=${pi.calls.length}; log=${deps.callLog.join(",")}`,
|
|
390
|
+
));
|
|
391
|
+
}, 1_000),
|
|
392
|
+
),
|
|
393
|
+
]);
|
|
394
|
+
} finally {
|
|
395
|
+
if (timeout) clearTimeout(timeout);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const finalGraph = readGraph(runDir);
|
|
399
|
+
assert.equal(pi.calls.length, 1, "step mode should dispatch exactly one custom step");
|
|
400
|
+
assert.equal(finalGraph.steps[0]?.status, "complete", "first step should complete");
|
|
401
|
+
assert.equal(finalGraph.steps[1]?.status, "pending", "second step should wait for the next /gsd next");
|
|
402
|
+
assert.equal(finalGraph.steps[2]?.status, "pending", "third step should wait for a later step");
|
|
403
|
+
assert.equal(
|
|
404
|
+
deps.callLog.some((e: string) => e.startsWith("stopAuto:")),
|
|
405
|
+
false,
|
|
406
|
+
"step-mode pause should not complete or stop the whole workflow",
|
|
407
|
+
);
|
|
408
|
+
assert.equal(s.preserveStepSurfaceAfterLoopExit, true);
|
|
409
|
+
});
|
|
410
|
+
|
|
347
411
|
it("stops when engine reports isComplete on first derive", async () => {
|
|
348
412
|
_resetPendingResolve();
|
|
349
413
|
|
|
@@ -48,20 +48,27 @@ describe("auditOrphanedMilestoneBranches", () => {
|
|
|
48
48
|
const result = auditOrphanedMilestoneBranches(dir, "worktree");
|
|
49
49
|
assert.deepStrictEqual(result.recovered, []);
|
|
50
50
|
assert.deepStrictEqual(result.warnings, []);
|
|
51
|
+
assert.deepStrictEqual(result.actions, []);
|
|
52
|
+
assert.equal(result.blockingStrandedWork, null);
|
|
51
53
|
});
|
|
52
54
|
|
|
53
|
-
test("
|
|
54
|
-
// Create a milestone branch that would otherwise be detected
|
|
55
|
+
test("runs in none isolation mode and cleans safe completed residue", () => {
|
|
55
56
|
run("git branch milestone/M001", dir);
|
|
56
57
|
insertMilestone({ id: "M001", title: "Test", status: "complete" });
|
|
57
58
|
|
|
58
59
|
const result = auditOrphanedMilestoneBranches(dir, "none");
|
|
59
|
-
assert.
|
|
60
|
+
assert.ok(
|
|
61
|
+
result.recovered.some((r) => r.includes("Deleted merged branch milestone/M001")),
|
|
62
|
+
`should clean merged completed residue even in none mode; got: ${JSON.stringify(result.recovered)}`,
|
|
63
|
+
);
|
|
60
64
|
assert.deepStrictEqual(result.warnings, []);
|
|
65
|
+
assert.ok(
|
|
66
|
+
result.actions.some((action) => action.kind === "complete-merged-branch"),
|
|
67
|
+
"should record structured cleanup action",
|
|
68
|
+
);
|
|
61
69
|
|
|
62
|
-
// Branch should still exist
|
|
63
70
|
const branches = run("git branch --list milestone/M001", dir);
|
|
64
|
-
assert.
|
|
71
|
+
assert.equal(branches, "", "safe completed branch should be cleaned in none mode");
|
|
65
72
|
});
|
|
66
73
|
|
|
67
74
|
test("deletes merged branch for completed milestone", () => {
|
|
@@ -149,9 +156,12 @@ describe("auditOrphanedMilestoneBranches", () => {
|
|
|
149
156
|
// Must surface a warning so the user knows the worktree holds uncollapsed work
|
|
150
157
|
assert.ok(result.warnings.length > 0, "should warn about in-progress orphan");
|
|
151
158
|
assert.ok(
|
|
152
|
-
result.warnings.some(w => w.includes("milestone/M001") && w.includes("in-progress")),
|
|
153
|
-
`warning should mention milestone/M001 and in-progress state; got: ${JSON.stringify(result.warnings)}`,
|
|
159
|
+
result.warnings.some(w => w.includes("Stranded work") && w.includes("milestone/M001") && w.includes("in-progress")),
|
|
160
|
+
`warning should mention stranded milestone/M001 and in-progress state; got: ${JSON.stringify(result.warnings)}`,
|
|
154
161
|
);
|
|
162
|
+
assert.equal(result.blockingStrandedWork?.milestoneId, "M001");
|
|
163
|
+
assert.equal(result.blockingStrandedWork?.recoveryMode, "branch");
|
|
164
|
+
assert.equal(result.blockingStrandedWork?.commitsAhead, 1);
|
|
155
165
|
|
|
156
166
|
// Branch must still exist
|
|
157
167
|
const branches = run("git branch --list milestone/M001", dir);
|
|
@@ -184,6 +194,53 @@ describe("auditOrphanedMilestoneBranches", () => {
|
|
|
184
194
|
result.warnings.some(w => w.includes(".gsd/worktrees/M001") || w.includes("worktree")),
|
|
185
195
|
`warning should reference the worktree location; got: ${JSON.stringify(result.warnings)}`,
|
|
186
196
|
);
|
|
197
|
+
assert.equal(result.blockingStrandedWork?.recoveryMode, "worktree");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("detects dirty in-progress worktree even when branch has no commits ahead", () => {
|
|
201
|
+
run("git branch milestone/M001", dir);
|
|
202
|
+
|
|
203
|
+
const wtDir = join(dir, ".gsd", "worktrees", "M001");
|
|
204
|
+
mkdirSync(wtDir, { recursive: true });
|
|
205
|
+
writeFileSync(join(wtDir, ".git"), `gitdir: ${join(dir, ".git", "worktrees", "M001")}\n`);
|
|
206
|
+
writeFileSync(join(wtDir, "dirty.txt"), "uncommitted work\n");
|
|
207
|
+
|
|
208
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
209
|
+
|
|
210
|
+
const result = auditOrphanedMilestoneBranches(dir, "worktree", {
|
|
211
|
+
hasChanges: (basePath) => basePath === wtDir,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
assert.deepStrictEqual(result.recovered, []);
|
|
215
|
+
assert.ok(
|
|
216
|
+
result.warnings.some((w) => w.includes("uncommitted changes")),
|
|
217
|
+
`dirty worktree should be treated as stranded work; got: ${JSON.stringify(result.warnings)}`,
|
|
218
|
+
);
|
|
219
|
+
assert.equal(result.blockingStrandedWork?.milestoneId, "M001");
|
|
220
|
+
assert.equal(result.blockingStrandedWork?.dirtyWorktree, true);
|
|
221
|
+
assert.equal(result.blockingStrandedWork?.recoveryMode, "worktree");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("detects dirty in-progress worktree even when milestone branch is absent", () => {
|
|
225
|
+
const wtDir = join(dir, ".gsd", "worktrees", "M001");
|
|
226
|
+
mkdirSync(wtDir, { recursive: true });
|
|
227
|
+
writeFileSync(join(wtDir, ".git"), `gitdir: ${join(dir, ".git", "worktrees", "M001")}\n`);
|
|
228
|
+
writeFileSync(join(wtDir, "dirty.txt"), "branchless work\n");
|
|
229
|
+
|
|
230
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
231
|
+
|
|
232
|
+
const result = auditOrphanedMilestoneBranches(dir, "none", {
|
|
233
|
+
hasChanges: (basePath) => basePath === wtDir,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
assert.deepStrictEqual(result.recovered, []);
|
|
237
|
+
assert.ok(
|
|
238
|
+
result.warnings.some((w) => w.includes("Stranded work") && w.includes("M001")),
|
|
239
|
+
`branchless dirty worktree should block; got: ${JSON.stringify(result.warnings)}`,
|
|
240
|
+
);
|
|
241
|
+
assert.equal(result.blockingStrandedWork?.branch, undefined);
|
|
242
|
+
assert.equal(result.blockingStrandedWork?.dirtyWorktree, true);
|
|
243
|
+
assert.equal(result.blockingStrandedWork?.recoveryMode, "worktree");
|
|
187
244
|
});
|
|
188
245
|
|
|
189
246
|
test("cleans up orphaned worktree directory for merged milestone", () => {
|
|
@@ -359,7 +416,7 @@ describe("auditOrphanedMilestoneBranches", () => {
|
|
|
359
416
|
assert.ok(existsSync(wtDir), "active milestone worktree dir must be preserved");
|
|
360
417
|
});
|
|
361
418
|
|
|
362
|
-
test("#5879 —
|
|
419
|
+
test("#5879 — cleans branch-less complete orphan in 'none' isolation mode", () => {
|
|
363
420
|
insertMilestone({ id: "M001", title: "Test", status: "complete" });
|
|
364
421
|
|
|
365
422
|
const wtDir = join(dir, ".gsd", "worktrees", "M001");
|
|
@@ -368,7 +425,10 @@ describe("auditOrphanedMilestoneBranches", () => {
|
|
|
368
425
|
|
|
369
426
|
const result = auditOrphanedMilestoneBranches(dir, "none");
|
|
370
427
|
|
|
371
|
-
assert.
|
|
372
|
-
|
|
428
|
+
assert.ok(
|
|
429
|
+
result.recovered.some((r) => r.includes("M001") && r.includes("branch already deleted")),
|
|
430
|
+
`none mode should still clean safe completed residue; got: ${JSON.stringify(result.recovered)}`,
|
|
431
|
+
);
|
|
432
|
+
assert.ok(!existsSync(wtDir), "completed orphan worktree dir should be cleaned in none mode");
|
|
373
433
|
});
|
|
374
434
|
});
|
|
@@ -15,6 +15,13 @@ function readGsdFile(relativePath: string): string {
|
|
|
15
15
|
return readFileSync(resolve(gsdDir, relativePath), "utf-8");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function firstIndexOfAny(source: string, needles: string[]): number {
|
|
19
|
+
const indexes = needles
|
|
20
|
+
.map((needle) => source.indexOf(needle))
|
|
21
|
+
.filter((index) => index > -1);
|
|
22
|
+
return indexes.length > 0 ? Math.min(...indexes) : -1;
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
test("command entrypoints use startAutoDetached instead of awaiting startAuto (#3733)", () => {
|
|
19
26
|
const autoHandlerSrc = readGsdFile("commands/handlers/auto.ts");
|
|
20
27
|
const workflowHandlerSrc = readGsdFile("commands/handlers/workflow.ts");
|
|
@@ -145,7 +152,11 @@ test("fresh start registers the auto worker before bootstrap enters worktree flo
|
|
|
145
152
|
const resumeEnterMilestoneIdx = resumeBody.indexOf("buildLifecycle().enterMilestone");
|
|
146
153
|
const dbOpenIdx = bootstrapBody.indexOf("await openProjectDbIfPresent(base);");
|
|
147
154
|
const bootstrapRegisterIdx = bootstrapBody.indexOf("registerAutoWorkerForSession(base);");
|
|
148
|
-
const enterMilestoneIdx = bootstrapBody
|
|
155
|
+
const enterMilestoneIdx = firstIndexOfAny(bootstrapBody, [
|
|
156
|
+
"buildLifecycle().enterMilestone",
|
|
157
|
+
"lifecycle.enterMilestone",
|
|
158
|
+
"lifecycle.adoptStrandedMilestone",
|
|
159
|
+
]);
|
|
149
160
|
|
|
150
161
|
assert.ok(startAutoIdx > -1, "startAuto should exist");
|
|
151
162
|
assert.ok(preBootstrapRegisterIdx > -1, "startAuto should register worker before bootstrap");
|
|
@@ -158,7 +169,7 @@ test("fresh start registers the auto worker before bootstrap enters worktree flo
|
|
|
158
169
|
assert.ok(bootstrapIdx > -1, "bootstrapAutoSession should exist");
|
|
159
170
|
assert.ok(dbOpenIdx > -1, "bootstrap should open the project DB");
|
|
160
171
|
assert.ok(bootstrapRegisterIdx > -1, "bootstrap should register worker after DB open");
|
|
161
|
-
assert.ok(enterMilestoneIdx > -1, "bootstrap should enter milestones through lifecycle");
|
|
172
|
+
assert.ok(enterMilestoneIdx > -1, "bootstrap should enter or adopt milestones through lifecycle");
|
|
162
173
|
assert.ok(
|
|
163
174
|
preBootstrapRegisterIdx < bootstrapCallIdx,
|
|
164
175
|
"worker registration must happen before bootstrap so enterMilestone can claim milestone leases on first entry",
|
|
@@ -235,12 +235,16 @@ test("gsd_task_complete — enrichment arrays are optional", () => {
|
|
|
235
235
|
"milestoneId",
|
|
236
236
|
"oneLiner",
|
|
237
237
|
"narrative",
|
|
238
|
-
"verification",
|
|
239
238
|
];
|
|
240
239
|
for (const field of coreRequired) {
|
|
241
240
|
assert.ok(required.has(field), `core field "${field}" must be required`);
|
|
242
241
|
}
|
|
243
242
|
|
|
243
|
+
assert.ok(
|
|
244
|
+
!required.has("verification"),
|
|
245
|
+
"verification must be optional at the schema layer so step-mode can recover when verificationEvidence is present",
|
|
246
|
+
);
|
|
247
|
+
|
|
244
248
|
// Enrichment fields must be optional
|
|
245
249
|
const enrichmentFields = [
|
|
246
250
|
"keyFiles",
|
|
@@ -272,6 +276,25 @@ test("gsd_task_complete — validates with only core params", () => {
|
|
|
272
276
|
assert.strictEqual(errors.length, 0, `Minimal params should validate but got errors: ${errors.join(", ")}`);
|
|
273
277
|
});
|
|
274
278
|
|
|
279
|
+
test("gsd_task_complete — accepts evidence-only verification at schema layer", () => {
|
|
280
|
+
const tool = getTool("gsd_task_complete");
|
|
281
|
+
assert.ok(tool, "gsd_task_complete must be registered");
|
|
282
|
+
|
|
283
|
+
const params = {
|
|
284
|
+
taskId: "T01",
|
|
285
|
+
sliceId: "S01",
|
|
286
|
+
milestoneId: "M001",
|
|
287
|
+
oneLiner: "Implemented the feature",
|
|
288
|
+
narrative: "Created the module and wired it up.",
|
|
289
|
+
verificationEvidence: [
|
|
290
|
+
{ command: "npm test", exitCode: 0, verdict: "pass", durationMs: 1234 },
|
|
291
|
+
],
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const errors = validateSchema(tool, params);
|
|
295
|
+
assert.strictEqual(errors.length, 0, `Evidence-only params should validate but got errors: ${errors.join(", ")}`);
|
|
296
|
+
});
|
|
297
|
+
|
|
275
298
|
// ─── gsd_complete_milestone: enrichment arrays must be optional ──────────────
|
|
276
299
|
|
|
277
300
|
test("gsd_complete_milestone — enrichment arrays are optional", () => {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
3
6
|
|
|
7
|
+
import { registerHooks } from "../bootstrap/register-hooks.ts";
|
|
8
|
+
import { GSD_WORKFLOW_MCP_SERVER_NAME } from "../mcp-project-config.ts";
|
|
4
9
|
import { prepareWorkflowMcpForProject, shouldAutoPrepareWorkflowMcp } from "../workflow-mcp-auto-prep.ts";
|
|
5
10
|
|
|
6
11
|
test("shouldAutoPrepareWorkflowMcp enables prep for externalCli local transport", () => {
|
|
@@ -74,3 +79,58 @@ test("prepareWorkflowMcpForProject warns with /gsd mcp init guidance when prep f
|
|
|
74
79
|
assert.equal(notifications[0].level, "warning");
|
|
75
80
|
assert.match(notifications[0].message, /Please run \/gsd mcp init \./);
|
|
76
81
|
});
|
|
82
|
+
|
|
83
|
+
test("before_agent_start auto-prepares project workflow MCP for Claude Code CLI", async (t) => {
|
|
84
|
+
const projectRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-before-agent-"));
|
|
85
|
+
const originalCwd = process.cwd();
|
|
86
|
+
const notifications: string[] = [];
|
|
87
|
+
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
|
|
88
|
+
const pi = {
|
|
89
|
+
on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
|
|
90
|
+
const existing = handlers.get(event) ?? [];
|
|
91
|
+
existing.push(handler);
|
|
92
|
+
handlers.set(event, existing);
|
|
93
|
+
},
|
|
94
|
+
getActiveTools: () => [],
|
|
95
|
+
getAllTools: () => [],
|
|
96
|
+
setActiveTools() {},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
t.after(() => {
|
|
100
|
+
process.chdir(originalCwd);
|
|
101
|
+
rmSync(projectRoot, { recursive: true, force: true });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
process.chdir(projectRoot);
|
|
105
|
+
registerHooks(pi as any, []);
|
|
106
|
+
|
|
107
|
+
const beforeAgentStart = handlers.get("before_agent_start")?.[0];
|
|
108
|
+
assert.ok(beforeAgentStart, "before_agent_start hook should be registered");
|
|
109
|
+
|
|
110
|
+
await beforeAgentStart(
|
|
111
|
+
{ prompt: "hello", systemPrompt: "base" },
|
|
112
|
+
{
|
|
113
|
+
cwd: projectRoot,
|
|
114
|
+
model: { provider: "claude-code", baseUrl: "local://claude-code" },
|
|
115
|
+
modelRegistry: {
|
|
116
|
+
getProviderAuthMode: () => "externalCli",
|
|
117
|
+
isProviderRequestReady: () => true,
|
|
118
|
+
},
|
|
119
|
+
ui: {
|
|
120
|
+
notify(message: string) {
|
|
121
|
+
notifications.push(message);
|
|
122
|
+
},
|
|
123
|
+
setWidget() {},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const configPath = join(projectRoot, ".mcp.json");
|
|
129
|
+
assert.equal(existsSync(configPath), true, "Claude Code CLI turns should create project MCP config");
|
|
130
|
+
|
|
131
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as {
|
|
132
|
+
mcpServers?: Record<string, unknown>;
|
|
133
|
+
};
|
|
134
|
+
assert.ok(parsed.mcpServers?.[GSD_WORKFLOW_MCP_SERVER_NAME]);
|
|
135
|
+
assert.match(notifications.join("\n"), /Claude Code MCP prepared/);
|
|
136
|
+
});
|
|
@@ -148,6 +148,60 @@ test("executeTaskComplete coerces string verificationEvidence entries", async ()
|
|
|
148
148
|
}
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
+
test("executeTaskComplete derives missing verification from evidence", async () => {
|
|
152
|
+
const base = makeTmpBase();
|
|
153
|
+
try {
|
|
154
|
+
openTestDb(base);
|
|
155
|
+
const planDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
|
|
156
|
+
mkdirSync(planDir, { recursive: true });
|
|
157
|
+
writeFileSync(join(planDir, "S01-PLAN.md"), "# S01\n\n- [ ] **T01: Demo** `est:5m`\n");
|
|
158
|
+
|
|
159
|
+
const result = await inProjectDir(base, () => executeTaskComplete({
|
|
160
|
+
milestoneId: "M001",
|
|
161
|
+
sliceId: "S01",
|
|
162
|
+
taskId: "T01",
|
|
163
|
+
oneLiner: "Completed task",
|
|
164
|
+
narrative: "Did the work",
|
|
165
|
+
verificationEvidence: [
|
|
166
|
+
{ command: "npm test", exitCode: 0, verdict: "pass", durationMs: 1234 },
|
|
167
|
+
],
|
|
168
|
+
}, base));
|
|
169
|
+
|
|
170
|
+
assert.equal(result.details.operation, "complete_task");
|
|
171
|
+
const db = _getAdapter();
|
|
172
|
+
assert.ok(db, "DB should be open");
|
|
173
|
+
const row = db!.prepare(
|
|
174
|
+
"SELECT verification_result FROM tasks WHERE milestone_id = ? AND slice_id = ? AND id = ?",
|
|
175
|
+
).get("M001", "S01", "T01") as Record<string, unknown> | undefined;
|
|
176
|
+
|
|
177
|
+
assert.match(String(row?.verification_result), /Verification evidence recorded/);
|
|
178
|
+
assert.match(String(row?.verification_result), /`npm test` exited 0 \(pass\)/);
|
|
179
|
+
} finally {
|
|
180
|
+
closeDatabase();
|
|
181
|
+
cleanup(base);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("executeTaskComplete returns a tool error when verification cannot be derived", async () => {
|
|
186
|
+
const base = makeTmpBase();
|
|
187
|
+
try {
|
|
188
|
+
openTestDb(base);
|
|
189
|
+
const result = await inProjectDir(base, () => executeTaskComplete({
|
|
190
|
+
milestoneId: "M001",
|
|
191
|
+
sliceId: "S01",
|
|
192
|
+
taskId: "T01",
|
|
193
|
+
oneLiner: "Completed task",
|
|
194
|
+
narrative: "Did the work",
|
|
195
|
+
}, base));
|
|
196
|
+
|
|
197
|
+
assert.equal(result.isError, true);
|
|
198
|
+
assert.match(String(result.content[0]?.text), /verification is required/);
|
|
199
|
+
} finally {
|
|
200
|
+
closeDatabase();
|
|
201
|
+
cleanup(base);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
151
205
|
test("executeSliceComplete preserves omitted optional requirement arrays", async () => {
|
|
152
206
|
const base = makeTmpBase();
|
|
153
207
|
try {
|
|
@@ -9,7 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
import { probeGitConflictState } from "../git-conflict-state.js";
|
|
10
10
|
import { ensureWorkspaceGitReadyForPath } from "../workspace-git-preflight.js";
|
|
11
11
|
import { isWorkspaceGitAllowedCommand } from "../workspace-git-guard.js";
|
|
12
|
-
import { cleanup, git, makeTempRepo } from "./test-utils.ts";
|
|
12
|
+
import { cleanup, git, makeTempDir, makeTempRepo } from "./test-utils.ts";
|
|
13
13
|
|
|
14
14
|
function seedGsdConflict(base: string): void {
|
|
15
15
|
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
@@ -60,6 +60,21 @@ test("probeGitConflictState reports clean repo", () => {
|
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
test("ensureWorkspaceGitReadyForPath allows fresh non-git project setup folders", async () => {
|
|
64
|
+
const base = makeTempDir("gsd-ws-git-non-repo-");
|
|
65
|
+
try {
|
|
66
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
67
|
+
|
|
68
|
+
const probe = probeGitConflictState(base);
|
|
69
|
+
assert.equal(probe.status, "clean");
|
|
70
|
+
|
|
71
|
+
const ready = await ensureWorkspaceGitReadyForPath(base);
|
|
72
|
+
assert.equal(ready.ok, true);
|
|
73
|
+
} finally {
|
|
74
|
+
cleanup(base);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
63
78
|
test("ensureWorkspaceGitReadyForPath auto-resolves .gsd/ conflicts", async () => {
|
|
64
79
|
const base = makeTempRepo("gsd-ws-git-heal-");
|
|
65
80
|
try {
|
|
@@ -212,6 +212,34 @@ test("enterMilestone returns ok:true mode:none when isolation disabled", () => {
|
|
|
212
212
|
assert.equal(s.basePath, "/project");
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
+
test("adoptStrandedMilestone forces branch recovery even when normal preferences differ", (t) => {
|
|
216
|
+
const previousCwd = process.cwd();
|
|
217
|
+
const base = makeGitRepoBase({ isolation: "worktree" });
|
|
218
|
+
t.after(() => cleanupRepoBase(base, previousCwd));
|
|
219
|
+
|
|
220
|
+
const s = makeSession({ basePath: base, originalBasePath: base });
|
|
221
|
+
const deps = makeDeps();
|
|
222
|
+
const ctx = makeCtx();
|
|
223
|
+
const lifecycle = new WorktreeLifecycle(s, deps);
|
|
224
|
+
|
|
225
|
+
const result = lifecycle.adoptStrandedMilestone("M001", base, ctx, {
|
|
226
|
+
mode: "branch",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
assert.equal(result.ok, true, `expected ok:true, got: ${JSON.stringify(result)}`);
|
|
230
|
+
if (result.ok) {
|
|
231
|
+
assert.equal(result.mode, "branch");
|
|
232
|
+
assert.equal(result.path, base);
|
|
233
|
+
}
|
|
234
|
+
assert.equal(s.basePath, base);
|
|
235
|
+
assert.equal(s.strandedRecoveryIsolationMode, "branch");
|
|
236
|
+
const currentBranch = execFileSync("git", ["branch", "--show-current"], {
|
|
237
|
+
cwd: base,
|
|
238
|
+
encoding: "utf-8",
|
|
239
|
+
}).trim();
|
|
240
|
+
assert.equal(currentBranch, "milestone/M001");
|
|
241
|
+
});
|
|
242
|
+
|
|
215
243
|
test("enterMilestone returns ok:false reason:isolation-degraded when session degraded", () => {
|
|
216
244
|
const s = makeSession({ isolationDegraded: true });
|
|
217
245
|
const deps = makeDeps({ getIsolationMode: () => "branch" });
|
|
@@ -13,7 +13,7 @@ import { mkdtempSync, mkdirSync, rmSync, existsSync, writeFileSync, readFileSync
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
15
|
|
|
16
|
-
import { runWorktreePostCreateHook } from "../
|
|
16
|
+
import { runWorktreePostCreateHook } from "../worktree-post-create-hook.ts";
|
|
17
17
|
|
|
18
18
|
function makeTmpDir(): string {
|
|
19
19
|
return mkdtempSync(join(tmpdir(), "gsd-wt-hook-test-"));
|
|
@@ -45,10 +45,150 @@ function writeNodeHookScript(filePath: string, code: string): void {
|
|
|
45
45
|
test("returns null when no hook path is provided", () => {
|
|
46
46
|
const src = makeTmpDir();
|
|
47
47
|
const wt = makeTmpDir();
|
|
48
|
+
const previousGsdHome = process.env.GSD_HOME;
|
|
48
49
|
try {
|
|
50
|
+
process.env.GSD_HOME = join(src, "empty-gsd-home");
|
|
49
51
|
const result = runWorktreePostCreateHook(src, wt, undefined);
|
|
50
52
|
assert.equal(result, null);
|
|
51
53
|
} finally {
|
|
54
|
+
if (previousGsdHome === undefined) delete process.env.GSD_HOME;
|
|
55
|
+
else process.env.GSD_HOME = previousGsdHome;
|
|
56
|
+
rmSync(src, { recursive: true, force: true });
|
|
57
|
+
rmSync(wt, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("reads git.worktree_post_create from project preferences without loading auto-worktree", () => {
|
|
62
|
+
const src = makeTmpDir();
|
|
63
|
+
const wt = makeTmpDir();
|
|
64
|
+
const previousGsdHome = process.env.GSD_HOME;
|
|
65
|
+
try {
|
|
66
|
+
process.env.GSD_HOME = join(src, "empty-gsd-home");
|
|
67
|
+
const hooksDir = join(src, ".gsd", "hooks");
|
|
68
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
69
|
+
writeFileSync(
|
|
70
|
+
join(src, ".gsd", "PREFERENCES.md"),
|
|
71
|
+
[
|
|
72
|
+
"---",
|
|
73
|
+
"git:",
|
|
74
|
+
` worktree_post_create: ${hookPath(".gsd/hooks/post-create")}`,
|
|
75
|
+
"---",
|
|
76
|
+
"",
|
|
77
|
+
].join("\n"),
|
|
78
|
+
);
|
|
79
|
+
const hookFile = hookPath(join(hooksDir, "post-create"));
|
|
80
|
+
writeNodeHookScript(
|
|
81
|
+
hookFile,
|
|
82
|
+
`require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "ok");`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const result = runWorktreePostCreateHook(src, wt);
|
|
86
|
+
|
|
87
|
+
assert.equal(result, null);
|
|
88
|
+
assert.ok(existsSync(join(wt, "configured-hook-ran")), "configured hook should run");
|
|
89
|
+
} finally {
|
|
90
|
+
if (previousGsdHome === undefined) delete process.env.GSD_HOME;
|
|
91
|
+
else process.env.GSD_HOME = previousGsdHome;
|
|
92
|
+
rmSync(src, { recursive: true, force: true });
|
|
93
|
+
rmSync(wt, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("prefers canonical project PREFERENCES.md over legacy lowercase preferences.md", () => {
|
|
98
|
+
const src = makeTmpDir();
|
|
99
|
+
const wt = makeTmpDir();
|
|
100
|
+
const previousGsdHome = process.env.GSD_HOME;
|
|
101
|
+
try {
|
|
102
|
+
process.env.GSD_HOME = join(src, "empty-gsd-home");
|
|
103
|
+
const hooksDir = join(src, ".gsd", "hooks");
|
|
104
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
105
|
+
writeFileSync(
|
|
106
|
+
join(src, ".gsd", "preferences.md"),
|
|
107
|
+
[
|
|
108
|
+
"---",
|
|
109
|
+
"git:",
|
|
110
|
+
` worktree_post_create: ${hookPath(".gsd/hooks/legacy")}`,
|
|
111
|
+
"---",
|
|
112
|
+
"",
|
|
113
|
+
].join("\n"),
|
|
114
|
+
);
|
|
115
|
+
writeFileSync(
|
|
116
|
+
join(src, ".gsd", "PREFERENCES.md"),
|
|
117
|
+
[
|
|
118
|
+
"---",
|
|
119
|
+
"git:",
|
|
120
|
+
` worktree_post_create: ${hookPath(".gsd/hooks/canonical")}`,
|
|
121
|
+
"---",
|
|
122
|
+
"",
|
|
123
|
+
].join("\n"),
|
|
124
|
+
);
|
|
125
|
+
writeNodeHookScript(
|
|
126
|
+
hookPath(join(hooksDir, "canonical")),
|
|
127
|
+
`require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "canonical");`,
|
|
128
|
+
);
|
|
129
|
+
writeNodeHookScript(
|
|
130
|
+
hookPath(join(hooksDir, "legacy")),
|
|
131
|
+
`require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "legacy");`,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const result = runWorktreePostCreateHook(src, wt);
|
|
135
|
+
|
|
136
|
+
assert.equal(result, null);
|
|
137
|
+
assert.equal(readFileSync(join(wt, "configured-hook-ran"), "utf-8"), "canonical");
|
|
138
|
+
} finally {
|
|
139
|
+
if (previousGsdHome === undefined) delete process.env.GSD_HOME;
|
|
140
|
+
else process.env.GSD_HOME = previousGsdHome;
|
|
141
|
+
rmSync(src, { recursive: true, force: true });
|
|
142
|
+
rmSync(wt, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("prefers canonical global PREFERENCES.md over legacy lowercase preferences.md", () => {
|
|
147
|
+
const src = makeTmpDir();
|
|
148
|
+
const wt = makeTmpDir();
|
|
149
|
+
const previousGsdHome = process.env.GSD_HOME;
|
|
150
|
+
try {
|
|
151
|
+
const gsdHomeDir = join(src, "global-gsd-home");
|
|
152
|
+
process.env.GSD_HOME = gsdHomeDir;
|
|
153
|
+
const hooksDir = join(src, ".gsd", "hooks");
|
|
154
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
155
|
+
mkdirSync(gsdHomeDir, { recursive: true });
|
|
156
|
+
writeFileSync(
|
|
157
|
+
join(gsdHomeDir, "preferences.md"),
|
|
158
|
+
[
|
|
159
|
+
"---",
|
|
160
|
+
"git:",
|
|
161
|
+
` worktree_post_create: ${hookPath(".gsd/hooks/legacy")}`,
|
|
162
|
+
"---",
|
|
163
|
+
"",
|
|
164
|
+
].join("\n"),
|
|
165
|
+
);
|
|
166
|
+
writeFileSync(
|
|
167
|
+
join(gsdHomeDir, "PREFERENCES.md"),
|
|
168
|
+
[
|
|
169
|
+
"---",
|
|
170
|
+
"git:",
|
|
171
|
+
` worktree_post_create: ${hookPath(".gsd/hooks/canonical")}`,
|
|
172
|
+
"---",
|
|
173
|
+
"",
|
|
174
|
+
].join("\n"),
|
|
175
|
+
);
|
|
176
|
+
writeNodeHookScript(
|
|
177
|
+
hookPath(join(hooksDir, "canonical")),
|
|
178
|
+
`require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "canonical");`,
|
|
179
|
+
);
|
|
180
|
+
writeNodeHookScript(
|
|
181
|
+
hookPath(join(hooksDir, "legacy")),
|
|
182
|
+
`require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "legacy");`,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const result = runWorktreePostCreateHook(src, wt);
|
|
186
|
+
|
|
187
|
+
assert.equal(result, null);
|
|
188
|
+
assert.equal(readFileSync(join(wt, "configured-hook-ran"), "utf-8"), "canonical");
|
|
189
|
+
} finally {
|
|
190
|
+
if (previousGsdHome === undefined) delete process.env.GSD_HOME;
|
|
191
|
+
else process.env.GSD_HOME = previousGsdHome;
|
|
52
192
|
rmSync(src, { recursive: true, force: true });
|
|
53
193
|
rmSync(wt, { recursive: true, force: true });
|
|
54
194
|
}
|