@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
|
@@ -507,6 +507,21 @@ function initSessionNotifications(ctx: ExtensionContext): void {
|
|
|
507
507
|
initNotificationWidget(ctx);
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
+
async function prepareWorkflowMcpForHookContext(
|
|
511
|
+
ctx: ExtensionContext,
|
|
512
|
+
basePath: string,
|
|
513
|
+
): Promise<void> {
|
|
514
|
+
// Skip MCP auto-prep when running inside an auto-worktree. The worktree
|
|
515
|
+
// already has .mcp.json from createAutoWorktree, and re-running the writer
|
|
516
|
+
// post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
|
|
517
|
+
// CLI path resolution), dirtying the tree and breaking the milestone merge.
|
|
518
|
+
const { isInAutoWorktree } = await import("../auto-worktree.js");
|
|
519
|
+
if (isInAutoWorktree(basePath)) return;
|
|
520
|
+
|
|
521
|
+
const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
|
|
522
|
+
prepareWorkflowMcpForProject(ctx, basePath);
|
|
523
|
+
}
|
|
524
|
+
|
|
510
525
|
export function registerHooks(
|
|
511
526
|
pi: ExtensionAPI,
|
|
512
527
|
ecosystemHandlers: GSDEcosystemBeforeAgentStartHandler[],
|
|
@@ -532,12 +547,7 @@ export function registerHooks(
|
|
|
532
547
|
await syncServiceTierStatus(ctx);
|
|
533
548
|
await applyDisabledModelProviderPolicy(ctx);
|
|
534
549
|
await applyCompactionThresholdOverride(ctx);
|
|
535
|
-
|
|
536
|
-
const { isInAutoWorktree } = await import("../auto-worktree.js");
|
|
537
|
-
if (!isInAutoWorktree(basePath)) {
|
|
538
|
-
const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
|
|
539
|
-
prepareWorkflowMcpForProject(ctx, basePath);
|
|
540
|
-
}
|
|
550
|
+
await prepareWorkflowMcpForHookContext(ctx, basePath);
|
|
541
551
|
|
|
542
552
|
// Apply show_token_cost preference (#1515)
|
|
543
553
|
try {
|
|
@@ -563,15 +573,7 @@ export function registerHooks(
|
|
|
563
573
|
await syncServiceTierStatus(ctx);
|
|
564
574
|
await applyDisabledModelProviderPolicy(ctx);
|
|
565
575
|
await applyCompactionThresholdOverride(ctx);
|
|
566
|
-
|
|
567
|
-
// already has .mcp.json from createAutoWorktree, and re-running the writer
|
|
568
|
-
// post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
|
|
569
|
-
// CLI path resolution), dirtying the tree and breaking the milestone merge.
|
|
570
|
-
const { isInAutoWorktree } = await import("../auto-worktree.js");
|
|
571
|
-
if (!isInAutoWorktree(basePath)) {
|
|
572
|
-
const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
|
|
573
|
-
prepareWorkflowMcpForProject(ctx, basePath);
|
|
574
|
-
}
|
|
576
|
+
await prepareWorkflowMcpForHookContext(ctx, basePath);
|
|
575
577
|
await loadToolApiKeysForSession();
|
|
576
578
|
if (!isAutoActive()) {
|
|
577
579
|
ctx.ui.setWidget("gsd-progress", undefined);
|
|
@@ -608,6 +610,11 @@ export function registerHooks(
|
|
|
608
610
|
}
|
|
609
611
|
clearDeferredApprovalGate(beforeAgentBasePath);
|
|
610
612
|
|
|
613
|
+
// session_start can fire before the active provider has settled. By
|
|
614
|
+
// before_agent_start, Claude Code CLI sessions should get the same
|
|
615
|
+
// project MCP config that /gsd mcp init would write.
|
|
616
|
+
await prepareWorkflowMcpForHookContext(ctx, beforeAgentBasePath);
|
|
617
|
+
|
|
611
618
|
// GSD's own context injection (existing behavior — unchanged).
|
|
612
619
|
const { buildBeforeAgentStartResult } = await import("./system-context.js");
|
|
613
620
|
const gsdResult = await buildBeforeAgentStartResult(event, ctx);
|
|
@@ -205,7 +205,12 @@ export function getCloseoutManualResolveBlocker(basePath: string): string | null
|
|
|
205
205
|
return `Unmerged paths remain in ${basePath}: ${conflictProbe.unmerged.slice(0, 5).join(", ")}`;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
let status: string;
|
|
209
|
+
try {
|
|
210
|
+
status = runGit(basePath, ["status", "--porcelain"]);
|
|
211
|
+
} catch {
|
|
212
|
+
return `Could not inspect git status in ${basePath}.`;
|
|
213
|
+
}
|
|
209
214
|
if (status) {
|
|
210
215
|
return `Working tree still has uncommitted changes in ${basePath}. Commit, stash, or run /gsd closeout retry first.`;
|
|
211
216
|
}
|
|
@@ -208,7 +208,15 @@ export async function handleAutoCommand(trimmed: string, ctx: ExtensionCommandCo
|
|
|
208
208
|
|
|
209
209
|
if (trimmed === "") {
|
|
210
210
|
if (!(await guardRemoteSession(ctx, pi))) return true;
|
|
211
|
-
|
|
211
|
+
const basePath = projectRoot();
|
|
212
|
+
const { hasGsdBootstrapArtifacts } = await import("../../detection.js");
|
|
213
|
+
const { gsdRoot } = await import("../../paths.js");
|
|
214
|
+
if (!hasGsdBootstrapArtifacts(gsdRoot(basePath))) {
|
|
215
|
+
const { showSmartEntry } = await import("../../guided-flow.js");
|
|
216
|
+
await showSmartEntry(ctx, pi, basePath, { step: true });
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
if (await hasUnresolvedCloseoutBlocker(ctx, basePath)) return true;
|
|
212
220
|
const { showGsdHome } = await import("../../gsd-command-home.js");
|
|
213
221
|
await showGsdHome(ctx, pi, projectRoot());
|
|
214
222
|
return true;
|
|
@@ -26,6 +26,7 @@ import { isAutoActive, checkRemoteAutoSession } from "./auto.js";
|
|
|
26
26
|
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
27
27
|
import { currentDirectoryRoot, projectRoot } from "./commands/context.js";
|
|
28
28
|
import { loadPrompt } from "./prompt-loader.js";
|
|
29
|
+
import { isPnpmInstall } from "../../shared/package-manager-detection.js";
|
|
29
30
|
import {
|
|
30
31
|
buildDoctorHealIssuePayload,
|
|
31
32
|
buildDoctorHealSummary,
|
|
@@ -57,6 +58,7 @@ function isBunInstall(argv1: string | undefined = process.argv[1]): boolean {
|
|
|
57
58
|
|
|
58
59
|
function resolveInstallCommand(pkg: string): string {
|
|
59
60
|
if (isBunInstall()) return `bun add -g ${pkg}`;
|
|
61
|
+
if (isPnpmInstall()) return `pnpm add -g ${pkg}`;
|
|
60
62
|
return `npm install -g ${pkg}`;
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
|
-
import { join } from "node:path";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
|
|
8
8
|
import { autoResolveSafeConflictPaths } from "./git-conflict-resolve.js";
|
|
9
9
|
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
@@ -14,6 +14,21 @@ function splitZeroDelimited(output: string): string[] {
|
|
|
14
14
|
return output.split("\0").filter(Boolean);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function hasGitMarker(basePath: string): boolean {
|
|
18
|
+
try {
|
|
19
|
+
let dir = resolve(basePath);
|
|
20
|
+
for (let i = 0; i < 30; i++) {
|
|
21
|
+
if (existsSync(join(dir, ".git"))) return true;
|
|
22
|
+
const parent = dirname(dir);
|
|
23
|
+
if (parent === dir) break;
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// Fall through to the git probes, which will report unknown on failure.
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
export function listUnmergedGitPaths(basePath: string): string[] | null {
|
|
18
33
|
try {
|
|
19
34
|
const output = spawnSync("git", ["diff", "--name-only", "--diff-filter=U", "-z"], {
|
|
@@ -75,6 +90,15 @@ export interface GitConflictProbeResult {
|
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
export function probeGitConflictState(basePath: string): GitConflictProbeResult {
|
|
93
|
+
if (!hasGitMarker(basePath)) {
|
|
94
|
+
return {
|
|
95
|
+
status: "clean",
|
|
96
|
+
unmerged: [],
|
|
97
|
+
checkFailures: [],
|
|
98
|
+
mergeStateBlockers: [],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
78
102
|
const unmerged = listUnmergedGitPaths(basePath);
|
|
79
103
|
if (unmerged === null) {
|
|
80
104
|
return {
|
|
@@ -52,6 +52,103 @@ function makeRepoWithUnmergedCompletedMilestone(): string {
|
|
|
52
52
|
return base;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function makeRepoWithStrandedActiveMilestone(options: { deepPlanning?: boolean } = {}): string {
|
|
56
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-stranded-bootstrap-"));
|
|
57
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
58
|
+
writeFileSync(
|
|
59
|
+
join(base, ".gsd", "PREFERENCES.md"),
|
|
60
|
+
options.deepPlanning
|
|
61
|
+
? "---\nplanning_depth: deep\ngit:\n isolation: \"none\"\n---\n"
|
|
62
|
+
: "---\ngit:\n isolation: \"none\"\n---\n",
|
|
63
|
+
);
|
|
64
|
+
runGit(base, ["init"]);
|
|
65
|
+
runGit(base, ["config", "user.email", "test@test.com"]);
|
|
66
|
+
runGit(base, ["config", "user.name", "Test"]);
|
|
67
|
+
writeFileSync(join(base, "README.md"), "# test\n");
|
|
68
|
+
runGit(base, ["add", "-A"]);
|
|
69
|
+
runGit(base, ["commit", "-m", "init"]);
|
|
70
|
+
runGit(base, ["branch", "-M", "main"]);
|
|
71
|
+
|
|
72
|
+
runGit(base, ["checkout", "-b", "milestone/M001"]);
|
|
73
|
+
writeFileSync(join(base, "m001.txt"), "in-progress stranded work\n");
|
|
74
|
+
runGit(base, ["add", "-A"]);
|
|
75
|
+
runGit(base, ["commit", "-m", "feat: M001 in progress"]);
|
|
76
|
+
runGit(base, ["checkout", "main"]);
|
|
77
|
+
|
|
78
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
79
|
+
insertMilestone({ id: "M001", title: "Active milestone", status: "active" });
|
|
80
|
+
closeDatabase();
|
|
81
|
+
|
|
82
|
+
return base;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function makeRepoWithMultipleStrandedMilestones(): string {
|
|
86
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-multiple-stranded-bootstrap-"));
|
|
87
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
88
|
+
mkdirSync(join(base, ".gsd", "milestones", "M002"), { recursive: true });
|
|
89
|
+
writeFileSync(
|
|
90
|
+
join(base, ".gsd", "PREFERENCES.md"),
|
|
91
|
+
"---\ngit:\n isolation: \"none\"\n---\n",
|
|
92
|
+
);
|
|
93
|
+
runGit(base, ["init"]);
|
|
94
|
+
runGit(base, ["config", "user.email", "test@test.com"]);
|
|
95
|
+
runGit(base, ["config", "user.name", "Test"]);
|
|
96
|
+
writeFileSync(join(base, "README.md"), "# test\n");
|
|
97
|
+
runGit(base, ["add", "-A"]);
|
|
98
|
+
runGit(base, ["commit", "-m", "init"]);
|
|
99
|
+
runGit(base, ["branch", "-M", "main"]);
|
|
100
|
+
|
|
101
|
+
runGit(base, ["checkout", "-b", "milestone/M001"]);
|
|
102
|
+
writeFileSync(join(base, "m001.txt"), "active stranded work\n");
|
|
103
|
+
runGit(base, ["add", "-A"]);
|
|
104
|
+
runGit(base, ["commit", "-m", "feat: M001 in progress"]);
|
|
105
|
+
runGit(base, ["checkout", "main"]);
|
|
106
|
+
|
|
107
|
+
runGit(base, ["checkout", "-b", "milestone/M002"]);
|
|
108
|
+
writeFileSync(join(base, "m002.txt"), "additional stranded work\n");
|
|
109
|
+
runGit(base, ["add", "-A"]);
|
|
110
|
+
runGit(base, ["commit", "-m", "feat: M002 in progress"]);
|
|
111
|
+
runGit(base, ["checkout", "main"]);
|
|
112
|
+
|
|
113
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
114
|
+
insertMilestone({ id: "M001", title: "Active milestone", status: "active" });
|
|
115
|
+
insertMilestone({ id: "M002", title: "Pending milestone", status: "pending" });
|
|
116
|
+
closeDatabase();
|
|
117
|
+
|
|
118
|
+
return base;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function makeRepoWithRecoveredCleanupAndStrandedMismatch(): string {
|
|
122
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-headless-stranded-bootstrap-"));
|
|
123
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
124
|
+
mkdirSync(join(base, ".gsd", "milestones", "M002"), { recursive: true });
|
|
125
|
+
writeFileSync(
|
|
126
|
+
join(base, ".gsd", "PREFERENCES.md"),
|
|
127
|
+
"---\ngit:\n isolation: \"none\"\n---\n",
|
|
128
|
+
);
|
|
129
|
+
runGit(base, ["init"]);
|
|
130
|
+
runGit(base, ["config", "user.email", "test@test.com"]);
|
|
131
|
+
runGit(base, ["config", "user.name", "Test"]);
|
|
132
|
+
writeFileSync(join(base, "README.md"), "# test\n");
|
|
133
|
+
runGit(base, ["add", "-A"]);
|
|
134
|
+
runGit(base, ["commit", "-m", "init"]);
|
|
135
|
+
runGit(base, ["branch", "-M", "main"]);
|
|
136
|
+
|
|
137
|
+
runGit(base, ["branch", "milestone/M001"]);
|
|
138
|
+
runGit(base, ["checkout", "-b", "milestone/M002"]);
|
|
139
|
+
writeFileSync(join(base, "m002.txt"), "in-progress stranded work\n");
|
|
140
|
+
runGit(base, ["add", "-A"]);
|
|
141
|
+
runGit(base, ["commit", "-m", "feat: M002 in progress"]);
|
|
142
|
+
runGit(base, ["checkout", "main"]);
|
|
143
|
+
|
|
144
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
145
|
+
insertMilestone({ id: "M001", title: "Completed milestone", status: "complete" });
|
|
146
|
+
insertMilestone({ id: "M002", title: "Stranded milestone", status: "active" });
|
|
147
|
+
closeDatabase();
|
|
148
|
+
|
|
149
|
+
return base;
|
|
150
|
+
}
|
|
151
|
+
|
|
55
152
|
function makeCtx(notifications: Array<{ message: string; level?: string }>) {
|
|
56
153
|
const model = { provider: "claude-code", id: "claude-sonnet-4-6", contextWindow: 128000 };
|
|
57
154
|
return {
|
|
@@ -159,3 +256,342 @@ test("bootstrap aborts before starting next milestone when completed orphan merg
|
|
|
159
256
|
rmSync(base, { recursive: true, force: true });
|
|
160
257
|
}
|
|
161
258
|
});
|
|
259
|
+
|
|
260
|
+
test("headless bootstrap checks stranded work before recovered-complete shortcut", async () => {
|
|
261
|
+
const base = makeRepoWithRecoveredCleanupAndStrandedMismatch();
|
|
262
|
+
const previousCwd = process.cwd();
|
|
263
|
+
const previousHeadless = process.env.GSD_HEADLESS;
|
|
264
|
+
const previousParallelWorker = process.env.GSD_PARALLEL_WORKER;
|
|
265
|
+
const previousMilestoneLock = process.env.GSD_MILESTONE_LOCK;
|
|
266
|
+
const s = new AutoSession();
|
|
267
|
+
const notifications: Array<{ message: string; level?: string }> = [];
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
process.env.GSD_HEADLESS = "1";
|
|
271
|
+
process.env.GSD_PARALLEL_WORKER = "1";
|
|
272
|
+
process.env.GSD_MILESTONE_LOCK = "M001";
|
|
273
|
+
|
|
274
|
+
const ready = await bootstrapAutoSession(
|
|
275
|
+
s,
|
|
276
|
+
makeCtx(notifications) as any,
|
|
277
|
+
{
|
|
278
|
+
getThinkingLevel: () => "medium",
|
|
279
|
+
getActiveTools: () => [],
|
|
280
|
+
events: { emit: () => {} },
|
|
281
|
+
} as any,
|
|
282
|
+
base,
|
|
283
|
+
false,
|
|
284
|
+
false,
|
|
285
|
+
{
|
|
286
|
+
shouldUseWorktreeIsolation: () => false,
|
|
287
|
+
registerSigtermHandler: () => {},
|
|
288
|
+
registerAutoWorkerForSession: () => {},
|
|
289
|
+
lockBase: () => base,
|
|
290
|
+
buildLifecycle: () => ({
|
|
291
|
+
adoptSessionRoot: (sessionBase: string, originalBase?: string) => {
|
|
292
|
+
s.basePath = sessionBase;
|
|
293
|
+
if (originalBase !== undefined) {
|
|
294
|
+
s.originalBasePath = originalBase;
|
|
295
|
+
} else if (!s.originalBasePath) {
|
|
296
|
+
s.originalBasePath = sessionBase;
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
enterMilestone: () => ({ ok: true, mode: "none", path: base }),
|
|
300
|
+
adoptOrphanWorktree: <T extends { merged: boolean }>(
|
|
301
|
+
_mid: string,
|
|
302
|
+
_base: string,
|
|
303
|
+
run: () => T,
|
|
304
|
+
): T => run(),
|
|
305
|
+
}) as any,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
classification: "none",
|
|
309
|
+
lock: null,
|
|
310
|
+
pausedSession: null,
|
|
311
|
+
state: null,
|
|
312
|
+
recovery: null,
|
|
313
|
+
recoveryPrompt: null,
|
|
314
|
+
recoveryToolCallCount: 0,
|
|
315
|
+
artifactSatisfied: false,
|
|
316
|
+
hasResumableDiskState: false,
|
|
317
|
+
isBootstrapCrash: false,
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const messages = notifications.map((entry) => entry.message).join("\n");
|
|
322
|
+
assert.equal(ready, false);
|
|
323
|
+
assert.match(messages, /Stranded work for M002 blocks auto-mode/);
|
|
324
|
+
assert.doesNotMatch(messages, /all milestones complete/);
|
|
325
|
+
} finally {
|
|
326
|
+
if (previousHeadless === undefined) {
|
|
327
|
+
delete process.env.GSD_HEADLESS;
|
|
328
|
+
} else {
|
|
329
|
+
process.env.GSD_HEADLESS = previousHeadless;
|
|
330
|
+
}
|
|
331
|
+
if (previousParallelWorker === undefined) {
|
|
332
|
+
delete process.env.GSD_PARALLEL_WORKER;
|
|
333
|
+
} else {
|
|
334
|
+
process.env.GSD_PARALLEL_WORKER = previousParallelWorker;
|
|
335
|
+
}
|
|
336
|
+
if (previousMilestoneLock === undefined) {
|
|
337
|
+
delete process.env.GSD_MILESTONE_LOCK;
|
|
338
|
+
} else {
|
|
339
|
+
process.env.GSD_MILESTONE_LOCK = previousMilestoneLock;
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
closeDatabase();
|
|
343
|
+
} catch {}
|
|
344
|
+
process.chdir(previousCwd);
|
|
345
|
+
rmSync(base, { recursive: true, force: true });
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("bootstrap blocks active stranded recovery when another open milestone also has stranded work", async () => {
|
|
350
|
+
const base = makeRepoWithMultipleStrandedMilestones();
|
|
351
|
+
const previousCwd = process.cwd();
|
|
352
|
+
const s = new AutoSession();
|
|
353
|
+
const adoptCalls: string[] = [];
|
|
354
|
+
const notifications: Array<{ message: string; level?: string }> = [];
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const ready = await bootstrapAutoSession(
|
|
358
|
+
s,
|
|
359
|
+
makeCtx(notifications) as any,
|
|
360
|
+
{
|
|
361
|
+
getThinkingLevel: () => "medium",
|
|
362
|
+
getActiveTools: () => [],
|
|
363
|
+
events: { emit: () => {} },
|
|
364
|
+
} as any,
|
|
365
|
+
base,
|
|
366
|
+
false,
|
|
367
|
+
false,
|
|
368
|
+
{
|
|
369
|
+
shouldUseWorktreeIsolation: () => false,
|
|
370
|
+
registerSigtermHandler: () => {},
|
|
371
|
+
registerAutoWorkerForSession: () => {},
|
|
372
|
+
lockBase: () => base,
|
|
373
|
+
buildLifecycle: () => ({
|
|
374
|
+
adoptSessionRoot: (sessionBase: string, originalBase?: string) => {
|
|
375
|
+
s.basePath = sessionBase;
|
|
376
|
+
if (originalBase !== undefined) {
|
|
377
|
+
s.originalBasePath = originalBase;
|
|
378
|
+
} else if (!s.originalBasePath) {
|
|
379
|
+
s.originalBasePath = sessionBase;
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
enterMilestone: () => ({ ok: true, mode: "none", path: base }),
|
|
383
|
+
adoptStrandedMilestone: (milestoneId: string) => {
|
|
384
|
+
adoptCalls.push(milestoneId);
|
|
385
|
+
return { ok: true, mode: "branch", path: base };
|
|
386
|
+
},
|
|
387
|
+
adoptOrphanWorktree: <T extends { merged: boolean }>(
|
|
388
|
+
_mid: string,
|
|
389
|
+
_base: string,
|
|
390
|
+
run: () => T,
|
|
391
|
+
): T => run(),
|
|
392
|
+
}) as any,
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
classification: "none",
|
|
396
|
+
lock: null,
|
|
397
|
+
pausedSession: null,
|
|
398
|
+
state: null,
|
|
399
|
+
recovery: null,
|
|
400
|
+
recoveryPrompt: null,
|
|
401
|
+
recoveryToolCallCount: 0,
|
|
402
|
+
artifactSatisfied: false,
|
|
403
|
+
hasResumableDiskState: false,
|
|
404
|
+
isBootstrapCrash: false,
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const messages = notifications.map((entry) => entry.message).join("\n");
|
|
409
|
+
assert.equal(ready, false);
|
|
410
|
+
assert.deepEqual(adoptCalls, []);
|
|
411
|
+
assert.match(messages, /Stranded work for M002 blocks auto-mode before M001/);
|
|
412
|
+
} finally {
|
|
413
|
+
try {
|
|
414
|
+
closeDatabase();
|
|
415
|
+
} catch {}
|
|
416
|
+
process.chdir(previousCwd);
|
|
417
|
+
rmSync(base, { recursive: true, force: true });
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("bootstrap adopts stranded active branch even when isolation is none", async () => {
|
|
422
|
+
const base = makeRepoWithStrandedActiveMilestone();
|
|
423
|
+
const previousCwd = process.cwd();
|
|
424
|
+
const s = new AutoSession();
|
|
425
|
+
const adoptCalls: Array<{ milestoneId: string; mode: string }> = [];
|
|
426
|
+
const enterCalls: string[] = [];
|
|
427
|
+
const notifications: Array<{ message: string; level?: string }> = [];
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
const ready = await bootstrapAutoSession(
|
|
431
|
+
s,
|
|
432
|
+
makeCtx(notifications) as any,
|
|
433
|
+
{
|
|
434
|
+
getThinkingLevel: () => "medium",
|
|
435
|
+
getActiveTools: () => [],
|
|
436
|
+
events: { emit: () => {} },
|
|
437
|
+
} as any,
|
|
438
|
+
base,
|
|
439
|
+
false,
|
|
440
|
+
false,
|
|
441
|
+
{
|
|
442
|
+
shouldUseWorktreeIsolation: () => false,
|
|
443
|
+
registerSigtermHandler: () => {},
|
|
444
|
+
registerAutoWorkerForSession: () => {},
|
|
445
|
+
lockBase: () => base,
|
|
446
|
+
buildLifecycle: () => ({
|
|
447
|
+
adoptSessionRoot: (sessionBase: string, originalBase?: string) => {
|
|
448
|
+
s.basePath = sessionBase;
|
|
449
|
+
if (originalBase !== undefined) {
|
|
450
|
+
s.originalBasePath = originalBase;
|
|
451
|
+
} else if (!s.originalBasePath) {
|
|
452
|
+
s.originalBasePath = sessionBase;
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
enterMilestone: (milestoneId: string) => {
|
|
456
|
+
enterCalls.push(milestoneId);
|
|
457
|
+
return { ok: true, mode: "none", path: base };
|
|
458
|
+
},
|
|
459
|
+
adoptStrandedMilestone: (
|
|
460
|
+
milestoneId: string,
|
|
461
|
+
sessionBase: string,
|
|
462
|
+
_ctx: unknown,
|
|
463
|
+
opts: { mode: "worktree" | "branch" },
|
|
464
|
+
) => {
|
|
465
|
+
adoptCalls.push({ milestoneId, mode: opts.mode });
|
|
466
|
+
s.basePath = sessionBase;
|
|
467
|
+
s.originalBasePath = sessionBase;
|
|
468
|
+
s.strandedRecoveryIsolationMode = opts.mode;
|
|
469
|
+
return { ok: true, mode: opts.mode, path: sessionBase };
|
|
470
|
+
},
|
|
471
|
+
adoptOrphanWorktree: <T extends { merged: boolean }>(
|
|
472
|
+
_mid: string,
|
|
473
|
+
_base: string,
|
|
474
|
+
run: () => T,
|
|
475
|
+
): T => run(),
|
|
476
|
+
}) as any,
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
classification: "none",
|
|
480
|
+
lock: null,
|
|
481
|
+
pausedSession: null,
|
|
482
|
+
state: null,
|
|
483
|
+
recovery: null,
|
|
484
|
+
recoveryPrompt: null,
|
|
485
|
+
recoveryToolCallCount: 0,
|
|
486
|
+
artifactSatisfied: false,
|
|
487
|
+
hasResumableDiskState: false,
|
|
488
|
+
isBootstrapCrash: false,
|
|
489
|
+
},
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
assert.equal(ready, true);
|
|
493
|
+
assert.deepEqual(adoptCalls, [{ milestoneId: "M001", mode: "branch" }]);
|
|
494
|
+
assert.deepEqual(enterCalls, []);
|
|
495
|
+
assert.equal(s.currentMilestoneId, "M001");
|
|
496
|
+
assert.equal(s.strandedRecoveryIsolationMode, "branch");
|
|
497
|
+
assert.match(
|
|
498
|
+
notifications.map((entry) => entry.message).join("\n"),
|
|
499
|
+
/Recovering stranded work for M001/,
|
|
500
|
+
);
|
|
501
|
+
} finally {
|
|
502
|
+
try {
|
|
503
|
+
closeDatabase();
|
|
504
|
+
} catch {}
|
|
505
|
+
process.chdir(previousCwd);
|
|
506
|
+
rmSync(base, { recursive: true, force: true });
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test("bootstrap adopts stranded active branch before deep project setup", async () => {
|
|
511
|
+
const base = makeRepoWithStrandedActiveMilestone({ deepPlanning: true });
|
|
512
|
+
const previousCwd = process.cwd();
|
|
513
|
+
const s = new AutoSession();
|
|
514
|
+
const adoptCalls: Array<{ milestoneId: string; mode: string }> = [];
|
|
515
|
+
const enterCalls: string[] = [];
|
|
516
|
+
const notifications: Array<{ message: string; level?: string }> = [];
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
const ready = await bootstrapAutoSession(
|
|
520
|
+
s,
|
|
521
|
+
makeCtx(notifications) as any,
|
|
522
|
+
{
|
|
523
|
+
getThinkingLevel: () => "medium",
|
|
524
|
+
getActiveTools: () => [],
|
|
525
|
+
events: { emit: () => {} },
|
|
526
|
+
} as any,
|
|
527
|
+
base,
|
|
528
|
+
false,
|
|
529
|
+
false,
|
|
530
|
+
{
|
|
531
|
+
shouldUseWorktreeIsolation: () => false,
|
|
532
|
+
registerSigtermHandler: () => {},
|
|
533
|
+
registerAutoWorkerForSession: () => {},
|
|
534
|
+
lockBase: () => base,
|
|
535
|
+
buildLifecycle: () => ({
|
|
536
|
+
adoptSessionRoot: (sessionBase: string, originalBase?: string) => {
|
|
537
|
+
s.basePath = sessionBase;
|
|
538
|
+
if (originalBase !== undefined) {
|
|
539
|
+
s.originalBasePath = originalBase;
|
|
540
|
+
} else if (!s.originalBasePath) {
|
|
541
|
+
s.originalBasePath = sessionBase;
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
enterMilestone: (milestoneId: string) => {
|
|
545
|
+
enterCalls.push(milestoneId);
|
|
546
|
+
return { ok: true, mode: "none", path: base };
|
|
547
|
+
},
|
|
548
|
+
adoptStrandedMilestone: (
|
|
549
|
+
milestoneId: string,
|
|
550
|
+
sessionBase: string,
|
|
551
|
+
_ctx: unknown,
|
|
552
|
+
opts: { mode: "worktree" | "branch" },
|
|
553
|
+
) => {
|
|
554
|
+
adoptCalls.push({ milestoneId, mode: opts.mode });
|
|
555
|
+
s.basePath = sessionBase;
|
|
556
|
+
s.originalBasePath = sessionBase;
|
|
557
|
+
s.strandedRecoveryIsolationMode = opts.mode;
|
|
558
|
+
return { ok: true, mode: opts.mode, path: sessionBase };
|
|
559
|
+
},
|
|
560
|
+
adoptOrphanWorktree: <T extends { merged: boolean }>(
|
|
561
|
+
_mid: string,
|
|
562
|
+
_base: string,
|
|
563
|
+
run: () => T,
|
|
564
|
+
): T => run(),
|
|
565
|
+
}) as any,
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
classification: "none",
|
|
569
|
+
lock: null,
|
|
570
|
+
pausedSession: null,
|
|
571
|
+
state: null,
|
|
572
|
+
recovery: null,
|
|
573
|
+
recoveryPrompt: null,
|
|
574
|
+
recoveryToolCallCount: 0,
|
|
575
|
+
artifactSatisfied: false,
|
|
576
|
+
hasResumableDiskState: false,
|
|
577
|
+
isBootstrapCrash: false,
|
|
578
|
+
},
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
assert.equal(ready, true);
|
|
582
|
+
assert.deepEqual(adoptCalls, [{ milestoneId: "M001", mode: "branch" }]);
|
|
583
|
+
assert.deepEqual(enterCalls, []);
|
|
584
|
+
assert.equal(s.currentMilestoneId, "M001");
|
|
585
|
+
assert.equal(s.strandedRecoveryIsolationMode, "branch");
|
|
586
|
+
assert.match(
|
|
587
|
+
notifications.map((entry) => entry.message).join("\n"),
|
|
588
|
+
/Recovering stranded work for M001/,
|
|
589
|
+
);
|
|
590
|
+
} finally {
|
|
591
|
+
try {
|
|
592
|
+
closeDatabase();
|
|
593
|
+
} catch {}
|
|
594
|
+
process.chdir(previousCwd);
|
|
595
|
+
rmSync(base, { recursive: true, force: true });
|
|
596
|
+
}
|
|
597
|
+
});
|
|
@@ -9,6 +9,7 @@ import { tmpdir } from "node:os";
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
+
getCloseoutManualResolveBlocker,
|
|
12
13
|
listUnresolvedCloseoutFailures,
|
|
13
14
|
markLatestCloseoutFailureResolved,
|
|
14
15
|
retryLatestCloseoutFailure,
|
|
@@ -99,3 +100,17 @@ test("closeout manual resolve refuses a dirty worktree", () => {
|
|
|
99
100
|
rmSync(base, { recursive: true, force: true });
|
|
100
101
|
}
|
|
101
102
|
});
|
|
103
|
+
|
|
104
|
+
test("closeout manual resolve blocks non-git project roots without throwing", () => {
|
|
105
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-closeout-recovery-non-git-"));
|
|
106
|
+
try {
|
|
107
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
108
|
+
|
|
109
|
+
assert.equal(
|
|
110
|
+
getCloseoutManualResolveBlocker(base),
|
|
111
|
+
`Could not inspect git status in ${base}.`,
|
|
112
|
+
);
|
|
113
|
+
} finally {
|
|
114
|
+
rmSync(base, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
import test from "node:test";
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
|
-
import { writeFileSync } from "node:fs";
|
|
6
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
9
|
import { getWorkspaceGitBlockMessageForBase } from "../workspace-git-guard.js";
|
|
10
|
-
import { cleanup, git, makeTempRepo } from "./test-utils.ts";
|
|
10
|
+
import { cleanup, git, makeTempDir, makeTempRepo } from "./test-utils.ts";
|
|
11
11
|
|
|
12
12
|
function seedProductConflict(base: string): void {
|
|
13
13
|
writeFileSync(join(base, "app.ts"), "root\n");
|
|
@@ -41,6 +41,19 @@ test("getWorkspaceGitBlockMessageForBase blocks auto when product conflicts rema
|
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
test("getWorkspaceGitBlockMessageForBase does not block project setup in non-git folders", async () => {
|
|
45
|
+
const base = makeTempDir("gsd-dispatch-ws-git-new-project-");
|
|
46
|
+
try {
|
|
47
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
48
|
+
|
|
49
|
+
assert.equal(await getWorkspaceGitBlockMessageForBase(base, ""), null);
|
|
50
|
+
assert.equal(await getWorkspaceGitBlockMessageForBase(base, "init"), null);
|
|
51
|
+
assert.equal(await getWorkspaceGitBlockMessageForBase(base, "new-project"), null);
|
|
52
|
+
} finally {
|
|
53
|
+
cleanup(base);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
44
57
|
test("getWorkspaceGitBlockMessageForBase allows doctor on product conflicts", async () => {
|
|
45
58
|
const base = makeTempRepo("gsd-dispatch-ws-git-doctor-");
|
|
46
59
|
try {
|