gsd-pi 2.80.0-dev.c5c38454b → 2.80.0-dev.e146beb20

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.
Files changed (120) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/GSD-WORKFLOW.md +2 -2
  3. package/dist/resources/extensions/gsd/auto/loop.js +32 -1
  4. package/dist/resources/extensions/gsd/auto/phases.js +37 -30
  5. package/dist/resources/extensions/gsd/auto-post-unit.js +10 -10
  6. package/dist/resources/extensions/gsd/auto-prompts.js +111 -1
  7. package/dist/resources/extensions/gsd/auto.js +9 -1
  8. package/dist/resources/extensions/gsd/clean-root-preflight.js +42 -4
  9. package/dist/resources/extensions/gsd/commands/dispatcher.js +5 -0
  10. package/dist/resources/extensions/gsd/crash-recovery.js +56 -10
  11. package/dist/resources/extensions/gsd/detection.js +106 -0
  12. package/dist/resources/extensions/gsd/guided-flow.js +47 -10
  13. package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
  14. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
  15. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  16. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  17. package/dist/resources/extensions/gsd/safety/evidence-collector.js +10 -2
  18. package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
  19. package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
  20. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  21. package/dist/resources/extensions/gsd/worktree-manager.js +16 -14
  22. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  23. package/dist/web/standalone/.next/BUILD_ID +1 -1
  24. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  25. package/dist/web/standalone/.next/build-manifest.json +2 -2
  26. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.html +1 -1
  44. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  51. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  53. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  54. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  55. package/package.json +1 -1
  56. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +30 -0
  57. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  58. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
  59. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
  60. package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
  61. package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
  62. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
  63. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
  64. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
  65. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
  66. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +2 -0
  68. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
  70. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
  71. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
  72. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
  73. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
  74. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
  75. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
  76. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +36 -0
  77. package/packages/pi-coding-agent/src/core/db-snapshot.test.ts +32 -0
  78. package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
  79. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +2 -0
  80. package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
  81. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
  82. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  83. package/src/resources/GSD-WORKFLOW.md +2 -2
  84. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  85. package/src/resources/extensions/gsd/auto/loop.ts +50 -8
  86. package/src/resources/extensions/gsd/auto/phases.ts +42 -28
  87. package/src/resources/extensions/gsd/auto-post-unit.ts +10 -10
  88. package/src/resources/extensions/gsd/auto-prompts.ts +116 -1
  89. package/src/resources/extensions/gsd/auto.ts +12 -1
  90. package/src/resources/extensions/gsd/clean-root-preflight.ts +41 -3
  91. package/src/resources/extensions/gsd/commands/dispatcher.ts +6 -0
  92. package/src/resources/extensions/gsd/crash-recovery.ts +67 -10
  93. package/src/resources/extensions/gsd/detection.ts +128 -0
  94. package/src/resources/extensions/gsd/guided-flow.ts +47 -10
  95. package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
  96. package/src/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
  97. package/src/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  98. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  99. package/src/resources/extensions/gsd/safety/evidence-collector.ts +11 -2
  100. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1 -1
  101. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +88 -2
  102. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +55 -0
  103. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +22 -0
  104. package/src/resources/extensions/gsd/tests/detection.test.ts +140 -0
  105. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
  106. package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
  107. package/src/resources/extensions/gsd/tests/right-sized-workflow-prompts.test.ts +192 -0
  108. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +29 -0
  109. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +38 -0
  110. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +46 -2
  111. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +7 -0
  112. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +37 -6
  113. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +7 -0
  114. package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +9 -2
  115. package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
  116. package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
  117. package/src/resources/extensions/gsd/unit-runtime.ts +14 -0
  118. package/src/resources/extensions/gsd/worktree-manager.ts +15 -4
  119. /package/dist/web/standalone/.next/static/{TCSim36ZpcPu2WgeoC45g → y73quA-XdLo9n41nxphjW}/_buildManifest.js +0 -0
  120. /package/dist/web/standalone/.next/static/{TCSim36ZpcPu2WgeoC45g → y73quA-XdLo9n41nxphjW}/_ssgManifest.js +0 -0
@@ -144,6 +144,18 @@ test("safety-harness-bug2-race: bash evidence survives mid-unit reset between to
144
144
  assert.ok(bash[0].outputSnippet.includes("found"), "output snippet captured");
145
145
  });
146
146
 
147
+ test("safety-harness: gsd_exec counts as execution evidence", () => {
148
+ resetEvidence();
149
+
150
+ recordToolCall("tc-exec-1", "gsd_exec", { command: "grep -n render index.html" });
151
+ recordToolResult("tc-exec-1", "gsd_exec", "Command exited with code 0\n1:render\n", false);
152
+
153
+ const bash = getEvidence().filter((e): e is BashEvidence => e.kind === "bash");
154
+ assert.equal(bash.length, 1, "gsd_exec must be tracked as execution evidence");
155
+ assert.equal(bash[0].command, "grep -n render index.html");
156
+ assert.equal(bash[0].exitCode, 0);
157
+ });
158
+
147
159
  // ─── Bug 3: git diff HEAD~1 scope check ─────────────────────────────────────
148
160
 
149
161
  test("safety-harness-bug3: validateFileChanges works on initial commit (no HEAD~1)", (t) => {
@@ -237,3 +249,20 @@ test("safety-harness-bug3: validateFileChanges works on merge commit", (t) => {
237
249
  // Must produce a valid result without throwing
238
250
  assert.ok(audit !== null, "audit must be produced for merge commit repo");
239
251
  });
252
+
253
+ test("safety-harness: planned changed file avoids unexpected-file warning", (t) => {
254
+ const base = mkdtempSync(join(tmpdir(), "gsd-planned-file-"));
255
+ t.after(() => rmSync(base, { recursive: true, force: true }));
256
+
257
+ execFileSync("git", ["init"], { cwd: base });
258
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: base });
259
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd: base });
260
+ writeFileSync(join(base, "index.html"), "<main></main>\n");
261
+ execFileSync("git", ["add", "index.html"], { cwd: base });
262
+ execFileSync("git", ["commit", "-m", "add static app"], { cwd: base });
263
+
264
+ const audit = validateFileChanges(base, [], ["index.html"]);
265
+ assert.ok(audit !== null, "audit must be produced");
266
+ assert.deepEqual(audit!.unexpectedFiles, [], "planned index.html must not be unexpected");
267
+ assert.deepEqual(audit!.missingFiles, [], "planned index.html must not be missing");
268
+ });
@@ -47,6 +47,9 @@ test("guided-flow complete branch offers a chooser for next milestone or status"
47
47
  const branchChunk = guidedFlowSource.slice(branchIdx, nextBranchIdx === -1 ? branchIdx + 1600 : nextBranchIdx);
48
48
 
49
49
  assert.match(branchChunk, /showNextAction\(/, "complete branch should present a chooser");
50
+ assert.match(branchChunk, /id:\s*"quick_task"/, "complete branch should offer quick task before milestone planning");
51
+ assert.match(branchChunk, /Do a small bounded task without opening a milestone/, "quick task action should explain that it avoids milestones");
52
+ assert.match(branchChunk, /recommended:\s*true/, "quick task action should be the recommended complete-state action");
50
53
  assert.match(branchChunk, /findMilestoneIds\(basePath\)/, "complete branch should compute the next milestone id");
51
54
  assert.match(
52
55
  branchChunk,
@@ -56,6 +59,41 @@ test("guided-flow complete branch offers a chooser for next milestone or status"
56
59
  assert.match(branchChunk, /dispatchWorkflow\(pi, await prepareAndBuildDiscussPrompt\(/, "complete branch should dispatch the prepared discuss prompt");
57
60
  });
58
61
 
62
+ test("guided-flow quick task choices prompt for text and route through /gsd quick", () => {
63
+ const guidedFlowSource = readFileSync(join(import.meta.dirname, "..", "guided-flow.ts"), "utf-8");
64
+
65
+ assert.match(
66
+ guidedFlowSource,
67
+ /async function runQuickTaskChoice\([\s\S]*ctx\.ui\.input\("Quick task"[\s\S]*await import\("\.\/quick\.js"\)[\s\S]*await handleQuick\(task,\s*ctx,\s*pi\)/,
68
+ "quick task chooser should prompt for task text and route through handleQuick",
69
+ );
70
+
71
+ const notifyOnlyPattern = /if \(choice === "quick_task"\) \{\s*ctx\.ui\.notify\("Run \/gsd quick <task>/;
72
+ assert.doesNotMatch(
73
+ guidedFlowSource,
74
+ notifyOnlyPattern,
75
+ "quick task chooser must not merely print instructions after selection",
76
+ );
77
+
78
+ const quickChoiceCalls = guidedFlowSource.match(/if \(choice === "quick_task"\) \{\s*await runQuickTaskChoice\(ctx,\s*pi\);/g) ?? [];
79
+ assert.equal(quickChoiceCalls.length, 3, "all guided-flow quick task choices should dispatch the helper");
80
+ });
81
+
82
+ test("dispatcher routes multi-word freeform /gsd input through /gsd do", () => {
83
+ const dispatcherSource = readFileSync(join(import.meta.dirname, "..", "commands", "dispatcher.ts"), "utf-8");
84
+
85
+ assert.match(
86
+ dispatcherSource,
87
+ /if\s*\(trimmed\.includes\(" "\)\)\s*\{[\s\S]*handleDo\(trimmed,\s*ctx,\s*pi\)/,
88
+ "dispatcher should treat multi-word unknown input as natural-language /gsd do work",
89
+ );
90
+ assert.match(
91
+ dispatcherSource,
92
+ /Unknown: \/gsd/,
93
+ "single-token unknown commands should still report the normal unknown-command warning",
94
+ );
95
+ });
96
+
59
97
  test("guided-flow needs-discussion skip branch opens the project DB before reserving a new milestone", () => {
60
98
  const guidedFlowSource = readFileSync(join(import.meta.dirname, "..", "guided-flow.ts"), "utf-8");
61
99
  const laterDbOpenIdx = guidedFlowSource.indexOf("// Ensure DB is open before querying slices (#2560).");
@@ -3,6 +3,8 @@ import assert from "node:assert/strict";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
 
6
+ import { _withDetachedAutoKeepaliveForTest } from "../auto.ts";
7
+
6
8
  const gsdDir = resolve(import.meta.dirname, "..");
7
9
 
8
10
  function readGsdFile(relativePath: string): string {
@@ -102,8 +104,8 @@ test("startAutoDetached reports failures asynchronously (#3733)", () => {
102
104
  "auto.ts should export startAutoDetached",
103
105
  );
104
106
  assert.ok(
105
- autoSrc.includes("void startAuto(ctx, pi, base, verboseMode, options).catch"),
106
- "startAutoDetached should launch startAuto without awaiting it",
107
+ autoSrc.includes("void withDetachedAutoKeepalive(startAuto(ctx, pi, base, verboseMode, options)).catch"),
108
+ "startAutoDetached should launch startAuto without awaiting it and keep the process alive",
107
109
  );
108
110
  assert.ok(
109
111
  autoSrc.includes("ctx.ui.notify(`Auto-start failed: ${message}`, \"error\")"),
@@ -111,6 +113,48 @@ test("startAutoDetached reports failures asynchronously (#3733)", () => {
111
113
  );
112
114
  });
113
115
 
116
+ test("detached auto-start keeps a ref'ed handle until the run settles", async () => {
117
+ const originalSetInterval = globalThis.setInterval;
118
+ const originalClearInterval = globalThis.clearInterval;
119
+ let intervalCreated = false;
120
+ let intervalCleared = false;
121
+ let createdHandle: NodeJS.Timeout | undefined;
122
+ let resolveRun!: () => void;
123
+ const run = new Promise<void>((resolve) => {
124
+ resolveRun = resolve;
125
+ });
126
+
127
+ globalThis.setInterval = ((handler: TimerHandler, timeout?: number, ...args: unknown[]) => {
128
+ intervalCreated = true;
129
+ assert.equal(timeout, 30_000);
130
+ void handler;
131
+ void args;
132
+ createdHandle = originalSetInterval(() => {}, 1_000_000);
133
+ assert.equal(createdHandle.hasRef(), true, "detached auto keepalive must be ref'ed");
134
+ return createdHandle;
135
+ }) as unknown as typeof setInterval;
136
+
137
+ globalThis.clearInterval = ((handle?: NodeJS.Timeout | number | string) => {
138
+ if (handle === createdHandle) intervalCleared = true;
139
+ return originalClearInterval(handle);
140
+ }) as unknown as typeof clearInterval;
141
+
142
+ try {
143
+ const heldRun = _withDetachedAutoKeepaliveForTest(run);
144
+ assert.equal(intervalCreated, true, "keepalive interval should start immediately");
145
+ assert.equal(intervalCleared, false, "keepalive should remain active while auto-mode is running");
146
+
147
+ resolveRun();
148
+ await heldRun;
149
+
150
+ assert.equal(intervalCleared, true, "keepalive interval should clear when auto-mode settles");
151
+ } finally {
152
+ if (createdHandle) originalClearInterval(createdHandle);
153
+ globalThis.setInterval = originalSetInterval;
154
+ globalThis.clearInterval = originalClearInterval;
155
+ }
156
+ });
157
+
114
158
  test("detached auto-start preserves milestone lock across pause/stop cleanup (#3733)", () => {
115
159
  const autoSrc = readGsdFile("auto.ts");
116
160
  const sessionSrc = readGsdFile("auto/session.ts");
@@ -5,6 +5,7 @@ import {
5
5
  clearUnitRuntimeRecord,
6
6
  formatExecuteTaskRecoveryStatus,
7
7
  inspectExecuteTaskDurability,
8
+ isInFlightRuntimePhase,
8
9
  readUnitRuntimeRecord,
9
10
  writeUnitRuntimeRecord,
10
11
  } from "../unit-runtime.ts";
@@ -22,6 +23,12 @@ writeFileSync(
22
23
  "utf-8",
23
24
  );
24
25
 
26
+ console.log("\n=== in-flight runtime phases ===");
27
+ {
28
+ assert.equal(isInFlightRuntimePhase("crashed"), true, "crashed records remain recoverable");
29
+ assert.equal(isInFlightRuntimePhase("finalized"), false, "finalized records are terminal");
30
+ }
31
+
25
32
  console.log("\n=== runtime record write/read/update ===");
26
33
  {
27
34
  const first = writeUnitRuntimeRecord(base, "execute-task", "M100/S02/T09", 1000, { phase: "dispatched" });
@@ -9,12 +9,12 @@
9
9
 
10
10
  import { describe, test, beforeEach, afterEach } from "node:test";
11
11
  import assert from "node:assert/strict";
12
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readdirSync } from "node:fs";
12
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { tmpdir } from "node:os";
15
15
  import { execSync } from "node:child_process";
16
16
 
17
- import { PROJECT_FILES } from "../detection.js";
17
+ import { PROJECT_FILES, classifyProject } from "../detection.js";
18
18
 
19
19
  // ─── Helpers ─────────────────────────────────────────────────────────────────
20
20
 
@@ -30,6 +30,14 @@ function createGitRepo(): string {
30
30
  return dir;
31
31
  }
32
32
 
33
+ function createEmptyGitRepo(): string {
34
+ const dir = mkdtempSync(join(tmpdir(), "wt-dispatch-test-empty-"));
35
+ execSync("git init", { cwd: dir, stdio: "ignore" });
36
+ execSync("git config user.email test@test.com", { cwd: dir, stdio: "ignore" });
37
+ execSync("git config user.name Test", { cwd: dir, stdio: "ignore" });
38
+ return dir;
39
+ }
40
+
33
41
  /**
34
42
  * Simulate the health check logic from auto/phases.ts.
35
43
  *
@@ -64,8 +72,6 @@ function hasXcodeBundle(basePath: string): boolean {
64
72
  } catch { return false; }
65
73
  }
66
74
 
67
- import { existsSync } from "node:fs";
68
-
69
75
  // ─── Tests ───────────────────────────────────────────────────────────────────
70
76
 
71
77
  test("PROJECT_FILES is exported and contains expected multi-ecosystem entries", () => {
@@ -80,6 +86,21 @@ test("PROJECT_FILES is exported and contains expected multi-ecosystem entries",
80
86
  assert.ok(PROJECT_FILES.includes("Package.swift"), "includes Swift marker");
81
87
  });
82
88
 
89
+ test("runUnitPhase fails closed when classification returns invalid-repo", () => {
90
+ const source = readFileSync(join(process.cwd(), "src/resources/extensions/gsd/auto/phases.ts"), "utf-8");
91
+ const invalidRepoBranch = source.slice(
92
+ source.indexOf('projectClassification.kind === "invalid-repo"'),
93
+ source.indexOf('projectClassification.kind === "greenfield"'),
94
+ );
95
+
96
+ assert.match(invalidRepoBranch, /projectClassification\.reason === "missing \.git" && hasGit/);
97
+ assert.match(invalidRepoBranch, /project classification could not confirm \.git/);
98
+ assert.match(invalidRepoBranch, /ctx\.ui\.notify\(msg,\s*"error"\)/);
99
+ assert.match(invalidRepoBranch, /await deps\.stopAuto\(ctx,\s*pi,\s*msg\)/);
100
+ assert.match(invalidRepoBranch, /return \{ action: "break", reason: "worktree-invalid" \}/);
101
+ assert.match(invalidRepoBranch, /classified as invalid-repo/);
102
+ });
103
+
83
104
  describe("health check with git repo", () => {
84
105
  let dir: string;
85
106
  beforeEach(() => { dir = createGitRepo(); });
@@ -132,8 +153,18 @@ describe("health check with git repo", () => {
132
153
  });
133
154
 
134
155
  test("health check passes for empty git repo (greenfield project)", () => {
135
- assert.ok(wouldPassHealthCheck(dir, existsSync), "empty git repo should pass health check (greenfield)");
136
- assert.ok(!hasRecognizedProjectFiles(dir, existsSync), "empty git repo has no recognized project files");
156
+ const empty = createEmptyGitRepo();
157
+ try {
158
+ assert.ok(wouldPassHealthCheck(empty, existsSync), "empty git repo should pass health check (greenfield)");
159
+ assert.equal(classifyProject(empty).kind, "greenfield");
160
+ } finally {
161
+ rmSync(empty, { recursive: true, force: true });
162
+ }
163
+ });
164
+
165
+ test("health check classifies README-only repo as untyped existing, not greenfield", () => {
166
+ assert.ok(wouldPassHealthCheck(dir, existsSync), "README-only repo should pass health check");
167
+ assert.equal(classifyProject(dir).kind, "untyped-existing");
137
168
  });
138
169
  });
139
170
 
@@ -235,4 +235,11 @@ describe("removeWorktree — missing worktree", () => {
235
235
  "should not throw when worktree does not exist",
236
236
  );
237
237
  });
238
+
239
+ test("deleteBranch is quiet when the branch is already gone", () => {
240
+ assert.doesNotThrow(
241
+ () => removeWorktree(base, "nonexistent", { deleteBranch: true }),
242
+ "missing branch should be treated as already cleaned up",
243
+ );
244
+ });
238
245
  });
@@ -56,11 +56,11 @@ test("#2616: findNestedGitDirs ignores .git files (worktree pointers)", (t) => {
56
56
  );
57
57
  });
58
58
 
59
- test("#2616: findNestedGitDirs skips excluded directories (node_modules, .gsd, target)", (t) => {
59
+ test("#2616: findNestedGitDirs skips excluded directories (node_modules, .gsd, .bg-shell, target)", (t) => {
60
60
  const root = makeRoot(t);
61
61
 
62
62
  // All three of these contain a .git *directory*, but the scan must skip them.
63
- for (const excluded of ["node_modules", ".gsd", "target"]) {
63
+ for (const excluded of ["node_modules", ".gsd", ".bg-shell", "target"]) {
64
64
  const inside = join(root, excluded, "vendored-pkg");
65
65
  mkdirSync(join(inside, ".git"), { recursive: true });
66
66
  }
@@ -73,6 +73,13 @@ test("#2616: findNestedGitDirs skips excluded directories (node_modules, .gsd, t
73
73
  );
74
74
  });
75
75
 
76
+ test("#2616: findNestedGitDirs ignores normal missing child .git probes", (t) => {
77
+ const root = makeRoot(t);
78
+ mkdirSync(join(root, "plain-dir"), { recursive: true });
79
+
80
+ assert.deepEqual(findNestedGitDirs(root), []);
81
+ });
82
+
76
83
  test("#2616: findNestedGitDirs finds deeply nested repos", (t) => {
77
84
  const root = makeRoot(t);
78
85
  const deep = join(root, "a", "b", "c", "scaffolded");
@@ -18,6 +18,7 @@ import { renderAllProjections } from "../workflow-projections.js";
18
18
  import { writeManifest } from "../workflow-manifest.js";
19
19
  import { appendEvent } from "../workflow-events.js";
20
20
  import { logWarning } from "../workflow-logger.js";
21
+ import { validatePlanningPathScope } from "../planning-path-scope.js";
21
22
 
22
23
  export interface PlanSliceTaskInput {
23
24
  taskId: string;
@@ -141,6 +142,18 @@ export async function handlePlanSlice(
141
142
  return { error: `validation failed: ${(err as Error).message}` };
142
143
  }
143
144
 
145
+ const pathScopeError = validatePlanningPathScope(
146
+ basePath,
147
+ params.tasks.flatMap((task, index) => [
148
+ { field: `tasks[${index}].files`, values: task.files },
149
+ { field: `tasks[${index}].inputs`, values: task.inputs },
150
+ { field: `tasks[${index}].expectedOutput`, values: task.expectedOutput },
151
+ ]),
152
+ );
153
+ if (pathScopeError) {
154
+ return { error: `validation failed: ${pathScopeError}` };
155
+ }
156
+
144
157
  // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
145
158
  // Guards must be inside the transaction so the state they check cannot
146
159
  // change between the read and the write (#2723).
@@ -8,6 +8,7 @@ import { renderAllProjections } from "../workflow-projections.js";
8
8
  import { writeManifest } from "../workflow-manifest.js";
9
9
  import { appendEvent } from "../workflow-events.js";
10
10
  import { logWarning } from "../workflow-logger.js";
11
+ import { validatePlanningPathScope } from "../planning-path-scope.js";
11
12
 
12
13
  export interface PlanTaskParams {
13
14
  milestoneId: string;
@@ -66,6 +67,15 @@ export async function handlePlanTask(
66
67
  return { error: `validation failed: ${(err as Error).message}` };
67
68
  }
68
69
 
70
+ const pathScopeError = validatePlanningPathScope(basePath, [
71
+ { field: "files", values: params.files },
72
+ { field: "inputs", values: params.inputs },
73
+ { field: "expectedOutput", values: params.expectedOutput },
74
+ ]);
75
+ if (pathScopeError) {
76
+ return { error: `validation failed: ${pathScopeError}` };
77
+ }
78
+
69
79
  // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
70
80
  // Guards must be inside the transaction so the state they check cannot
71
81
  // change between the read and the write (#2723).
@@ -67,11 +67,25 @@ export type UnitRuntimePhase =
67
67
  | "wrapup-warning-sent"
68
68
  | "timeout"
69
69
  | "finalize-timeout"
70
+ | "crashed"
70
71
  | "recovered"
71
72
  | "finalized"
72
73
  | "paused"
73
74
  | "skipped";
74
75
 
76
+ export const IN_FLIGHT_RUNTIME_PHASES: ReadonlySet<UnitRuntimePhase> = new Set([
77
+ "dispatched",
78
+ "wrapup-warning-sent",
79
+ "timeout",
80
+ "finalize-timeout",
81
+ "crashed",
82
+ "paused",
83
+ ]);
84
+
85
+ export function isInFlightRuntimePhase(phase: UnitRuntimePhase): boolean {
86
+ return IN_FLIGHT_RUNTIME_PHASES.has(phase);
87
+ }
88
+
75
89
  export interface ExecuteTaskRecoveryStatus {
76
90
  planPath: string;
77
91
  summaryPath: string;
@@ -70,6 +70,15 @@ export interface WorktreeDiffSummary {
70
70
  removed: string[];
71
71
  }
72
72
 
73
+ function deleteBranchIfPresent(basePath: string, branch: string, warningPrefix: string): void {
74
+ try {
75
+ if (!nativeBranchExists(basePath, branch)) return;
76
+ nativeBranchDelete(basePath, branch, true);
77
+ } catch (e) {
78
+ logWarning("worktree", `${warningPrefix}: ${(e as Error).message}`);
79
+ }
80
+ }
81
+
73
82
  // ─── Path Helpers ──────────────────────────────────────────────────────────
74
83
 
75
84
  function normalizePathForComparison(path: string): string {
@@ -408,7 +417,7 @@ export function listWorktrees(basePath: string): WorktreeInfo[] {
408
417
 
409
418
  /** Directories to skip when scanning for nested .git dirs. */
410
419
  const NESTED_GIT_SKIP_DIRS = new Set([
411
- ".git", ".gsd", "node_modules", ".next", ".nuxt", "dist", "build",
420
+ ".git", ".gsd", ".bg-shell", "node_modules", ".next", ".nuxt", "dist", "build",
412
421
  "__pycache__", ".tox", ".venv", "venv", "target", "vendor",
413
422
  ]);
414
423
 
@@ -462,7 +471,9 @@ export function findNestedGitDirs(rootPath: string): string[] {
462
471
  continue;
463
472
  }
464
473
  } catch (e) {
465
- logWarning("worktree", `existsSync/.git check failed for ${fullPath}: ${(e as Error).message}`);
474
+ if ((e as NodeJS.ErrnoException).code !== "ENOENT") {
475
+ logWarning("worktree", `existsSync/.git check failed for ${fullPath}: ${(e as Error).message}`);
476
+ }
466
477
  }
467
478
 
468
479
  walk(fullPath, depth + 1);
@@ -535,7 +546,7 @@ export function removeWorktree(
535
546
  if (!existsSync(wtPath)) {
536
547
  nativeWorktreePrune(basePath);
537
548
  if (deleteBranch) {
538
- try { nativeBranchDelete(basePath, branch, true); } catch (e) { logWarning("worktree", `nativeBranchDelete failed: ${(e as Error).message}`); }
549
+ deleteBranchIfPresent(basePath, branch, "nativeBranchDelete failed");
539
550
  }
540
551
  return;
541
552
  }
@@ -670,7 +681,7 @@ export function removeWorktree(
670
681
  nativeWorktreePrune(basePath);
671
682
 
672
683
  if (deleteBranch) {
673
- try { nativeBranchDelete(basePath, branch, true); } catch (e) { logWarning("worktree", `final branch delete failed: ${(e as Error).message}`); }
684
+ deleteBranchIfPresent(basePath, branch, "final branch delete failed");
674
685
  }
675
686
  }
676
687