gsd-pi 2.82.0-dev.9d5798940 → 2.82.0-dev.dfbc5f58f

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 (185) hide show
  1. package/README.md +2 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/GSD-WORKFLOW.md +7 -0
  4. package/dist/resources/extensions/gsd/auto/infra-errors.js +9 -3
  5. package/dist/resources/extensions/gsd/auto/loop.js +5 -5
  6. package/dist/resources/extensions/gsd/auto/orchestrator.js +11 -0
  7. package/dist/resources/extensions/gsd/auto/phases.js +8 -1
  8. package/dist/resources/extensions/gsd/auto/workflow-memory-pressure.js +12 -0
  9. package/dist/resources/extensions/gsd/auto-model-selection.js +2 -0
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
  11. package/dist/resources/extensions/gsd/auto-start.js +78 -9
  12. package/dist/resources/extensions/gsd/auto-worktree.js +15 -1
  13. package/dist/resources/extensions/gsd/auto.js +30 -3
  14. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +9 -8
  15. package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +5 -2
  16. package/dist/resources/extensions/gsd/crash-recovery.js +31 -5
  17. package/dist/resources/extensions/gsd/db/unit-dispatches.js +1 -0
  18. package/dist/resources/extensions/gsd/dispatch-guard.js +2 -2
  19. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +28 -11
  20. package/dist/resources/extensions/gsd/doctor.js +2 -28
  21. package/dist/resources/extensions/gsd/git-service.js +39 -1
  22. package/dist/resources/extensions/gsd/gsd-db.js +1 -0
  23. package/dist/resources/extensions/gsd/guided-flow.js +6 -0
  24. package/dist/resources/extensions/gsd/migrate/parsers.js +10 -0
  25. package/dist/resources/extensions/gsd/native-git-bridge.js +40 -9
  26. package/dist/resources/extensions/gsd/post-execution-checks.js +73 -2
  27. package/dist/resources/extensions/gsd/pre-execution-checks.js +28 -1
  28. package/dist/resources/extensions/gsd/prompt-loader.js +1 -1
  29. package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  30. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  31. package/dist/resources/extensions/gsd/status-guards.js +4 -0
  32. package/dist/resources/extensions/gsd/templates/plan.md +8 -5
  33. package/dist/resources/extensions/gsd/templates/task-plan.md +4 -2
  34. package/dist/resources/extensions/gsd/tools/complete-milestone.js +6 -8
  35. package/dist/resources/extensions/gsd/tools/complete-slice.js +6 -8
  36. package/dist/resources/extensions/gsd/tools/plan-milestone.js +7 -1
  37. package/dist/resources/extensions/gsd/tools/plan-slice.js +88 -14
  38. package/dist/resources/extensions/gsd/validation.js +23 -1
  39. package/dist/resources/extensions/gsd/verification-gate.js +68 -7
  40. package/dist/resources/extensions/gsd/workflow-projections.js +6 -8
  41. package/dist/resources/extensions/gsd/worktree-lifecycle.js +5 -1
  42. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  43. package/dist/web/standalone/.next/BUILD_ID +1 -1
  44. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  45. package/dist/web/standalone/.next/build-manifest.json +2 -2
  46. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  47. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/index.html +1 -1
  65. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  72. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/package.json +2 -2
  77. package/packages/mcp-server/src/workflow-tools.test.ts +1 -1
  78. package/packages/native/tsconfig.json +2 -1
  79. package/packages/native/tsconfig.tsbuildinfo +1 -1
  80. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  81. package/packages/pi-ai/dist/providers/openai-codex-responses.js +82 -1
  82. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  83. package/packages/pi-ai/dist/providers/openai-codex-responses.test.d.ts +2 -0
  84. package/packages/pi-ai/dist/providers/openai-codex-responses.test.d.ts.map +1 -0
  85. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js +52 -0
  86. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js.map +1 -0
  87. package/packages/pi-ai/dist/providers/simple-options.d.ts +2 -4
  88. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  89. package/packages/pi-ai/dist/providers/simple-options.js +5 -6
  90. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  91. package/packages/pi-ai/dist/providers/simple-options.test.d.ts +2 -0
  92. package/packages/pi-ai/dist/providers/simple-options.test.d.ts.map +1 -0
  93. package/packages/pi-ai/dist/providers/simple-options.test.js +50 -0
  94. package/packages/pi-ai/dist/providers/simple-options.test.js.map +1 -0
  95. package/packages/pi-ai/src/providers/openai-codex-responses.test.ts +63 -0
  96. package/packages/pi-ai/src/providers/openai-codex-responses.ts +91 -1
  97. package/packages/pi-ai/src/providers/simple-options.test.ts +60 -0
  98. package/packages/pi-ai/src/providers/simple-options.ts +5 -6
  99. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  100. package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.d.ts +2 -0
  101. package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.d.ts.map +1 -0
  102. package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.js +66 -0
  103. package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.js.map +1 -0
  104. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  105. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  106. package/packages/pi-coding-agent/src/core/agent-session-thinking-level.test.ts +79 -0
  107. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  108. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  109. package/src/resources/GSD-WORKFLOW.md +7 -0
  110. package/src/resources/extensions/gsd/auto/contracts.ts +14 -6
  111. package/src/resources/extensions/gsd/auto/infra-errors.ts +9 -3
  112. package/src/resources/extensions/gsd/auto/loop.ts +8 -5
  113. package/src/resources/extensions/gsd/auto/orchestrator.ts +11 -0
  114. package/src/resources/extensions/gsd/auto/phases.ts +7 -1
  115. package/src/resources/extensions/gsd/auto/workflow-memory-pressure.ts +13 -0
  116. package/src/resources/extensions/gsd/auto-model-selection.ts +2 -1
  117. package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
  118. package/src/resources/extensions/gsd/auto-start.ts +85 -6
  119. package/src/resources/extensions/gsd/auto-worktree.ts +15 -1
  120. package/src/resources/extensions/gsd/auto.ts +32 -3
  121. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +9 -8
  122. package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +3 -1
  123. package/src/resources/extensions/gsd/crash-recovery.ts +30 -4
  124. package/src/resources/extensions/gsd/db/unit-dispatches.ts +1 -0
  125. package/src/resources/extensions/gsd/dispatch-guard.ts +2 -2
  126. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +25 -13
  127. package/src/resources/extensions/gsd/doctor.ts +2 -27
  128. package/src/resources/extensions/gsd/git-service.ts +45 -1
  129. package/src/resources/extensions/gsd/gsd-db.ts +3 -0
  130. package/src/resources/extensions/gsd/guided-flow.ts +6 -0
  131. package/src/resources/extensions/gsd/migrate/parsers.ts +11 -0
  132. package/src/resources/extensions/gsd/native-git-bridge.ts +46 -9
  133. package/src/resources/extensions/gsd/post-execution-checks.ts +87 -2
  134. package/src/resources/extensions/gsd/pre-execution-checks.ts +32 -1
  135. package/src/resources/extensions/gsd/prompt-loader.ts +1 -1
  136. package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  137. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  138. package/src/resources/extensions/gsd/status-guards.ts +5 -0
  139. package/src/resources/extensions/gsd/templates/plan.md +8 -5
  140. package/src/resources/extensions/gsd/templates/task-plan.md +4 -2
  141. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +54 -0
  142. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +80 -1
  143. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +6 -6
  144. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +1 -0
  145. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +4 -1
  146. package/src/resources/extensions/gsd/tests/complete-task.test.ts +3 -1
  147. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -2
  148. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +2 -0
  149. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +27 -0
  150. package/src/resources/extensions/gsd/tests/guided-flow.test.ts +21 -0
  151. package/src/resources/extensions/gsd/tests/hook-model-resolution.test.ts +5 -0
  152. package/src/resources/extensions/gsd/tests/infra-error.test.ts +2 -2
  153. package/src/resources/extensions/gsd/tests/infra-errors-cooldown.test.ts +9 -0
  154. package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +20 -0
  155. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +103 -1
  156. package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +6 -1
  157. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +24 -1
  158. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +63 -2
  159. package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +121 -1
  160. package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +26 -0
  161. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +200 -1
  162. package/src/resources/extensions/gsd/tests/plan-task.test.ts +17 -0
  163. package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +86 -0
  164. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +53 -0
  165. package/src/resources/extensions/gsd/tests/prompt-loader.test.ts +23 -0
  166. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +31 -1
  167. package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +26 -2
  168. package/src/resources/extensions/gsd/tests/summary-render-parity.test.ts +7 -3
  169. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +110 -1
  170. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +1 -1
  171. package/src/resources/extensions/gsd/tests/workflow-memory-pressure.test.ts +21 -1
  172. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +1 -1
  173. package/src/resources/extensions/gsd/tests/worktree-git-pathspec.test.ts +39 -0
  174. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +7 -0
  175. package/src/resources/extensions/gsd/tools/complete-milestone.ts +8 -10
  176. package/src/resources/extensions/gsd/tools/complete-slice.ts +6 -8
  177. package/src/resources/extensions/gsd/tools/plan-milestone.ts +5 -1
  178. package/src/resources/extensions/gsd/tools/plan-slice.ts +96 -12
  179. package/src/resources/extensions/gsd/types.ts +1 -1
  180. package/src/resources/extensions/gsd/validation.ts +23 -1
  181. package/src/resources/extensions/gsd/verification-gate.ts +78 -6
  182. package/src/resources/extensions/gsd/workflow-projections.ts +6 -8
  183. package/src/resources/extensions/gsd/worktree-lifecycle.ts +7 -1
  184. /package/dist/web/standalone/.next/static/{BdZQhe8yKl6bdKLiXVEzh → q0WYuDVbHeFFYbdd-fei2}/_buildManifest.js +0 -0
  185. /package/dist/web/standalone/.next/static/{BdZQhe8yKl6bdKLiXVEzh → q0WYuDVbHeFFYbdd-fei2}/_ssgManifest.js +0 -0
@@ -13,17 +13,19 @@
13
13
  import { describe, test, beforeEach, afterEach } from "node:test";
14
14
  import assert from "node:assert/strict";
15
15
  import { chmodSync, existsSync, mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs";
16
- import { join } from "node:path";
16
+ import { delimiter, join } from "node:path";
17
17
  import { tmpdir } from "node:os";
18
18
  import { execFileSync } from "node:child_process";
19
19
  import {
20
20
  assertWorktreeMaterialized,
21
21
  nativeBranchDelete,
22
22
  nativeCommit,
23
+ nativeGetCurrentBranch,
23
24
  nativeIsRepo,
24
25
  nativeResetHard,
25
26
  nativeWorktreeAdd,
26
27
  } from "../native-git-bridge.js";
28
+ import { GIT_NO_PROMPT_ENV } from "../git-constants.js";
27
29
 
28
30
  // Note: prior static-analysis tests that scanned native-git-bridge.ts for
29
31
  // the raw shell-spawn pattern were removed under #4827 — the integration
@@ -78,6 +80,55 @@ describe("native-git-bridge #4180: fallback runtime behaviour", () => {
78
80
  assert.equal(subject, "test: regression commit #4180");
79
81
  });
80
82
 
83
+ test("nativeCommit retries once after transient ENOBUFS from git", (t) => {
84
+ const bin = mkdtempSync(join(tmpdir(), "ngb-enobufs-bin-"));
85
+ t.after(() => rmSync(bin, { recursive: true, force: true }));
86
+
87
+ const realGit = execFileSync("git", ["--exec-path"], { encoding: "utf-8" }).trim();
88
+ const attempts = join(bin, "attempts.txt");
89
+ const fakeGit = join(bin, "fake-git.cjs");
90
+ writeFileSync(fakeGit, `
91
+ const { appendFileSync, readFileSync } = require("node:fs");
92
+ const { spawnSync } = require("node:child_process");
93
+ const attempts = ${JSON.stringify(attempts)};
94
+ const realGit = ${JSON.stringify(join(realGit, process.platform === "win32" ? "git.exe" : "git"))};
95
+ appendFileSync(attempts, "1");
96
+ if (process.argv[2] === "commit" && readFileSync(attempts, "utf-8").length === 1) {
97
+ console.error("spawnSync git ENOBUFS");
98
+ process.exit(1);
99
+ }
100
+ const result = spawnSync(realGit, process.argv.slice(2), { stdio: "inherit" });
101
+ process.exit(result.status ?? 1);
102
+ `, "utf-8");
103
+
104
+ if (process.platform === "win32") {
105
+ writeFileSync(join(bin, "git.cmd"), `@echo off\r\nnode "${fakeGit}" %*\r\n`, "utf-8");
106
+ } else {
107
+ const shim = join(bin, "git");
108
+ writeFileSync(shim, `#!/bin/sh\nexec node "${fakeGit}" "$@"\n`, "utf-8");
109
+ chmodSync(shim, 0o755);
110
+ }
111
+
112
+ writeFileSync(join(repo, "file.txt"), "retry commit\n");
113
+ git(["add", "."], repo);
114
+
115
+ const originalPath = process.env.PATH ?? "";
116
+ const gitEnv = GIT_NO_PROMPT_ENV as NodeJS.ProcessEnv;
117
+ const originalGitEnvPath = gitEnv.PATH;
118
+ try {
119
+ process.env.PATH = `${bin}${delimiter}${originalPath}`;
120
+ gitEnv.PATH = process.env.PATH;
121
+ const result = nativeCommit(repo, "test: retry ENOBUFS commit");
122
+ assert.ok(result !== null, "commit should succeed after retry");
123
+ } finally {
124
+ process.env.PATH = originalPath;
125
+ gitEnv.PATH = originalGitEnvPath;
126
+ }
127
+
128
+ assert.equal(readFileSync(attempts, "utf-8").length, 2);
129
+ assert.equal(git(["log", "-1", "--format=%s"], repo), "test: retry ENOBUFS commit");
130
+ });
131
+
81
132
  test("nativeCommit runs commit hooks", () => {
82
133
  const hookPath = join(repo, ".git", "hooks", "commit-msg");
83
134
  const marker = join(repo, "hook-ran.txt");
@@ -119,7 +170,17 @@ describe("native-git-bridge #4180: fallback runtime behaviour", () => {
119
170
  test("nativeBranchDelete throws when git cannot delete the branch", () => {
120
171
  assert.throws(
121
172
  () => nativeBranchDelete(repo, "does-not-exist"),
122
- /GSD_GIT_ERROR|git branch -D does-not-exist failed/,
173
+ /git branch -D does-not-exist failed[\s\S]*does-not-exist/,
174
+ );
175
+ });
176
+
177
+ test("nativeGetCurrentBranch preserves git stderr in fallback errors", (t) => {
178
+ const dir = mkdtempSync(join(tmpdir(), "ngb-stderr-notrepo-"));
179
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
180
+
181
+ assert.throws(
182
+ () => nativeGetCurrentBranch(dir),
183
+ /git branch --show-current failed[\s\S]*(not a git repository|not a git repo|fatal:)/,
123
184
  );
124
185
  });
125
186
 
@@ -243,7 +243,7 @@ describe("auditOrphanedMilestoneBranches", () => {
243
243
  );
244
244
  });
245
245
 
246
- test("handles milestone in DB but no branch (no-op)", () => {
246
+ test("milestone in DB, no branch, no worktree dir → no-op", () => {
247
247
  insertMilestone({ id: "M001", title: "Test", status: "complete" });
248
248
 
249
249
  const result = auditOrphanedMilestoneBranches(dir, "worktree");
@@ -251,4 +251,124 @@ describe("auditOrphanedMilestoneBranches", () => {
251
251
  assert.deepStrictEqual(result.recovered, []);
252
252
  assert.deepStrictEqual(result.warnings, []);
253
253
  });
254
+
255
+ test("#5879 — cleans orphaned worktree dir for complete milestone whose branch was already deleted", () => {
256
+ // Reproduces the postflight-stash-restore-failed scenario:
257
+ // 1. An earlier audit deleted milestone/M001 (merged + complete).
258
+ // 2. The worktree dir cleanup failed silently (logWarning only).
259
+ // 3. On the next startup the branch is gone, so the existing branch-keyed
260
+ // loop is invisible to the orphan dir. Without the second pass, the
261
+ // directory lives forever.
262
+ insertMilestone({ id: "M001", title: "Test", status: "complete" });
263
+
264
+ const wtDir = join(dir, ".gsd", "worktrees", "M001");
265
+ mkdirSync(wtDir, { recursive: true });
266
+ writeFileSync(join(wtDir, "leftover.txt"), "stranded from a prior session\n");
267
+
268
+ // No milestone/M001 branch — already deleted on a previous run.
269
+ const branches = run("git branch --list milestone/M001", dir);
270
+ assert.equal(branches, "", "test fixture: branch should not exist");
271
+
272
+ const result = auditOrphanedMilestoneBranches(dir, "worktree");
273
+
274
+ assert.ok(
275
+ result.recovered.some((r) => r.includes("M001") && r.includes("branch already deleted")),
276
+ `should report branch-less orphan cleanup; got: ${JSON.stringify(result.recovered)}`,
277
+ );
278
+ assert.ok(!existsSync(wtDir), "branch-less orphan worktree dir should be removed");
279
+ });
280
+
281
+ test("#5879 — branch list failure still cleans complete orphan only after branch absence is verified", () => {
282
+ insertMilestone({ id: "M001", title: "Test", status: "complete" });
283
+
284
+ const wtDir = join(dir, ".gsd", "worktrees", "M001");
285
+ mkdirSync(wtDir, { recursive: true });
286
+ writeFileSync(join(wtDir, "leftover.txt"), "stranded from a prior session\n");
287
+
288
+ const result = auditOrphanedMilestoneBranches(dir, "worktree", {
289
+ branchList: () => {
290
+ throw new Error("branch list failed");
291
+ },
292
+ branchExists: (_basePath, branch) => {
293
+ assert.equal(branch, "milestone/M001");
294
+ return false;
295
+ },
296
+ });
297
+
298
+ assert.ok(
299
+ result.recovered.some((r) => r.includes("M001") && r.includes("branch already deleted")),
300
+ `should report verified branch-less orphan cleanup; got: ${JSON.stringify(result.recovered)}`,
301
+ );
302
+ assert.ok(!existsSync(wtDir), "verified branch-less orphan worktree dir should be removed");
303
+ });
304
+
305
+ test("#5879 — branch list failure preserves complete worktree when branch still exists", () => {
306
+ insertMilestone({ id: "M001", title: "Test", status: "complete" });
307
+
308
+ const wtDir = join(dir, ".gsd", "worktrees", "M001");
309
+ mkdirSync(wtDir, { recursive: true });
310
+ writeFileSync(join(wtDir, "live-work.txt"), "do not delete\n");
311
+
312
+ const result = auditOrphanedMilestoneBranches(dir, "worktree", {
313
+ branchList: () => {
314
+ throw new Error("branch list failed");
315
+ },
316
+ branchExists: (_basePath, branch) => {
317
+ assert.equal(branch, "milestone/M001");
318
+ return true;
319
+ },
320
+ });
321
+
322
+ assert.deepStrictEqual(result.recovered, []);
323
+ assert.ok(existsSync(wtDir), "worktree dir must be preserved when branch existence is verified");
324
+ });
325
+
326
+ test("#5879 — skips branch-less orphan for milestone that is not complete", () => {
327
+ // Defensive: only `complete` milestones get the branch-less cleanup. An
328
+ // `active` milestone with no branch but a worktree dir is a different
329
+ // state (probably mid-recovery) and should not be silently wiped.
330
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
331
+
332
+ const wtDir = join(dir, ".gsd", "worktrees", "M001");
333
+ mkdirSync(wtDir, { recursive: true });
334
+ writeFileSync(join(wtDir, "live-work.txt"), "do not delete\n");
335
+
336
+ const result = auditOrphanedMilestoneBranches(dir, "worktree");
337
+
338
+ assert.deepStrictEqual(result.recovered, []);
339
+ assert.ok(existsSync(wtDir), "active milestone worktree dir must be preserved");
340
+ });
341
+
342
+ test("#5879 — branch list failure does not delete worktree for milestone that is not complete", () => {
343
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
344
+
345
+ const wtDir = join(dir, ".gsd", "worktrees", "M001");
346
+ mkdirSync(wtDir, { recursive: true });
347
+ writeFileSync(join(wtDir, "live-work.txt"), "do not delete\n");
348
+
349
+ const result = auditOrphanedMilestoneBranches(dir, "worktree", {
350
+ branchList: () => {
351
+ throw new Error("branch list failed");
352
+ },
353
+ branchExists: () => {
354
+ throw new Error("branchExists should not be called for active milestones");
355
+ },
356
+ });
357
+
358
+ assert.deepStrictEqual(result.recovered, []);
359
+ assert.ok(existsSync(wtDir), "active milestone worktree dir must be preserved");
360
+ });
361
+
362
+ test("#5879 — skips branch-less orphan in 'none' isolation mode", () => {
363
+ insertMilestone({ id: "M001", title: "Test", status: "complete" });
364
+
365
+ const wtDir = join(dir, ".gsd", "worktrees", "M001");
366
+ mkdirSync(wtDir, { recursive: true });
367
+ writeFileSync(join(wtDir, "leftover.txt"), "stranded\n");
368
+
369
+ const result = auditOrphanedMilestoneBranches(dir, "none");
370
+
371
+ assert.deepStrictEqual(result.recovered, []);
372
+ assert.ok(existsSync(wtDir), "'none' mode must not touch worktree dirs");
373
+ });
254
374
  });
@@ -116,6 +116,32 @@ test('handlePlanMilestone rejects invalid payloads', async () => {
116
116
  }
117
117
  });
118
118
 
119
+ test('handlePlanMilestone rejects delimiter characters in milestone and slice titles', async () => {
120
+ const base = makeTmpBase();
121
+ const dbPath = join(base, '.gsd', 'gsd.db');
122
+ openDatabase(dbPath);
123
+
124
+ try {
125
+ const milestoneResult = await handlePlanMilestone({ ...validParams(), title: 'Client/Server split' }, base);
126
+ assert.ok('error' in milestoneResult);
127
+ assert.match(milestoneResult.error, /validation failed: title is invalid: .*forward slash/);
128
+ assert.equal(getMilestone('M001'), null, 'invalid milestone title must not persist');
129
+
130
+ const sliceResult = await handlePlanMilestone({
131
+ ...validParams(),
132
+ slices: [
133
+ validParams().slices[0],
134
+ { ...validParams().slices[1], title: 'Client/Server migration' },
135
+ ],
136
+ }, base);
137
+ assert.ok('error' in sliceResult);
138
+ assert.match(sliceResult.error, /validation failed: slices\[1\]\.title is invalid: .*forward slash/);
139
+ assert.equal(getMilestoneSlices('M001').length, 0, 'invalid slice title must not persist partial roadmap state');
140
+ } finally {
141
+ cleanup(base);
142
+ }
143
+ });
144
+
119
145
  test('handlePlanMilestone surfaces render failures and does not clear parse-visible state on failure', async () => {
120
146
  const base = makeTmpBase();
121
147
  const dbPath = join(base, '.gsd', 'gsd.db');
@@ -6,7 +6,7 @@ import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync, writeFileSync
6
6
  import { join } from 'node:path';
7
7
  import { tmpdir } from 'node:os';
8
8
 
9
- import { openDatabase, closeDatabase, insertMilestone, insertSlice, getSlice, getSliceTasks, getTask } from '../gsd-db.ts';
9
+ import { openDatabase, closeDatabase, insertMilestone, insertSlice, getSlice, getSliceTasks, getTask, getGateResults, updateTaskStatus } from '../gsd-db.ts';
10
10
  import { handlePlanSlice } from '../tools/plan-slice.ts';
11
11
  import { parsePlan } from '../parsers-legacy.ts';
12
12
  import { parseTaskPlanFile } from '../files.ts';
@@ -15,6 +15,10 @@ import { deriveState, invalidateStateCache } from '../state.ts';
15
15
  function makeTmpBase(): string {
16
16
  const base = mkdtempSync(join(tmpdir(), 'gsd-plan-slice-'));
17
17
  mkdirSync(join(base, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'tasks'), { recursive: true });
18
+ mkdirSync(join(base, 'src', 'resources', 'extensions', 'gsd', 'tools'), { recursive: true });
19
+ writeFileSync(join(base, 'src', 'resources', 'extensions', 'gsd', 'tools', 'plan-milestone.ts'), '// fixture\n', 'utf-8');
20
+ writeFileSync(join(base, 'src', 'resources', 'extensions', 'gsd', 'tools', 'plan-task.ts'), '// fixture\n', 'utf-8');
21
+ writeFileSync(join(base, 'stale-input.py'), '# fixture\n', 'utf-8');
18
22
  return base;
19
23
  }
20
24
 
@@ -197,6 +201,28 @@ test('handlePlanSlice rejects invalid payloads', async () => {
197
201
  }
198
202
  });
199
203
 
204
+ test('handlePlanSlice explains string task IO fields must be arrays', async () => {
205
+ const base = makeTmpBase();
206
+ openDatabase(join(base, '.gsd', 'gsd.db'));
207
+
208
+ try {
209
+ seedParentSlice();
210
+ const result = await handlePlanSlice({
211
+ ...validParams(),
212
+ tasks: [
213
+ {
214
+ ...validParams().tasks[0],
215
+ inputs: 'src/index.ts' as unknown as string[],
216
+ },
217
+ ],
218
+ }, base);
219
+ assert.ok('error' in result);
220
+ assert.match(result.error, /validation failed: tasks\[0\]\.inputs must be an array of strings, not string/);
221
+ } finally {
222
+ cleanup(base);
223
+ }
224
+ });
225
+
200
226
  test('handlePlanSlice rejects absolute task IO paths outside the active worktree', async () => {
201
227
  const base = makeTmpBase();
202
228
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -223,6 +249,63 @@ test('handlePlanSlice rejects absolute task IO paths outside the active worktree
223
249
  }
224
250
  });
225
251
 
252
+ test('handlePlanSlice rejects missing task input paths before persisting tasks', async () => {
253
+ const base = makeTmpBase();
254
+ openDatabase(join(base, '.gsd', 'gsd.db'));
255
+
256
+ try {
257
+ seedParentSlice();
258
+ const result = await handlePlanSlice({
259
+ ...validParams(),
260
+ tasks: [
261
+ {
262
+ ...validParams().tasks[0],
263
+ inputs: ['fixtures/missing-source.json'],
264
+ },
265
+ ],
266
+ }, base);
267
+
268
+ assert.ok('error' in result);
269
+ assert.match(result.error, /pre-execution validation failed:/);
270
+ assert.match(result.error, /fixtures\/missing-source\.json/);
271
+ assert.equal(getSliceTasks('M001', 'S02').length, 0, 'invalid planning IO must not persist tasks');
272
+ } finally {
273
+ cleanup(base);
274
+ }
275
+ });
276
+
277
+ test('handlePlanSlice rejects task input paths created by later tasks before persisting tasks', async () => {
278
+ const base = makeTmpBase();
279
+ openDatabase(join(base, '.gsd', 'gsd.db'));
280
+
281
+ try {
282
+ seedParentSlice();
283
+ const params = validParams();
284
+ const result = await handlePlanSlice({
285
+ ...params,
286
+ tasks: [
287
+ {
288
+ ...params.tasks[0],
289
+ inputs: ['generated/report.json'],
290
+ expectedOutput: ['generated/summary.json'],
291
+ },
292
+ {
293
+ ...params.tasks[1],
294
+ inputs: [],
295
+ expectedOutput: ['generated/report.json'],
296
+ },
297
+ ],
298
+ }, base);
299
+
300
+ assert.ok('error' in result);
301
+ assert.match(result.error, /pre-execution validation failed:/);
302
+ assert.match(result.error, /sequence violation/);
303
+ assert.equal(getSliceTasks('M001', 'S02').length, 0, 'invalid task ordering must not persist tasks');
304
+ } finally {
305
+ cleanup(base);
306
+ }
307
+ });
308
+
226
309
  test('handlePlanSlice accepts absolute task IO paths inside the active worktree', async () => {
227
310
  const base = makeTmpBase();
228
311
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -329,3 +412,119 @@ test('handlePlanSlice reruns idempotently and refreshes parse-visible state', as
329
412
  cleanup(base);
330
413
  }
331
414
  });
415
+
416
+ test('handlePlanSlice removes omitted pending tasks when replanning a smaller task set', async () => {
417
+ const base = makeTmpBase();
418
+ openDatabase(join(base, '.gsd', 'gsd.db'));
419
+
420
+ try {
421
+ seedParentSlice();
422
+ const fourTaskPlan = {
423
+ ...validParams(),
424
+ tasks: [
425
+ ...validParams().tasks,
426
+ { ...validParams().tasks[0], taskId: 'T03', title: 'Third task' },
427
+ { ...validParams().tasks[0], taskId: 'T04', title: 'Stale task', inputs: ['stale-input.py'] },
428
+ ],
429
+ };
430
+
431
+ const first = await handlePlanSlice(fourTaskPlan, base);
432
+ assert.ok(!('error' in first), `unexpected error: ${'error' in first ? first.error : ''}`);
433
+ const staleTaskPlanPath = join(base, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'tasks', 'T04-PLAN.md');
434
+ assert.ok(existsSync(staleTaskPlanPath), 'initial plan should render T04');
435
+
436
+ const second = await handlePlanSlice({
437
+ ...validParams(),
438
+ tasks: fourTaskPlan.tasks.filter((task) => task.taskId !== 'T04'),
439
+ }, base);
440
+ assert.ok(!('error' in second), `unexpected error: ${'error' in second ? second.error : ''}`);
441
+
442
+ assert.deepEqual(getSliceTasks('M001', 'S02').map((task) => task.id), ['T01', 'T02', 'T03']);
443
+ assert.equal(getGateResults('M001', 'S02', 'task').some((gate) => gate.task_id === 'T04'), false);
444
+ assert.equal(existsSync(staleTaskPlanPath), false, 'omitted task plan artifact should be removed');
445
+ } finally {
446
+ cleanup(base);
447
+ }
448
+ });
449
+
450
+ test('handlePlanSlice rejects omitted completed tasks without changing slice or task state', async () => {
451
+ const base = makeTmpBase();
452
+ openDatabase(join(base, '.gsd', 'gsd.db'));
453
+
454
+ try {
455
+ seedParentSlice();
456
+ const fourTaskPlan = {
457
+ ...validParams(),
458
+ tasks: [
459
+ ...validParams().tasks,
460
+ { ...validParams().tasks[0], taskId: 'T03', title: 'Third task' },
461
+ { ...validParams().tasks[0], taskId: 'T04', title: 'Stale task', inputs: ['stale-input.py'] },
462
+ ],
463
+ };
464
+
465
+ const first = await handlePlanSlice(fourTaskPlan, base);
466
+ assert.ok(!('error' in first), `unexpected error: ${'error' in first ? first.error : ''}`);
467
+ const staleTaskPlanPath = join(base, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'tasks', 'T04-PLAN.md');
468
+ assert.ok(existsSync(staleTaskPlanPath), 'initial plan should render T04');
469
+
470
+ updateTaskStatus('M001', 'S02', 'T04', 'complete', '2026-05-12T00:00:00.000Z');
471
+ const tasksBefore = getSliceTasks('M001', 'S02');
472
+ const gatesBefore = getGateResults('M001', 'S02', 'task');
473
+
474
+ const second = await handlePlanSlice({
475
+ ...validParams(),
476
+ goal: 'Rejected replan should not persist.',
477
+ tasks: fourTaskPlan.tasks.filter((task) => task.taskId !== 'T04'),
478
+ }, base);
479
+ assert.deepEqual(second, { error: 'cannot remove completed task T04' });
480
+
481
+ assert.equal(getSlice('M001', 'S02')?.goal, 'Persist slice planning through the DB.');
482
+ assert.deepEqual(getSliceTasks('M001', 'S02'), tasksBefore);
483
+ assert.deepEqual(getGateResults('M001', 'S02', 'task'), gatesBefore);
484
+ assert.ok(existsSync(staleTaskPlanPath), 'completed task plan artifact should remain after rejected replan');
485
+ } finally {
486
+ cleanup(base);
487
+ }
488
+ });
489
+
490
+ test('regression: validateTasks surfaces clean per-field errors for non-array IO inputs', async () => {
491
+ // Regression for the bug fixed in PR #5872: an earlier refactor on main
492
+ // (0b0e1a901) re-added validateStringArray() calls inside validateTasks
493
+ // without re-adding its import. The catch around validateParams swallowed
494
+ // the ReferenceError into a generic "validation failed: validateStringArray
495
+ // is not defined" message, so silent runtime breakage was possible.
496
+ //
497
+ // Exercise every validateStringArray call site (files, inputs, expectedOutput)
498
+ // so a future missing-import would surface as a per-field assertion failure
499
+ // here, not a deep ReferenceError that's easy to mis-diagnose.
500
+ const base = makeTmpBase();
501
+ openDatabase(join(base, '.gsd', 'gsd.db'));
502
+
503
+ try {
504
+ seedParentSlice();
505
+
506
+ for (const field of ['files', 'inputs', 'expectedOutput'] as const) {
507
+ const result = await handlePlanSlice({
508
+ ...validParams(),
509
+ tasks: [{
510
+ ...validParams().tasks[0],
511
+ [field]: 'not-an-array' as unknown as string[],
512
+ }],
513
+ }, base);
514
+ assert.ok('error' in result, `${field}: expected validation error, got success`);
515
+ assert.match(
516
+ result.error,
517
+ new RegExp(`tasks\\[0\\]\\.${field} must be an array`),
518
+ `${field}: expected per-field validation message, got: ${result.error}`,
519
+ );
520
+ assert.doesNotMatch(
521
+ result.error,
522
+ /is not defined/,
523
+ `${field}: validation surfaced ReferenceError — likely a missing import in plan-slice.ts`,
524
+ );
525
+ assert.equal(getSliceTasks('M001', 'S02').length, 0, `${field}: invalid input must not persist`);
526
+ }
527
+ } finally {
528
+ cleanup(base);
529
+ }
530
+ });
@@ -79,6 +79,23 @@ test('handlePlanTask rejects invalid payloads', async () => {
79
79
  }
80
80
  });
81
81
 
82
+ test('handlePlanTask explains string IO fields must be arrays', async () => {
83
+ const base = makeTmpBase();
84
+ openDatabase(join(base, '.gsd', 'gsd.db'));
85
+
86
+ try {
87
+ seedParent();
88
+ const result = await handlePlanTask({
89
+ ...validParams(),
90
+ expectedOutput: 'src/output.ts' as unknown as string[],
91
+ }, base);
92
+ assert.ok('error' in result);
93
+ assert.match(result.error, /validation failed: expectedOutput must be an array of strings, not string/);
94
+ } finally {
95
+ cleanup(base);
96
+ }
97
+ });
98
+
82
99
  test('handlePlanTask rejects absolute task IO paths outside the active worktree', async () => {
83
100
  const base = makeTmpBase();
84
101
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -127,6 +127,61 @@ import { b } from './b';
127
127
  assert.equal(imports.length, 2);
128
128
  });
129
129
 
130
+ test("ignores import-looking string literals in test fixtures", () => {
131
+ const source = `
132
+ const rewritten = source.replace(
133
+ 'import { normalizeZagrebBusinessDeadline } from "./cutoff";',
134
+ 'const helper = true;'
135
+ );
136
+
137
+ import realThing from "./real-thing";
138
+ `;
139
+ const imports = extractRelativeImports(source);
140
+ assert.deepEqual(imports, [
141
+ { importPath: "./real-thing", lineNum: 7 },
142
+ ]);
143
+ });
144
+
145
+ test("ignores import-looking lines inside template literals", () => {
146
+ const source = [
147
+ "const fixture = `",
148
+ "import missingThing from './missing-thing';",
149
+ "`;",
150
+ "",
151
+ "import realThing from './real-thing';",
152
+ ].join("\n");
153
+ const imports = extractRelativeImports(source);
154
+ assert.deepEqual(imports, [
155
+ { importPath: "./real-thing", lineNum: 5 },
156
+ ]);
157
+ });
158
+
159
+ test("ignores require() inside string literals", () => {
160
+ const source = [
161
+ 'const fixture = "const x = require(\'./missing\');";',
162
+ "const otherFixture = 'const y = require(\"./also-missing\");';",
163
+ "const real = require('./real');",
164
+ ].join("\n");
165
+ const imports = extractRelativeImports(source);
166
+ assert.deepEqual(imports, [
167
+ { importPath: "./real", lineNum: 3 },
168
+ ]);
169
+ });
170
+
171
+ test("ignores require() inside template literals", () => {
172
+ const source = [
173
+ "const fixture = `",
174
+ "const x = require('./missing');",
175
+ "`;",
176
+ "",
177
+ "const real = require('./real');",
178
+ ].join("\n");
179
+ const imports = extractRelativeImports(source);
180
+ assert.deepEqual(imports, [
181
+ { importPath: "./real", lineNum: 5 },
182
+ ]);
183
+ });
184
+
130
185
  test("handles empty source", () => {
131
186
  const imports = extractRelativeImports("");
132
187
  assert.deepEqual(imports, []);
@@ -810,6 +865,37 @@ describe("runPostExecutionChecks", () => {
810
865
  }
811
866
  });
812
867
 
868
+ test("does not fail on import-looking strings in task key files", () => {
869
+ tempDir = join(tmpdir(), `post-exec-test-${Date.now()}`);
870
+ mkdirSync(tempDir, { recursive: true });
871
+ mkdirSync(join(tempDir, "tests"), { recursive: true });
872
+ writeFileSync(join(tempDir, "tests", "real-thing.ts"), "export default true;");
873
+ writeFileSync(
874
+ join(tempDir, "tests", "source-verifier.test.ts"),
875
+ `
876
+ const rewritten = source.replace(
877
+ 'import { normalizeZagrebBusinessDeadline } from "./cutoff";',
878
+ 'const helper = true;'
879
+ );
880
+
881
+ import realThing from "./real-thing";
882
+ assert.ok(realThing);
883
+ `
884
+ );
885
+
886
+ try {
887
+ const task = createTask({
888
+ id: "T03",
889
+ key_files: ["tests/source-verifier.test.ts"],
890
+ });
891
+ const result = runPostExecutionChecks(task, [], tempDir);
892
+ assert.equal(result.status, "pass");
893
+ assert.deepEqual(result.checks, []);
894
+ } finally {
895
+ rmSync(tempDir, { recursive: true, force: true });
896
+ }
897
+ });
898
+
813
899
  test("returns fail status when blocking failure exists", () => {
814
900
  tempDir = join(tmpdir(), `post-exec-test-${Date.now()}`);
815
901
  mkdirSync(tempDir, { recursive: true });