@opengsd/gsd-pi 1.3.0-dev.965d1788 → 1.3.0-dev.cca9c184

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 (168) hide show
  1. package/dist/cli.js +17 -4
  2. package/dist/headless-answers.js +1 -1
  3. package/dist/headless-ui.d.ts +1 -1
  4. package/dist/headless-ui.js +1 -1
  5. package/dist/headless.js +1 -1
  6. package/dist/resources/.managed-resources-content-hash +1 -1
  7. package/dist/resources/extensions/claude-code-cli/models.js +9 -0
  8. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  9. package/dist/resources/extensions/gsd/auto-budget.js +5 -1
  10. package/dist/resources/extensions/gsd/auto-dispatch.js +14 -1
  11. package/dist/resources/extensions/gsd/auto-prompts.js +3 -3
  12. package/dist/resources/extensions/gsd/auto-recovery.js +3 -3
  13. package/dist/resources/extensions/gsd/auto.js +19 -10
  14. package/dist/resources/extensions/gsd/bootstrap/core-session-tools.js +1 -0
  15. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +5 -4
  16. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  17. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +25 -2
  18. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +32 -4
  19. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +15 -4
  20. package/dist/resources/extensions/gsd/commands/handlers/auto.js +1 -1
  21. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  22. package/dist/resources/extensions/gsd/commands-usage.js +1 -1
  23. package/dist/resources/extensions/gsd/db/engine.js +4 -1
  24. package/dist/resources/extensions/gsd/db-memory-fts-schema.js +34 -0
  25. package/dist/resources/extensions/gsd/delegation-policy.js +1 -1
  26. package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -2
  27. package/dist/resources/extensions/gsd/doctor-engine-checks.js +192 -4
  28. package/dist/resources/extensions/gsd/exec-sandbox.js +8 -2
  29. package/dist/resources/extensions/gsd/git-service.js +56 -0
  30. package/dist/resources/extensions/gsd/gitignore.js +1 -1
  31. package/dist/resources/extensions/gsd/migrate-external.js +11 -2
  32. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  33. package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  34. package/dist/resources/extensions/gsd/root-write-leak-guard.js +7 -1
  35. package/dist/resources/extensions/gsd/safety/destructive-guard.js +8 -1
  36. package/dist/resources/extensions/gsd/safety/evidence-collector.js +16 -1
  37. package/dist/resources/extensions/gsd/tools/plan-slice.js +59 -44
  38. package/dist/resources/extensions/gsd/tools/plan-task.js +28 -5
  39. package/dist/resources/extensions/gsd/unit-registry.js +4 -2
  40. package/dist/resources/extensions/gsd/workflow-manifest.js +122 -12
  41. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  42. package/dist/web/standalone/.next/BUILD_ID +1 -1
  43. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  44. package/dist/web/standalone/.next/build-manifest.json +2 -2
  45. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  46. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.html +1 -1
  63. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  70. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  71. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  73. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  74. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  75. package/package.json +1 -1
  76. package/packages/cloud-mcp-gateway/package.json +2 -2
  77. package/packages/contracts/package.json +1 -1
  78. package/packages/daemon/package.json +4 -4
  79. package/packages/gsd-agent-core/package.json +5 -5
  80. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts +2 -0
  81. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  82. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +23 -5
  83. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
  84. package/packages/gsd-agent-modes/package.json +7 -7
  85. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  86. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  87. package/packages/mcp-server/package.json +4 -4
  88. package/packages/native/package.json +1 -1
  89. package/packages/pi-agent-core/dist/agent-loop.js +3 -2
  90. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  91. package/packages/pi-agent-core/package.json +1 -1
  92. package/packages/pi-ai/dist/models.generated.d.ts +46 -0
  93. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  94. package/packages/pi-ai/dist/models.generated.js +38 -0
  95. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  96. package/packages/pi-ai/package.json +1 -1
  97. package/packages/pi-coding-agent/package.json +7 -7
  98. package/packages/pi-tui/package.json +2 -2
  99. package/packages/rpc-client/package.json +2 -2
  100. package/pkg/package.json +1 -1
  101. package/src/resources/extensions/claude-code-cli/models.ts +9 -0
  102. package/src/resources/extensions/claude-code-cli/tests/index.test.ts +14 -0
  103. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  104. package/src/resources/extensions/gsd/auto/session.ts +8 -0
  105. package/src/resources/extensions/gsd/auto-budget.ts +12 -1
  106. package/src/resources/extensions/gsd/auto-dispatch.ts +14 -1
  107. package/src/resources/extensions/gsd/auto-prompts.ts +3 -3
  108. package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
  109. package/src/resources/extensions/gsd/auto.ts +23 -8
  110. package/src/resources/extensions/gsd/bootstrap/core-session-tools.ts +1 -0
  111. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +5 -4
  112. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  113. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +32 -2
  114. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +49 -1
  115. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +34 -4
  116. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +16 -4
  117. package/src/resources/extensions/gsd/commands/handlers/auto.ts +1 -1
  118. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  119. package/src/resources/extensions/gsd/commands-usage.ts +1 -1
  120. package/src/resources/extensions/gsd/db/engine.ts +8 -1
  121. package/src/resources/extensions/gsd/db-memory-fts-schema.ts +45 -0
  122. package/src/resources/extensions/gsd/delegation-policy.ts +1 -1
  123. package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -2
  124. package/src/resources/extensions/gsd/doctor-engine-checks.ts +233 -5
  125. package/src/resources/extensions/gsd/doctor-types.ts +3 -0
  126. package/src/resources/extensions/gsd/exec-sandbox.ts +7 -2
  127. package/src/resources/extensions/gsd/git-service.ts +63 -0
  128. package/src/resources/extensions/gsd/gitignore.ts +1 -1
  129. package/src/resources/extensions/gsd/migrate-external.ts +8 -2
  130. package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  131. package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  132. package/src/resources/extensions/gsd/root-write-leak-guard.ts +13 -1
  133. package/src/resources/extensions/gsd/safety/destructive-guard.ts +9 -1
  134. package/src/resources/extensions/gsd/safety/evidence-collector.ts +16 -1
  135. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +14 -5
  136. package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +36 -0
  137. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +182 -4
  138. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +35 -0
  139. package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +30 -0
  140. package/src/resources/extensions/gsd/tests/db-memory-fts-schema.test.ts +58 -3
  141. package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +44 -0
  142. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +143 -0
  143. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +239 -2
  144. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +16 -0
  145. package/src/resources/extensions/gsd/tests/memory-store.test.ts +48 -0
  146. package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +57 -0
  147. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +2 -0
  148. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +103 -4
  149. package/src/resources/extensions/gsd/tests/plan-task.test.ts +44 -1
  150. package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +4 -2
  151. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +58 -0
  152. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -6
  153. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +103 -0
  154. package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +20 -1
  155. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +8 -3
  156. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +62 -5
  157. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +6 -3
  158. package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +38 -0
  159. package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +44 -17
  160. package/src/resources/extensions/gsd/tests/workflow-mcp-preflight.test.ts +1 -0
  161. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +8 -4
  162. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +6 -5
  163. package/src/resources/extensions/gsd/tools/plan-slice.ts +60 -47
  164. package/src/resources/extensions/gsd/tools/plan-task.ts +30 -5
  165. package/src/resources/extensions/gsd/unit-registry.ts +4 -2
  166. package/src/resources/extensions/gsd/workflow-manifest.ts +137 -6
  167. /package/dist/web/standalone/.next/static/{-ji77QJck2IKIwrGza1Bs → Eud_8lddsyi_13HIlB2i7}/_buildManifest.js +0 -0
  168. /package/dist/web/standalone/.next/static/{-ji77QJck2IKIwrGza1Bs → Eud_8lddsyi_13HIlB2i7}/_ssgManifest.js +0 -0
@@ -0,0 +1,44 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { classifyCommand } from "../safety/destructive-guard.ts";
5
+
6
+ test("destructive guard ignores rm -rf text inside quoted literals and comments", () => {
7
+ const readOnlyCommands = [
8
+ 'grep -rn "rm -rf" .',
9
+ 'echo "rm -rf /tmp/x"',
10
+ 'git commit -m "remove stray rm -rf usage"',
11
+ "cat notes.md # remove stray rm -rf usage",
12
+ 'printf "rm -rf /tmp/x\\n"',
13
+ ];
14
+
15
+ for (const command of readOnlyCommands) {
16
+ assert.deepEqual(classifyCommand(command), { destructive: false, labels: [] }, command);
17
+ }
18
+ });
19
+
20
+ test("destructive guard still detects direct and wrapper-prefixed recursive deletes", () => {
21
+ const destructiveCommands = [
22
+ "rm -rf build",
23
+ "rm build -rf",
24
+ "sudo rm -rf /",
25
+ "doas rm -rf /",
26
+ "sudo -u bob rm -rf /x",
27
+ "timeout 5 rm -rf /x",
28
+ "time rm -rf build",
29
+ "find . -exec rm -rf {} \\;",
30
+ "cat doomed.txt | xargs rm -rf /tmp/x",
31
+ 'rm -rf "/tmp/my build"',
32
+ ];
33
+
34
+ for (const command of destructiveCommands) {
35
+ assert.deepEqual(classifyCommand(command), {
36
+ destructive: true,
37
+ labels: ["recursive delete"],
38
+ }, command);
39
+ }
40
+ });
41
+
42
+ test("destructive guard treats quoted interpreter payloads as literals", () => {
43
+ assert.deepEqual(classifyCommand('bash -c "rm -rf /"'), { destructive: false, labels: [] });
44
+ });
@@ -96,6 +96,40 @@ function scaffoldMilestoneContext(basePath: string, mid: string): void {
96
96
  ].join("\n"));
97
97
  }
98
98
 
99
+ function scaffoldLegacyMilestoneContext(basePath: string, mid: string): void {
100
+ const dir = join(basePath, ".gsd", "milestones", mid);
101
+ mkdirSync(dir, { recursive: true });
102
+ writeFileSync(join(dir, `${mid}-CONTEXT.md`), [
103
+ `# ${mid}: Test Milestone`,
104
+ "",
105
+ "Context for legacy dispatch recovery tests.",
106
+ "",
107
+ ].join("\n"));
108
+ }
109
+
110
+ function scaffoldLegacySlicePlan(basePath: string, mid: string, sid: string): void {
111
+ const dir = join(basePath, ".gsd", "milestones", mid, "slices", sid);
112
+ mkdirSync(join(dir, "tasks"), { recursive: true });
113
+ writeFileSync(join(dir, `${sid}-PLAN.md`), [
114
+ `# ${sid}: Legacy Slice`,
115
+ "",
116
+ "## Tasks",
117
+ "- T01: Do something",
118
+ "",
119
+ ].join("\n"));
120
+ }
121
+
122
+ function scaffoldLegacyTaskPlan(basePath: string, mid: string, sid: string, tid: string): void {
123
+ const dir = join(basePath, ".gsd", "milestones", mid, "slices", sid, "tasks");
124
+ mkdirSync(dir, { recursive: true });
125
+ writeFileSync(join(dir, `${tid}-PLAN.md`), [
126
+ `# ${tid}: First Task`,
127
+ "",
128
+ "Implement the task.",
129
+ "",
130
+ ].join("\n"));
131
+ }
132
+
99
133
  function scaffoldTaskPlan(basePath: string, mid: string, sid: string, tid: string): void {
100
134
  // Flat-phase: no per-task plan files. This is a no-op — tasks live as
101
135
  // checkboxes inside the slice plan. Kept for backward-compat with tests
@@ -167,6 +201,115 @@ test("dispatch: present task plan proceeds to execute-task normally", async (t)
167
201
  `unitId should be M002/S03/T01, got: ${result.action === "dispatch" ? result.unitId : "(stop)"}`);
168
202
  });
169
203
 
204
+ test("dispatch: missing legacy task plan recovery increments a per-slice retry counter", async (t) => {
205
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-1087-retry-"));
206
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
207
+
208
+ scaffoldLegacyMilestoneContext(tmp, "M002");
209
+ scaffoldLegacySlicePlan(tmp, "M002", "S03");
210
+
211
+ const session = {
212
+ missingTaskPlanRetryCount: new Map<string, number>(),
213
+ };
214
+ const result = await resolveDispatch(makeContextFor(tmp, "M002", "S03", "T01", session));
215
+
216
+ assert.equal(result.action, "dispatch");
217
+ assert.ok(result.action === "dispatch" && result.unitType === "plan-slice",
218
+ `unitType should be plan-slice, got: ${result.action === "dispatch" ? result.unitType : "(stop)"}`);
219
+ assert.ok(result.action === "dispatch" && result.unitId === "M002/S03",
220
+ `unitId should be M002/S03, got: ${result.action === "dispatch" ? result.unitId : "(stop)"}`);
221
+ assert.equal(session.missingTaskPlanRetryCount.get("M002/S03"), 1);
222
+ });
223
+
224
+ test("dispatch: missing legacy task plan recovery stops when retry counter is exhausted", async (t) => {
225
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-1087-stop-"));
226
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
227
+
228
+ scaffoldLegacyMilestoneContext(tmp, "M002");
229
+ scaffoldLegacySlicePlan(tmp, "M002", "S03");
230
+
231
+ const session = {
232
+ missingTaskPlanRetryCount: new Map<string, number>([["M002/S03", 2]]),
233
+ };
234
+ const result = await resolveDispatch(makeContextFor(tmp, "M002", "S03", "T01", session));
235
+
236
+ assert.equal(result.action, "stop");
237
+ assert.ok(result.action === "stop");
238
+ assert.equal(result.level, "error");
239
+ assert.match(result.reason, /Missing task-plan recovery failed 2 times for M002\/S03/);
240
+ assert.match(result.reason, /manual intervention required/);
241
+ assert.equal(session.missingTaskPlanRetryCount.has("M002/S03"), false);
242
+ });
243
+
244
+ test("dispatch: present legacy task plan clears missing-plan recovery retry counter", async (t) => {
245
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-1087-clear-"));
246
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
247
+
248
+ scaffoldLegacyMilestoneContext(tmp, "M002");
249
+ scaffoldLegacySlicePlan(tmp, "M002", "S03");
250
+ scaffoldLegacyTaskPlan(tmp, "M002", "S03", "T01");
251
+
252
+ const session = {
253
+ missingTaskPlanRetryCount: new Map<string, number>([["M002/S03", 1]]),
254
+ preExecRetryCount: new Map<string, number>([["M002/S03", 2]]),
255
+ };
256
+ const result = await resolveDispatch(makeContextFor(tmp, "M002", "S03", "T01", session));
257
+
258
+ assert.equal(result.action, "dispatch");
259
+ assert.ok(result.action === "dispatch" && result.unitType === "execute-task",
260
+ `unitType should be execute-task, got: ${result.action === "dispatch" ? result.unitType : "(stop)"}`);
261
+ assert.equal(session.missingTaskPlanRetryCount.has("M002/S03"), false);
262
+ assert.equal(session.preExecRetryCount.get("M002/S03"), 2,
263
+ "pre-exec retry counter must not be cleared when task plan is present");
264
+ });
265
+
266
+ test("dispatch: missing-task-plan recovery loop terminates even when the shared pre-exec key is reset between rounds (#1087)", async (t) => {
267
+ // The recovery rule re-dispatches plan-slice with unitId "${mid}/${sid}".
268
+ // That regenerated plan-slice carries the same currentUnit.id, and on a
269
+ // pre-execution pass the post-unit hook deletes that key from
270
+ // preExecRetryCount (auto-post-unit.ts). Missing per-task PLAN projection
271
+ // files do not fail pre-exec checks, so the regenerated plan-slice typically
272
+ // passes and that delete fires every cycle. If recovery shared
273
+ // preExecRetryCount, its counter would be wiped to 0 each round and the loop
274
+ // would never reach the cap. Here we simulate that reset between rounds and
275
+ // assert the dedicated counter still climbs to MAX and stops.
276
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-1087-loop-term-"));
277
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
278
+
279
+ scaffoldLegacyMilestoneContext(tmp, "M002");
280
+ scaffoldLegacySlicePlan(tmp, "M002", "S03");
281
+
282
+ const session = {
283
+ missingTaskPlanRetryCount: new Map<string, number>(),
284
+ preExecRetryCount: new Map<string, number>(),
285
+ };
286
+
287
+ // Round 1: missing task plan → recover (counter 0 → 1).
288
+ const r1 = await resolveDispatch(makeContextFor(tmp, "M002", "S03", "T01", session));
289
+ assert.ok(r1.action === "dispatch" && r1.unitType === "plan-slice",
290
+ `round 1 should re-dispatch plan-slice, got: ${r1.action === "dispatch" ? r1.unitType : "(stop)"}`);
291
+ assert.equal(session.missingTaskPlanRetryCount.get("M002/S03"), 1);
292
+
293
+ // The regenerated plan-slice passes its post-unit pre-exec check, deleting
294
+ // the shared "${mid}/${sid}" key from preExecRetryCount.
295
+ session.preExecRetryCount.delete("M002/S03");
296
+
297
+ // Round 2: still missing → recover (counter 1 → 2), unaffected by the reset.
298
+ const r2 = await resolveDispatch(makeContextFor(tmp, "M002", "S03", "T01", session));
299
+ assert.ok(r2.action === "dispatch" && r2.unitType === "plan-slice",
300
+ `round 2 should re-dispatch plan-slice, got: ${r2.action === "dispatch" ? r2.unitType : "(stop)"}`);
301
+ assert.equal(session.missingTaskPlanRetryCount.get("M002/S03"), 2);
302
+
303
+ session.preExecRetryCount.delete("M002/S03");
304
+
305
+ // Round 3: cap reached → stop. The loop terminates despite the resets.
306
+ const r3 = await resolveDispatch(makeContextFor(tmp, "M002", "S03", "T01", session));
307
+ assert.equal(r3.action, "stop");
308
+ assert.ok(r3.action === "stop" && r3.level === "error");
309
+ assert.match(r3.reason, /Missing task-plan recovery failed 2 times for M002\/S03/);
310
+ assert.equal(session.missingTaskPlanRetryCount.has("M002/S03"), false);
311
+ });
312
+
170
313
  test("dispatch: session milestone mismatch stops before missing-task-plan recovery", async (t) => {
171
314
  const tmp = mkdtempSync(join(tmpdir(), "gsd-session-milestone-mismatch-"));
172
315
  t.after(() => rmSync(tmp, { recursive: true, force: true }));
@@ -1,12 +1,22 @@
1
1
  import { afterEach, test } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { _getAdapter, closeDatabase, insertMilestone, openDatabase } from "../gsd-db.ts";
4
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import {
4
+ _getAdapter,
5
+ closeDatabase,
6
+ insertArtifact,
7
+ insertMilestone,
8
+ insertSlice,
9
+ insertTask,
10
+ openDatabase,
11
+ } from "../gsd-db.ts";
12
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
13
  import { join } from "node:path";
6
14
  import { tmpdir } from "node:os";
7
15
  import { filterDoctorIssues } from "../doctor-format.ts";
8
16
  import { checkEngineHealth } from "../doctor-engine-checks.ts";
17
+ import { MEMORIES_FTS_REBUILT_KEY } from "../db-memory-fts-schema.ts";
9
18
  import { appendEvent } from "../workflow-events.ts";
19
+ import { renderPlanFromDb, renderRoadmapFromDb } from "../markdown-renderer.ts";
10
20
 
11
21
  afterEach(() => {
12
22
  closeDatabase();
@@ -61,6 +71,139 @@ test("checkEngineHealth reports db_unavailable when gsd.db exists but the DB is
61
71
  assert.equal(dbIssue.file, ".gsd/gsd.db");
62
72
  });
63
73
 
74
+ test("checkEngineHealth reports memories_fts without the rebuild marker", async (t) => {
75
+ const base = mkdtempSync(join(tmpdir(), "gsd-doctor-memory-fts-marker-"));
76
+ t.after(() => rmSync(base, { recursive: true, force: true }));
77
+
78
+ const gsdDir = join(base, ".gsd");
79
+ mkdirSync(gsdDir, { recursive: true });
80
+
81
+ openDatabase(join(gsdDir, "gsd.db"));
82
+ const adapter = _getAdapter()!;
83
+ const fts = adapter.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
84
+ if (!fts) return;
85
+
86
+ adapter.prepare(
87
+ "DELETE FROM runtime_kv WHERE scope = 'global' AND scope_id = '' AND key = :key",
88
+ ).run({ ":key": MEMORIES_FTS_REBUILT_KEY });
89
+
90
+ const issues: any[] = [];
91
+ await checkEngineHealth(base, issues, []);
92
+
93
+ const ftsIssue = issues.find((issue) => issue.code === "memories_fts_rebuild_missing");
94
+ assert.ok(ftsIssue, "doctor should surface a potentially desynced memory FTS index");
95
+ assert.equal(ftsIssue.severity, "warning");
96
+ assert.equal(ftsIssue.unitId, "project");
97
+ assert.equal(ftsIssue.file, ".gsd/gsd.db");
98
+ assert.equal(ftsIssue.fixable, false);
99
+ });
100
+
101
+ test("checkEngineHealth reports checkbox divergence against DB status", async (t) => {
102
+ const base = mkdtempSync(join(tmpdir(), "gsd-doctor-checkbox-drift-"));
103
+ t.after(() => rmSync(base, { recursive: true, force: true }));
104
+
105
+ const gsdDir = join(base, ".gsd");
106
+ mkdirSync(gsdDir, { recursive: true });
107
+
108
+ openDatabase(join(gsdDir, "gsd.db"));
109
+ insertMilestone({ id: "M001", title: "Foundation", status: "active" });
110
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
111
+ insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "Task", status: "complete", sequence: 1 });
112
+
113
+ const roadmap = await renderRoadmapFromDb(base, "M001");
114
+ if ("skipped" in roadmap) assert.fail("planned milestone should render a roadmap");
115
+ const plan = await renderPlanFromDb(base, "M001", "S01");
116
+
117
+ writeFileSync(roadmap.roadmapPath, readFileSync(roadmap.roadmapPath, "utf-8").replace("- [ ] **S01:", "- [x] **S01:"), "utf-8");
118
+ writeFileSync(plan.planPath, readFileSync(plan.planPath, "utf-8").replace("- [x] **T01**:", "- [ ] **T01**:"), "utf-8");
119
+
120
+ const issues: any[] = [];
121
+ await checkEngineHealth(base, issues, []);
122
+
123
+ const divergences = issues.filter((issue) => issue.code === "checkbox_db_status_divergence");
124
+ assert.deepEqual(
125
+ divergences.map((issue) => issue.unitId).sort(),
126
+ ["M001/S01", "M001/S01/T01"],
127
+ );
128
+ });
129
+
130
+ test("checkEngineHealth keeps PLAN checkbox divergence after stale projection flush", async (t) => {
131
+ const base = mkdtempSync(join(tmpdir(), "gsd-doctor-checkbox-plan-drift-"));
132
+ t.after(() => rmSync(base, { recursive: true, force: true }));
133
+
134
+ const gsdDir = join(base, ".gsd");
135
+ mkdirSync(gsdDir, { recursive: true });
136
+
137
+ openDatabase(join(gsdDir, "gsd.db"));
138
+ insertMilestone({ id: "M001", title: "Foundation", status: "active" });
139
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
140
+ insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "Task", status: "complete", sequence: 1 });
141
+
142
+ const roadmap = await renderRoadmapFromDb(base, "M001");
143
+ if ("skipped" in roadmap) assert.fail("planned milestone should render a roadmap");
144
+ const plan = await renderPlanFromDb(base, "M001", "S01");
145
+
146
+ writeFileSync(roadmap.roadmapPath, readFileSync(roadmap.roadmapPath, "utf-8").replace("- [ ] **S01:", "- [x] **S01:"), "utf-8");
147
+ writeFileSync(plan.planPath, readFileSync(plan.planPath, "utf-8").replace("- [x] **T01**:", "- [ ] **T01**:"), "utf-8");
148
+ appendEvent(base, {
149
+ cmd: "complete-task",
150
+ params: { milestoneId: "M001", sliceId: "S01", taskId: "T01" },
151
+ ts: "2999-01-01T00:00:00.000Z",
152
+ actor: "agent",
153
+ });
154
+
155
+ const issues: any[] = [];
156
+ const fixes: string[] = [];
157
+ await checkEngineHealth(base, issues, fixes);
158
+
159
+ const divergences = issues.filter((issue) => issue.code === "checkbox_db_status_divergence");
160
+ assert.deepEqual(
161
+ divergences.map((issue) => issue.unitId),
162
+ ["M001/S01/T01"],
163
+ "stale ROADMAP divergence is cleared after re-render, but stale PLAN task divergence remains",
164
+ );
165
+ assert.ok(fixes.includes("re-rendered stale projections for M001"));
166
+ assert.match(readFileSync(plan.planPath, "utf-8"), /- \[ \] \*\*T01\*\*:/);
167
+ });
168
+
169
+ test("checkEngineHealth reads task checkboxes from the <tasks> block, not a stray line above it", async (t) => {
170
+ const base = mkdtempSync(join(tmpdir(), "gsd-doctor-task-section-"));
171
+ t.after(() => rmSync(base, { recursive: true, force: true }));
172
+
173
+ const gsdDir = join(base, ".gsd");
174
+ mkdirSync(gsdDir, { recursive: true });
175
+
176
+ openDatabase(join(gsdDir, "gsd.db"));
177
+ insertMilestone({ id: "M001", title: "Foundation", status: "active" });
178
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
179
+ insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "Task", status: "complete", sequence: 1 });
180
+
181
+ const roadmap = await renderRoadmapFromDb(base, "M001");
182
+ if ("skipped" in roadmap) assert.fail("planned milestone should render a roadmap");
183
+ const plan = await renderPlanFromDb(base, "M001", "S01");
184
+
185
+ // Flip the authoritative <tasks> checkbox so it disagrees with the DB
186
+ // (DB: complete, markdown: unchecked = real drift), then inject a *checked*
187
+ // decoy line for the same task above the <tasks> block. The old whole-file
188
+ // regex matched the decoy first and hid the real drift; parsePlan reads the
189
+ // authoritative <tasks> block and still reports it. No events are appended,
190
+ // so the projection-drift re-render never runs and the crafted file stands.
191
+ const rendered = readFileSync(plan.planPath, "utf-8")
192
+ .replace("- [x] **T01**:", "- [ ] **T01**:")
193
+ .replace("<tasks>\n", "- [x] **T01**: stale duplicate above the tasks block\n\n<tasks>\n");
194
+ writeFileSync(plan.planPath, rendered, "utf-8");
195
+
196
+ const issues: any[] = [];
197
+ await checkEngineHealth(base, issues, []);
198
+
199
+ const divergences = issues.filter((issue) => issue.code === "checkbox_db_status_divergence");
200
+ assert.deepEqual(
201
+ divergences.map((issue) => issue.unitId),
202
+ ["M001/S01/T01"],
203
+ "task drift must be read from the authoritative <tasks> block, not a stray checkbox above it",
204
+ );
205
+ });
206
+
64
207
  test("checkEngineHealth reads canonical reopen events from worktree bases", async (t) => {
65
208
  const base = mkdtempSync(join(tmpdir(), "gsd-doctor-reopen-worktree-"));
66
209
  t.after(() => rmSync(base, { recursive: true, force: true }));
@@ -149,3 +292,97 @@ test("checkEngineHealth treats explicit reopen as authoritative when dispatch ti
149
292
  "explicit reopen should exempt reopened milestone even when completion dispatch timestamps are absent",
150
293
  );
151
294
  });
295
+
296
+ test("checkEngineHealth reports artifact rows whose files are missing on disk", async (t) => {
297
+ const base = mkdtempSync(join(tmpdir(), "gsd-doctor-missing-artifact-"));
298
+ t.after(() => rmSync(base, { recursive: true, force: true }));
299
+
300
+ const gsdDir = join(base, ".gsd");
301
+ const worktree = join(gsdDir, "worktrees", "M001");
302
+ mkdirSync(join(worktree, ".gsd", "milestones", "M001"), { recursive: true });
303
+ writeFileSync(join(worktree, ".git"), "gitdir: ../../../../.git/worktrees/M001\n", "utf-8");
304
+ writeFileSync(join(worktree, ".gsd", "milestones", "M001", "M001-CONTEXT.md"), "# Context\n", "utf-8");
305
+
306
+ openDatabase(join(gsdDir, "gsd.db"));
307
+ insertArtifact({
308
+ path: "milestones/M001/M001-CONTEXT.md",
309
+ artifact_type: "CONTEXT",
310
+ milestone_id: "M001",
311
+ slice_id: null,
312
+ task_id: null,
313
+ full_content: "# Context\n",
314
+ });
315
+ insertArtifact({
316
+ path: "milestones/M001/M001-MISSING.md",
317
+ artifact_type: "CONTEXT",
318
+ milestone_id: "M001",
319
+ slice_id: null,
320
+ task_id: null,
321
+ full_content: "# Missing\n",
322
+ });
323
+
324
+ const issues: any[] = [];
325
+ await checkEngineHealth(worktree, issues, []);
326
+
327
+ assert.equal(
328
+ issues.some((issue) => issue.code === "artifact_file_missing" && issue.file === "milestones/M001/M001-CONTEXT.md"),
329
+ false,
330
+ "worktree-local artifacts should resolve through the projection root",
331
+ );
332
+ const missing = issues.find((issue) => issue.code === "artifact_file_missing" && issue.file === "milestones/M001/M001-MISSING.md");
333
+ assert.ok(missing, "missing artifact rows should be reported");
334
+ assert.equal(missing.unitId, "M001");
335
+ assert.equal(missing.fixable, false);
336
+ });
337
+
338
+ test("checkEngineHealth clears artifact_file_missing after projection re-render recreates the file", async (t) => {
339
+ const base = mkdtempSync(join(tmpdir(), "gsd-doctor-stale-missing-artifact-"));
340
+ t.after(() => rmSync(base, { recursive: true, force: true }));
341
+
342
+ const gsdDir = join(base, ".gsd");
343
+ mkdirSync(gsdDir, { recursive: true });
344
+
345
+ openDatabase(join(gsdDir, "gsd.db"));
346
+ insertMilestone({ id: "M001", title: "Foundation", status: "active", planning: { vision: "Ship the foundation." } });
347
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
348
+ insertArtifact({
349
+ path: "phases/01-foundation/01-ROADMAP.md",
350
+ artifact_type: "ROADMAP",
351
+ milestone_id: "M001",
352
+ slice_id: null,
353
+ task_id: null,
354
+ full_content: "# stale row only\n",
355
+ });
356
+ // CONTEXT is not re-rendered by flushWorkflowProjections, so its missing-file
357
+ // diagnostic must survive the post-re-render cleanup (guards against the
358
+ // clearing logic over-broadening to artifacts the same run did not recreate).
359
+ insertArtifact({
360
+ path: "phases/01-foundation/01-CONTEXT.md",
361
+ artifact_type: "CONTEXT",
362
+ milestone_id: "M001",
363
+ slice_id: null,
364
+ task_id: null,
365
+ full_content: "# stale context row only\n",
366
+ });
367
+ appendEvent(base, {
368
+ cmd: "plan-milestone",
369
+ params: { milestoneId: "M001" },
370
+ ts: "2999-01-01T00:00:00.000Z",
371
+ actor: "agent",
372
+ });
373
+
374
+ const issues: any[] = [];
375
+ const fixes: string[] = [];
376
+ await checkEngineHealth(base, issues, fixes);
377
+
378
+ assert.ok(fixes.includes("re-rendered missing projections for M001"));
379
+ assert.equal(
380
+ issues.some((issue) => issue.code === "artifact_file_missing" && issue.file === "phases/01-foundation/01-ROADMAP.md"),
381
+ false,
382
+ "doctor should not report a missing artifact that projection repair recreated in the same run",
383
+ );
384
+ assert.ok(
385
+ issues.some((issue) => issue.code === "artifact_file_missing" && issue.file === "phases/01-foundation/01-CONTEXT.md"),
386
+ "doctor should still report a missing artifact that projection repair did not recreate",
387
+ );
388
+ });
@@ -7,6 +7,7 @@ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:f
7
7
  import { tmpdir } from 'node:os';
8
8
  import { join } from 'node:path';
9
9
 
10
+ import { getShellConfig } from '@gsd/pi-coding-agent';
10
11
  import { EXEC_DEFAULTS, runExecSandbox, type ExecSandboxOptions } from '../exec-sandbox.ts';
11
12
  import { buildExecOptions, executeGsdExec } from '../tools/exec-tool.ts';
12
13
  import { isContextModeEnabled } from '../preferences-types.ts';
@@ -53,6 +54,21 @@ test('runExecSandbox: captures stdout, persists artifacts, returns digest', asyn
53
54
  }
54
55
  });
55
56
 
57
+ test('runExecSandbox: bash runtime uses resolved shell config', async () => {
58
+ const base = freshBase();
59
+ try {
60
+ const { shell } = getShellConfig();
61
+ const result = await runExecSandbox(
62
+ { runtime: 'bash', script: 'printf "%s" "$0"' },
63
+ baseOpts(base),
64
+ );
65
+ assert.equal(result.exit_code, 0);
66
+ assert.equal(readFileSync(result.stdout_path, 'utf-8'), shell);
67
+ } finally {
68
+ cleanup(base);
69
+ }
70
+ });
71
+
56
72
  test('runExecSandbox: persists optional request metadata', async () => {
57
73
  const base = freshBase();
58
74
  try {
@@ -5,6 +5,9 @@ import {
5
5
  SCHEMA_VERSION,
6
6
  _getAdapter,
7
7
  } from '../gsd-db.ts';
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import * as os from 'node:os';
8
11
  import {
9
12
  _resetLogs,
10
13
  peekLogs,
@@ -22,6 +25,7 @@ import {
22
25
  markUnitProcessed,
23
26
  decayStaleMemories,
24
27
  enforceMemoryCap,
28
+ queryMemoriesRanked,
25
29
  applyMemoryActions,
26
30
  formatMemoriesForPrompt,
27
31
  } from '../memory-store.ts';
@@ -29,6 +33,16 @@ import type { MemoryAction } from '../memory-store.ts';
29
33
  import { describe, test, beforeEach, afterEach } from 'node:test';
30
34
  import assert from 'node:assert/strict';
31
35
 
36
+ function tempDbPath(): string {
37
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-memory-store-test-'));
38
+ return path.join(dir, 'test.db');
39
+ }
40
+
41
+ function cleanupDbPath(dbPath: string): void {
42
+ closeDatabase();
43
+ fs.rmSync(path.dirname(dbPath), { recursive: true, force: true });
44
+ }
45
+
32
46
  // ═══════════════════════════════════════════════════════════════════════════
33
47
  // memory-store: fallback when DB not open
34
48
  // ═══════════════════════════════════════════════════════════════════════════
@@ -366,6 +380,40 @@ test('memory-store: schema includes memories table', () => {
366
380
  closeDatabase();
367
381
  });
368
382
 
383
+ test('memory-store: reopening rebuilds FTS index for pre-FTS memories', () => {
384
+ const dbPath = tempDbPath();
385
+ try {
386
+ openDatabase(dbPath);
387
+ const id = createMemory({ category: 'gotcha', content: 'pre FTS auth memory must become searchable' });
388
+ assert.equal(id, 'MEM001');
389
+
390
+ const adapter = _getAdapter()!;
391
+ const fts = adapter.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
392
+ if (!fts) return;
393
+
394
+ adapter.exec('DROP TRIGGER IF EXISTS memories_ai');
395
+ adapter.exec('DROP TRIGGER IF EXISTS memories_ad');
396
+ adapter.exec('DROP TRIGGER IF EXISTS memories_au');
397
+ adapter.exec('DROP TABLE IF EXISTS memories_fts');
398
+ adapter.prepare(
399
+ "DELETE FROM runtime_kv WHERE scope = 'global' AND scope_id = '' AND key = 'memories_fts_rebuilt_at'",
400
+ ).run();
401
+ closeDatabase();
402
+
403
+ openDatabase(dbPath);
404
+ const reopened = _getAdapter()!;
405
+ const marker = reopened.prepare(
406
+ "SELECT value_json FROM runtime_kv WHERE scope = 'global' AND scope_id = '' AND key = 'memories_fts_rebuilt_at'",
407
+ ).get();
408
+ assert.ok(marker, 'FTS rebuild marker should be set after reopening');
409
+
410
+ const hits = queryMemoriesRanked({ query: 'auth', k: 5 }).map((hit) => hit.memory.id);
411
+ assert.deepEqual(hits, ['MEM001']);
412
+ } finally {
413
+ cleanupDbPath(dbPath);
414
+ }
415
+ });
416
+
369
417
  // ═══════════════════════════════════════════════════════════════════════════
370
418
  // regression #4967 — createMemory must not silently swallow SQL errors
371
419
  // ═══════════════════════════════════════════════════════════════════════════
@@ -7,7 +7,10 @@ import {
7
7
  existsSync,
8
8
  mkdirSync,
9
9
  realpathSync,
10
+ readFileSync,
10
11
  } from "node:fs";
12
+ import fs from "node:fs";
13
+ import { syncBuiltinESMExports } from "node:module";
11
14
  import { join } from "node:path";
12
15
  import { tmpdir } from "node:os";
13
16
  import { execSync } from "node:child_process";
@@ -116,6 +119,60 @@ describe("migrate-external worktree guard (#2970)", () => {
116
119
  });
117
120
  });
118
121
 
122
+ describe("migrateToExternalState copy failure rollback (#1047)", () => {
123
+ test("restores .gsd when a source entry fails to copy", () => {
124
+ const base = realpathSync(mkdtempSync(join(tmpdir(), "gsd-migrate-copy-fail-")));
125
+ const stateDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-state-copy-fail-")));
126
+ const previousStateDir = process.env.GSD_STATE_DIR;
127
+ const previousProjectId = process.env.GSD_PROJECT_ID;
128
+ const originalCpSync = fs.cpSync;
129
+ process.env.GSD_STATE_DIR = stateDir;
130
+ process.env.GSD_PROJECT_ID = "copy-fail";
131
+
132
+ try {
133
+ run("git init -b main", base);
134
+ mkdirSync(join(base, ".gsd"), { recursive: true });
135
+ writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "# prefs\n", "utf-8");
136
+ writeFileSync(join(base, ".gsd", "gsd.db"), "state", "utf-8");
137
+ const failingSource = join(base, ".gsd.migrating", "gsd.db");
138
+ fs.cpSync = ((...args: Parameters<typeof fs.cpSync>) => {
139
+ const src = args[0];
140
+ if (String(src) === failingSource) {
141
+ const error = new Error("simulated copy failure") as NodeJS.ErrnoException;
142
+ error.code = "EACCES";
143
+ throw error;
144
+ }
145
+ return originalCpSync(...args);
146
+ }) as typeof fs.cpSync;
147
+ syncBuiltinESMExports();
148
+
149
+ const result = migrateToExternalState(base);
150
+
151
+ assert.equal(result.migrated, false, "copy failure must fail the migration");
152
+ assert.match(result.error ?? "", /Migration copy failed/);
153
+ assert.ok(existsSync(join(base, ".gsd", "gsd.db")), "source .gsd must be restored");
154
+ assert.equal(readFileSync(join(base, ".gsd", "PREFERENCES.md"), "utf-8"), "# prefs\n");
155
+ assert.ok(!existsSync(join(base, ".gsd.migrating")), "backup staging dir must not remain after restore");
156
+ assert.ok(!existsSync(join(stateDir, "projects", "copy-fail", "gsd.db")), "failed file must not be reported as migrated");
157
+ } finally {
158
+ fs.cpSync = originalCpSync;
159
+ syncBuiltinESMExports();
160
+ if (previousStateDir === undefined) {
161
+ delete process.env.GSD_STATE_DIR;
162
+ } else {
163
+ process.env.GSD_STATE_DIR = previousStateDir;
164
+ }
165
+ if (previousProjectId === undefined) {
166
+ delete process.env.GSD_PROJECT_ID;
167
+ } else {
168
+ process.env.GSD_PROJECT_ID = previousProjectId;
169
+ }
170
+ rmSync(base, { recursive: true, force: true });
171
+ rmSync(stateDir, { recursive: true, force: true });
172
+ }
173
+ });
174
+ });
175
+
119
176
  // Regression tests for #5571 — recoverFailedMigration handles orphaned .gsd.migrating
120
177
  describe("recoverFailedMigration (#5571)", () => {
121
178
  test("returns false when .gsd.migrating does not exist", () => {
@@ -58,6 +58,8 @@ const ALLOWED_IMPORTERS = new Set([
58
58
  "gsd/artifact-verification.ts",
59
59
  // diagnostics-only surfaces: report on projections, make no dispatch decisions
60
60
  "gsd/doctor.ts",
61
+ // diagnostics-only: compares PLAN.md task checkboxes against DB status
62
+ "gsd/doctor-engine-checks.ts",
61
63
  // display/telemetry-only surfaces
62
64
  "gsd/workspace-index.ts",
63
65
  "gsd/visualizer-data.ts",