gsd-pi 2.80.0-dev.96f4d89d5 → 2.80.0-dev.b2a374d5b

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 (239) 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-dispatch.js +10 -0
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +10 -10
  7. package/dist/resources/extensions/gsd/auto-prompts.js +111 -1
  8. package/dist/resources/extensions/gsd/auto-recovery.js +154 -8
  9. package/dist/resources/extensions/gsd/auto-start.js +2 -3
  10. package/dist/resources/extensions/gsd/auto.js +16 -3
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +32 -1
  12. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +129 -1
  13. package/dist/resources/extensions/gsd/clean-root-preflight.js +42 -4
  14. package/dist/resources/extensions/gsd/commands/dispatcher.js +5 -0
  15. package/dist/resources/extensions/gsd/commands-extract-learnings.js +17 -12
  16. package/dist/resources/extensions/gsd/crash-recovery.js +56 -10
  17. package/dist/resources/extensions/gsd/db-base-schema.js +14 -0
  18. package/dist/resources/extensions/gsd/db-migration-steps.js +16 -0
  19. package/dist/resources/extensions/gsd/detection.js +106 -0
  20. package/dist/resources/extensions/gsd/gsd-db.js +102 -2
  21. package/dist/resources/extensions/gsd/guided-flow.js +63 -12
  22. package/dist/resources/extensions/gsd/migrate/command.js +40 -1
  23. package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
  24. package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
  25. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
  26. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  28. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  29. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  30. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  31. package/dist/resources/extensions/gsd/quick.js +34 -2
  32. package/dist/resources/extensions/gsd/safety/evidence-collector.js +10 -2
  33. package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
  34. package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
  35. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  36. package/dist/resources/extensions/gsd/worktree-manager.js +16 -14
  37. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  38. package/dist/web/standalone/.next/BUILD_ID +1 -1
  39. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  40. package/dist/web/standalone/.next/build-manifest.json +2 -2
  41. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  42. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.html +1 -1
  59. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  66. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  68. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  69. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  70. package/package.json +4 -2
  71. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  72. package/packages/pi-coding-agent/dist/core/agent-session.js +3 -0
  73. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  74. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +76 -0
  75. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  77. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  79. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  81. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  82. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  83. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  84. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
  85. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
  86. package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
  87. package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
  88. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
  89. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
  90. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
  91. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
  92. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +3 -0
  93. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/extensions/runner.js +17 -1
  95. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +99 -0
  97. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  99. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  102. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  104. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.js +6 -4
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +50 -19
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js +2 -2
  110. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +2 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.js +7 -6
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +13 -0
  119. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +77 -13
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +68 -2
  124. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  129. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +36 -27
  132. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  133. package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js +21 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
  136. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
  137. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
  138. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
  139. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
  140. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
  141. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
  142. package/packages/pi-coding-agent/src/core/agent-session.ts +3 -0
  143. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +89 -0
  144. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  145. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  146. package/packages/pi-coding-agent/src/core/db-snapshot.test.ts +32 -0
  147. package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
  148. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +110 -0
  149. package/packages/pi-coding-agent/src/core/extensions/runner.ts +19 -1
  150. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  151. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  152. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.ts +7 -5
  153. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +68 -19
  154. package/packages/pi-coding-agent/src/modes/interactive/components/adaptive-layout.ts +2 -2
  155. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +1 -0
  156. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +2 -1
  157. package/packages/pi-coding-agent/src/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.ts +10 -9
  158. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +88 -13
  159. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  160. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +91 -3
  161. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  162. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +36 -27
  163. package/packages/pi-coding-agent/src/modes/interactive/tui-mode.test.ts +25 -0
  164. package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
  165. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
  166. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  167. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  168. package/packages/pi-tui/dist/tui.js +17 -7
  169. package/packages/pi-tui/dist/tui.js.map +1 -1
  170. package/packages/pi-tui/src/tui.ts +19 -7
  171. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  172. package/pkg/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  173. package/pkg/dist/modes/interactive/theme/themes.js +36 -27
  174. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  175. package/src/resources/GSD-WORKFLOW.md +2 -2
  176. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  177. package/src/resources/extensions/gsd/auto/loop.ts +50 -8
  178. package/src/resources/extensions/gsd/auto/phases.ts +42 -28
  179. package/src/resources/extensions/gsd/auto-dispatch.ts +17 -0
  180. package/src/resources/extensions/gsd/auto-post-unit.ts +10 -10
  181. package/src/resources/extensions/gsd/auto-prompts.ts +116 -1
  182. package/src/resources/extensions/gsd/auto-recovery.ts +153 -7
  183. package/src/resources/extensions/gsd/auto-start.ts +7 -6
  184. package/src/resources/extensions/gsd/auto.ts +23 -3
  185. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +39 -1
  186. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +135 -1
  187. package/src/resources/extensions/gsd/clean-root-preflight.ts +41 -3
  188. package/src/resources/extensions/gsd/commands/dispatcher.ts +6 -0
  189. package/src/resources/extensions/gsd/commands-extract-learnings.ts +17 -12
  190. package/src/resources/extensions/gsd/crash-recovery.ts +67 -10
  191. package/src/resources/extensions/gsd/db-base-schema.ts +15 -0
  192. package/src/resources/extensions/gsd/db-migration-steps.ts +17 -0
  193. package/src/resources/extensions/gsd/detection.ts +128 -0
  194. package/src/resources/extensions/gsd/gsd-db.ts +119 -1
  195. package/src/resources/extensions/gsd/guided-flow.ts +66 -12
  196. package/src/resources/extensions/gsd/migrate/command.ts +47 -1
  197. package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
  198. package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
  199. package/src/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
  200. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  201. package/src/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  202. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  203. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  204. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  205. package/src/resources/extensions/gsd/quick.ts +37 -2
  206. package/src/resources/extensions/gsd/safety/evidence-collector.ts +11 -2
  207. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1 -1
  208. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +170 -1
  209. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +88 -2
  210. package/src/resources/extensions/gsd/tests/commands-extract-learnings.test.ts +9 -0
  211. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +55 -0
  212. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +22 -0
  213. package/src/resources/extensions/gsd/tests/db-schema-metadata.test.ts +2 -2
  214. package/src/resources/extensions/gsd/tests/detection.test.ts +140 -0
  215. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +44 -0
  216. package/src/resources/extensions/gsd/tests/has-pending-deep-stage.test.ts +33 -1
  217. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
  218. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
  219. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
  220. package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
  221. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  222. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  223. package/src/resources/extensions/gsd/tests/right-sized-workflow-prompts.test.ts +192 -0
  224. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +29 -0
  225. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +38 -0
  226. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +63 -2
  227. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  228. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +7 -0
  229. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +1 -1
  230. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +37 -6
  231. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +7 -0
  232. package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +9 -2
  233. package/src/resources/extensions/gsd/tests/worktree-write-gate.test.ts +179 -0
  234. package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
  235. package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
  236. package/src/resources/extensions/gsd/unit-runtime.ts +14 -0
  237. package/src/resources/extensions/gsd/worktree-manager.ts +15 -4
  238. /package/dist/web/standalone/.next/static/{y7d--cnc6Cibuva-Lzzw_ → FU0iL9dhdc2IBzEYBDOlY}/_buildManifest.js +0 -0
  239. /package/dist/web/standalone/.next/static/{y7d--cnc6Cibuva-Lzzw_ → FU0iL9dhdc2IBzEYBDOlY}/_ssgManifest.js +0 -0
@@ -30,6 +30,7 @@ import {
30
30
  isLockProcessAlive,
31
31
  } from "../crash-recovery.ts";
32
32
  import { normalizeRealPath } from "../paths.ts";
33
+ import { writeUnitRuntimeRecord } from "../unit-runtime.ts";
33
34
 
34
35
  function makeBase(): string {
35
36
  const base = mkdtempSync(join(tmpdir(), "gsd-crash-recovery-"));
@@ -102,6 +103,27 @@ test("readCrashLock synthesizes LockData from a stale dead worker (no dispatches
102
103
  assert.ok(lock!.startedAt, "startedAt populated from workers.started_at");
103
104
  });
104
105
 
106
+ test("readCrashLock falls back to latest in-flight runtime record when dispatch claim is missing", (t) => {
107
+ const base = makeBase();
108
+ t.after(() => cleanup(base));
109
+ openDatabase(join(base, ".gsd", "gsd.db"));
110
+ const projectRoot = normalizeRealPath(base);
111
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
112
+ writeUnitRuntimeRecord(base, "execute-task", "M008/S04/T02", 1778069087937, {
113
+ phase: "dispatched",
114
+ lastProgressAt: 1778069087937,
115
+ lastProgressKind: "dispatch",
116
+ });
117
+ setWorkerPid(workerId, 99999);
118
+ expireWorker(workerId);
119
+
120
+ const lock = readCrashLock(base);
121
+ assert.ok(lock, "stale worker surfaced as a crash lock");
122
+ assert.equal(lock!.unitType, "execute-task");
123
+ assert.equal(lock!.unitId, "M008/S04/T02");
124
+ assert.equal(lock!.unitStartedAt, new Date(1778069087937).toISOString());
125
+ });
126
+
105
127
  test("readCrashLock includes the most recent dispatch as unitType/unitId", (t) => {
106
128
  const base = makeBase();
107
129
  t.after(() => cleanup(base));
@@ -106,10 +106,10 @@ describe("db-schema-metadata", () => {
106
106
  test("records schema version rows with timestamps", () => {
107
107
  const db = new FakeAdapter();
108
108
 
109
- recordSchemaVersion(db, 25);
109
+ recordSchemaVersion(db, 26);
110
110
 
111
111
  assert.equal(db.runCalls.length, 1);
112
- assert.equal((db.runCalls[0][0] as Record<string, unknown>)[":version"], 25);
112
+ assert.equal((db.runCalls[0][0] as Record<string, unknown>)[":version"], 26);
113
113
  assert.equal(typeof (db.runCalls[0][0] as Record<string, unknown>)[":applied_at"], "string");
114
114
  });
115
115
  });
@@ -11,12 +11,14 @@
11
11
  import test from "node:test";
12
12
  import assert from "node:assert/strict";
13
13
  import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
14
+ import { execFileSync } from "node:child_process";
14
15
  import { join } from "node:path";
15
16
  import { tmpdir } from "node:os";
16
17
  import {
17
18
  detectProjectState,
18
19
  detectV1Planning,
19
20
  detectProjectSignals,
21
+ classifyProject,
20
22
  scanProjectFiles,
21
23
  } from "../detection.ts";
22
24
 
@@ -37,6 +39,18 @@ function cleanup(dir: string): void {
37
39
  }
38
40
  }
39
41
 
42
+ function git(dir: string, args: string[]): void {
43
+ execFileSync("git", args, { cwd: dir, stdio: "ignore" });
44
+ }
45
+
46
+ function makeGitRepo(prefix: string): string {
47
+ const dir = makeTempDir(prefix);
48
+ git(dir, ["init"]);
49
+ git(dir, ["config", "user.email", "test@example.com"]);
50
+ git(dir, ["config", "user.name", "Test User"]);
51
+ return dir;
52
+ }
53
+
40
54
  // ─── detectProjectState ─────────────────────────────────────────────────────────
41
55
 
42
56
  test("detectProjectState: empty directory returns state=none", (t) => {
@@ -49,6 +63,132 @@ test("detectProjectState: empty directory returns state=none", (t) => {
49
63
  assert.equal(result.v2, undefined);
50
64
  });
51
65
 
66
+ test("classifyProject: no git repo is invalid", (t) => {
67
+ const dir = makeTempDir("classify-invalid");
68
+ t.after(() => cleanup(dir));
69
+
70
+ const classification = classifyProject(dir);
71
+ assert.equal(classification.kind, "invalid-repo");
72
+ });
73
+
74
+ test("classifyProject: empty git repo is greenfield", (t) => {
75
+ const dir = makeGitRepo("classify-greenfield");
76
+ t.after(() => cleanup(dir));
77
+
78
+ const classification = classifyProject(dir);
79
+ assert.equal(classification.kind, "greenfield");
80
+ });
81
+
82
+ test("classifyProject: nested empty git repo does not inherit ancestor markers", (t) => {
83
+ const parent = makeGitRepo("classify-parent-marker");
84
+ t.after(() => cleanup(parent));
85
+
86
+ writeFileSync(join(parent, "package.json"), JSON.stringify({ name: "parent" }), "utf-8");
87
+ git(parent, ["add", "package.json"]);
88
+ git(parent, ["commit", "-m", "add parent marker"]);
89
+ const child = join(parent, "nested");
90
+ mkdirSync(child, { recursive: true });
91
+ git(child, ["init"]);
92
+ git(child, ["config", "user.email", "test@example.com"]);
93
+ git(child, ["config", "user.name", "Test User"]);
94
+
95
+ const classification = classifyProject(child);
96
+ assert.equal(classification.kind, "greenfield");
97
+ });
98
+
99
+ test("classifyProject: tracked static HTML is existing untyped content", (t) => {
100
+ const dir = makeGitRepo("classify-index");
101
+ t.after(() => cleanup(dir));
102
+
103
+ writeFileSync(join(dir, "index.html"), "<main></main>\n", "utf-8");
104
+ git(dir, ["add", "index.html"]);
105
+ git(dir, ["commit", "-m", "add static page"]);
106
+
107
+ const classification = classifyProject(dir);
108
+ assert.equal(classification.kind, "untyped-existing");
109
+ assert.deepEqual(classification.contentFiles, ["index.html"]);
110
+ });
111
+
112
+ test("classifyProject: README-only repo is existing untyped content", (t) => {
113
+ const dir = makeGitRepo("classify-readme");
114
+ t.after(() => cleanup(dir));
115
+
116
+ writeFileSync(join(dir, "README.md"), "# docs\n", "utf-8");
117
+ git(dir, ["add", "README.md"]);
118
+ git(dir, ["commit", "-m", "add docs"]);
119
+
120
+ const classification = classifyProject(dir);
121
+ assert.equal(classification.kind, "untyped-existing");
122
+ });
123
+
124
+ test("classifyProject: src-only content is untyped existing, not typed marker", (t) => {
125
+ const dir = makeGitRepo("classify-src-only");
126
+ t.after(() => cleanup(dir));
127
+
128
+ mkdirSync(join(dir, "src"), { recursive: true });
129
+ writeFileSync(join(dir, "src", "index.txt"), "content\n", "utf-8");
130
+ git(dir, ["add", "src/index.txt"]);
131
+ git(dir, ["commit", "-m", "add source content"]);
132
+
133
+ const classification = classifyProject(dir);
134
+ assert.equal(classification.kind, "untyped-existing");
135
+ assert.deepEqual(classification.contentFiles, ["src/index.txt"]);
136
+ });
137
+
138
+ test("classifyProject: nested untracked files count as project content", (t) => {
139
+ const dir = makeGitRepo("classify-untracked-nested");
140
+ t.after(() => cleanup(dir));
141
+
142
+ mkdirSync(join(dir, "docs"), { recursive: true });
143
+ writeFileSync(join(dir, "docs", "index.html"), "<main></main>\n", "utf-8");
144
+
145
+ const classification = classifyProject(dir);
146
+ assert.equal(classification.kind, "untyped-existing");
147
+ assert.deepEqual(classification.untrackedFiles, ["docs/index.html"]);
148
+ });
149
+
150
+ test("classifyProject: known markers produce typed existing project", (t) => {
151
+ const dir = makeGitRepo("classify-typed");
152
+ t.after(() => cleanup(dir));
153
+
154
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "typed" }), "utf-8");
155
+ git(dir, ["add", "package.json"]);
156
+ git(dir, ["commit", "-m", "add package"]);
157
+
158
+ const classification = classifyProject(dir);
159
+ assert.equal(classification.kind, "typed-existing");
160
+ assert.ok(classification.markers.includes("package.json"));
161
+ });
162
+
163
+ test("classifyProject: ignored build/cache-only files do not count as content", (t) => {
164
+ const dir = makeGitRepo("classify-ignored");
165
+ t.after(() => cleanup(dir));
166
+
167
+ writeFileSync(join(dir, ".gitignore"), "dist/\n.cache/\n", "utf-8");
168
+ git(dir, ["add", ".gitignore"]);
169
+ git(dir, ["commit", "-m", "ignore generated files"]);
170
+ mkdirSync(join(dir, "dist"), { recursive: true });
171
+ writeFileSync(join(dir, "dist", "bundle.js"), "generated\n", "utf-8");
172
+ mkdirSync(join(dir, ".cache"), { recursive: true });
173
+ writeFileSync(join(dir, ".cache", "x"), "cache\n", "utf-8");
174
+
175
+ const classification = classifyProject(dir);
176
+ assert.equal(classification.kind, "greenfield");
177
+ });
178
+
179
+ test("classifyProject: generated framework/cache dirs do not count as content", (t) => {
180
+ const dir = makeGitRepo("classify-generated-dirs");
181
+ t.after(() => cleanup(dir));
182
+
183
+ mkdirSync(join(dir, ".next", "server"), { recursive: true });
184
+ writeFileSync(join(dir, ".next", "server", "page.js"), "generated\n", "utf-8");
185
+ mkdirSync(join(dir, ".venv", "lib"), { recursive: true });
186
+ writeFileSync(join(dir, ".venv", "lib", "site.py"), "generated\n", "utf-8");
187
+
188
+ const classification = classifyProject(dir);
189
+ assert.equal(classification.kind, "greenfield");
190
+ });
191
+
52
192
  test("detectProjectState: directory with .gsd/milestones/M001 returns v2-gsd", (t) => {
53
193
  const dir = makeTempDir("v2-gsd");
54
194
  t.after(() => cleanup(dir));
@@ -30,6 +30,10 @@ import {
30
30
  insertTask,
31
31
  getTask,
32
32
  getSliceTasks,
33
+ deleteMilestone,
34
+ clearEngineHierarchy,
35
+ recordMilestoneCommitAttribution,
36
+ getMilestoneCommitAttributionShas,
33
37
  checkpointDatabase,
34
38
  refreshOpenDatabaseFromDisk,
35
39
  tryCreateMemoriesFts,
@@ -1047,6 +1051,46 @@ describe('gsd-db', () => {
1047
1051
  });
1048
1052
  });
1049
1053
 
1054
+ // ─── milestone_commit_attributions teardown ───────────────────────────────
1055
+
1056
+ describe('milestone commit attribution teardown', () => {
1057
+ test('deleteMilestone removes persisted milestone commit attributions', () => {
1058
+ openDatabase(':memory:');
1059
+ insertMilestone({ id: 'M001', title: 'Milestone', status: 'active' });
1060
+ recordMilestoneCommitAttribution({
1061
+ commitSha: '0123456789abcdef0123456789abcdef01234567',
1062
+ milestoneId: 'M001',
1063
+ source: 'backfill',
1064
+ confidence: 0.8,
1065
+ files: ['app.js'],
1066
+ createdAt: '2026-05-05T00:00:00.000Z',
1067
+ });
1068
+
1069
+ assert.deepEqual(getMilestoneCommitAttributionShas('M001'), ['0123456789abcdef0123456789abcdef01234567']);
1070
+ deleteMilestone('M001');
1071
+ assert.deepEqual(getMilestoneCommitAttributionShas('M001'), []);
1072
+ closeDatabase();
1073
+ });
1074
+
1075
+ test('clearEngineHierarchy removes persisted milestone commit attributions', () => {
1076
+ openDatabase(':memory:');
1077
+ insertMilestone({ id: 'M001', title: 'Milestone', status: 'active' });
1078
+ recordMilestoneCommitAttribution({
1079
+ commitSha: 'fedcba9876543210fedcba9876543210fedcba98',
1080
+ milestoneId: 'M001',
1081
+ source: 'backfill',
1082
+ confidence: 0.8,
1083
+ files: ['app.js'],
1084
+ createdAt: '2026-05-05T00:00:00.000Z',
1085
+ });
1086
+
1087
+ assert.deepEqual(getMilestoneCommitAttributionShas('M001'), ['fedcba9876543210fedcba9876543210fedcba98']);
1088
+ clearEngineHierarchy();
1089
+ assert.deepEqual(getMilestoneCommitAttributionShas('M001'), []);
1090
+ closeDatabase();
1091
+ });
1092
+ });
1093
+
1050
1094
  // ─── getDbStatus ───────────────────────────────────────────────────────────
1051
1095
 
1052
1096
  describe('getDbStatus', () => {
@@ -15,7 +15,7 @@ import { tmpdir } from "node:os";
15
15
  import { join } from "node:path";
16
16
  import { randomUUID } from "node:crypto";
17
17
 
18
- import { hasPendingDeepStage } from "../auto-dispatch.ts";
18
+ import { hasPendingDeepStage, shouldRunDeepProjectSetup } from "../auto-dispatch.ts";
19
19
  import type { GSDPreferences } from "../preferences.ts";
20
20
  import { loadEffectiveGSDPreferences } from "../preferences.ts";
21
21
 
@@ -57,6 +57,38 @@ test("hasPendingDeepStage: returns true in deep mode when nothing has been captu
57
57
  }
58
58
  });
59
59
 
60
+ test("shouldRunDeepProjectSetup: complete state wins over pending deep setup", async () => {
61
+ const base = makeBase();
62
+ try {
63
+ assert.equal(hasPendingDeepStage(deepPrefs, base), true);
64
+ assert.equal(
65
+ shouldRunDeepProjectSetup({ phase: "complete" }, deepPrefs, base),
66
+ false,
67
+ "completed projects must not restart deep setup and loop through auto-mode",
68
+ );
69
+ } finally {
70
+ rmSync(base, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ test("shouldRunDeepProjectSetup: only setup phases can trigger pending deep setup", () => {
75
+ const base = makeBase();
76
+ try {
77
+ assert.equal(hasPendingDeepStage(deepPrefs, base), true);
78
+ assert.equal(shouldRunDeepProjectSetup({ phase: "pre-planning" }, deepPrefs, base), true);
79
+ assert.equal(shouldRunDeepProjectSetup({ phase: "needs-discussion" }, deepPrefs, base), true);
80
+ assert.equal(shouldRunDeepProjectSetup({ phase: "planning" }, deepPrefs, base), true);
81
+ assert.equal(shouldRunDeepProjectSetup({ phase: "executing" }, deepPrefs, base), false);
82
+ assert.equal(shouldRunDeepProjectSetup({ phase: "blocked" }, deepPrefs, base), false);
83
+ assert.equal(
84
+ shouldRunDeepProjectSetup({ phase: "pre-planning" }, deepPrefs, base, { hasSurvivorBranch: true }),
85
+ false,
86
+ );
87
+ } finally {
88
+ rmSync(base, { recursive: true, force: true });
89
+ }
90
+ });
91
+
60
92
  test("hasPendingDeepStage: returns true in deep mode when only some gates pass", () => {
61
93
  // workflow-preferences captured but PROJECT.md still missing.
62
94
  const base = makeBase();
@@ -13,6 +13,9 @@ import { parseRoadmap, parsePlan } from '../parsers-legacy.ts';
13
13
  import { parseSummary } from '../files.ts';
14
14
  import { deriveState } from '../state.ts';
15
15
  import { invalidateAllCaches } from '../cache.ts';
16
+ import { ensureDbOpen } from '../bootstrap/dynamic-tools.ts';
17
+ import { closeDatabase, getAllMilestones } from '../gsd-db.ts';
18
+ import { importWrittenMigrationToDb } from '../migrate/command.ts';
16
19
  import type {
17
20
  GSDProject,
18
21
  GSDMilestone,
@@ -250,6 +253,51 @@ test('Scenario 1: Incomplete project — write, parse, deriveState', async () =>
250
253
  }
251
254
  });
252
255
 
256
+ test('Scenario 1b: written migration imports into authoritative DB state', async () => {
257
+ const base = mkdtempSync(join(tmpdir(), 'gsd-writer-db-import-'));
258
+ try {
259
+ const project = buildIncompleteProject();
260
+ await writeGSDDirectory(project, base);
261
+
262
+ assert.equal(await ensureDbOpen(base), true, 'db import: ensureDbOpen creates authoritative DB');
263
+
264
+ invalidateAllCaches();
265
+ const before = await deriveState(base);
266
+ assert.equal(before.activeMilestone, null, 'db import: markdown-only migration is invisible before DB import');
267
+
268
+ const imported = await importWrittenMigrationToDb(base);
269
+ assert.deepStrictEqual(imported.hierarchy, { milestones: 1, slices: 2, tasks: 3 }, 'db import: hierarchy counts');
270
+
271
+ invalidateAllCaches();
272
+ const after = await deriveState(base);
273
+ assert.deepStrictEqual(after.phase, 'executing', 'db import: deriveState sees imported DB hierarchy');
274
+ assert.deepStrictEqual(after.activeMilestone?.id, 'M001', 'db import: active milestone');
275
+ assert.deepStrictEqual(after.activeSlice?.id, 'S02', 'db import: active slice');
276
+ assert.deepStrictEqual(after.activeTask?.id, 'T03', 'db import: active task');
277
+ } finally {
278
+ closeDatabase();
279
+ rmSync(base, { recursive: true, force: true });
280
+ }
281
+ });
282
+
283
+ test('Scenario 1c: DB import verification fails when preview counts do not match', async () => {
284
+ const base = mkdtempSync(join(tmpdir(), 'gsd-writer-db-check-'));
285
+ try {
286
+ const project = buildIncompleteProject();
287
+ await writeGSDDirectory(project, base);
288
+
289
+ const preview = generatePreview(project);
290
+ await assert.rejects(
291
+ () => importWrittenMigrationToDb(base, { ...preview, totalTasks: preview.totalTasks + 1 }),
292
+ /migration DB import verification failed: tasks 3\/4/,
293
+ );
294
+ assert.deepStrictEqual(getAllMilestones(), [], 'db import: failed verification rolls back hierarchy rewrite');
295
+ } finally {
296
+ closeDatabase();
297
+ rmSync(base, { recursive: true, force: true });
298
+ }
299
+ });
300
+
253
301
  // ─── Scenario 2: Fully complete project ────────────────────────────────
254
302
 
255
303
  test('Scenario 2: Fully complete project — deriveState phase', async () => {
@@ -0,0 +1,127 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import test from "node:test";
6
+
7
+ import { ensureDbOpen } from "../bootstrap/dynamic-tools.ts";
8
+ import {
9
+ _getAdapter,
10
+ closeDatabase,
11
+ getAllMilestones,
12
+ getSliceTasks,
13
+ } from "../gsd-db.ts";
14
+ import {
15
+ autoImportMarkdownHierarchyIfDbMismatch,
16
+ countMarkdownHierarchy,
17
+ } from "../migration-auto-check.ts";
18
+ import { writeGSDDirectory } from "../migrate/writer.ts";
19
+ import type { GSDProject } from "../migrate/types.ts";
20
+
21
+ function makeBase(): string {
22
+ return mkdtempSync(join(tmpdir(), "gsd-migration-auto-check-"));
23
+ }
24
+
25
+ function cleanup(base: string): void {
26
+ closeDatabase();
27
+ rmSync(base, { recursive: true, force: true });
28
+ }
29
+
30
+ function projectFixture(): GSDProject {
31
+ return {
32
+ projectContent: "# Legacy Project\n",
33
+ decisionsContent: "",
34
+ requirements: [],
35
+ milestones: [
36
+ {
37
+ id: "M001",
38
+ title: "Legacy Milestone",
39
+ vision: "Carry forward previous work",
40
+ successCriteria: ["Existing task is visible"],
41
+ research: null,
42
+ boundaryMap: [],
43
+ slices: [
44
+ {
45
+ id: "S01",
46
+ title: "Legacy Slice",
47
+ risk: "medium",
48
+ depends: [],
49
+ done: false,
50
+ demo: "Legacy slice demo",
51
+ goal: "Legacy slice demo",
52
+ research: null,
53
+ summary: null,
54
+ tasks: [
55
+ {
56
+ id: "T01",
57
+ title: "Legacy Task",
58
+ description: "Task carried from markdown",
59
+ done: false,
60
+ estimate: "",
61
+ files: ["src/index.ts"],
62
+ mustHaves: [],
63
+ summary: null,
64
+ },
65
+ ],
66
+ },
67
+ ],
68
+ },
69
+ ],
70
+ };
71
+ }
72
+
73
+ test("migration auto-check imports markdown hierarchy when DB is empty", async () => {
74
+ const base = makeBase();
75
+ try {
76
+ await writeGSDDirectory(projectFixture(), base);
77
+ assert.deepEqual(countMarkdownHierarchy(base), { milestones: 1, slices: 1, tasks: 1 });
78
+
79
+ assert.equal(await ensureDbOpen(base), true);
80
+ assert.equal(getAllMilestones().length, 0, "fresh authoritative DB starts empty");
81
+
82
+ const result = await autoImportMarkdownHierarchyIfDbMismatch(base);
83
+ assert.equal(result.action, "imported");
84
+ assert.equal(result.reason, "db-empty");
85
+ assert.deepEqual(result.afterDb, { milestones: 1, slices: 1, tasks: 1 });
86
+ assert.equal(getAllMilestones().length, 1);
87
+ assert.equal(getSliceTasks("M001", "S01").length, 1);
88
+ } finally {
89
+ cleanup(base);
90
+ }
91
+ });
92
+
93
+ test("migration auto-check repairs DB hierarchy count mismatch", async () => {
94
+ const base = makeBase();
95
+ try {
96
+ await writeGSDDirectory(projectFixture(), base);
97
+ await autoImportMarkdownHierarchyIfDbMismatch(base);
98
+
99
+ _getAdapter()!.prepare("DELETE FROM tasks WHERE milestone_id = ? AND slice_id = ? AND id = ?").run("M001", "S01", "T01");
100
+ assert.equal(getSliceTasks("M001", "S01").length, 0, "test fixture simulates stale DB task count");
101
+
102
+ const result = await autoImportMarkdownHierarchyIfDbMismatch(base);
103
+ assert.equal(result.action, "imported");
104
+ assert.equal(result.reason, "count-mismatch");
105
+ assert.deepEqual(result.beforeDb, { milestones: 1, slices: 1, tasks: 0 });
106
+ assert.deepEqual(result.afterDb, { milestones: 1, slices: 1, tasks: 1 });
107
+ assert.equal(getSliceTasks("M001", "S01").length, 1);
108
+ } finally {
109
+ cleanup(base);
110
+ }
111
+ });
112
+
113
+ test("migration auto-check leaves matching DB hierarchy alone", async () => {
114
+ const base = makeBase();
115
+ try {
116
+ await writeGSDDirectory(projectFixture(), base);
117
+ await autoImportMarkdownHierarchyIfDbMismatch(base);
118
+
119
+ const result = await autoImportMarkdownHierarchyIfDbMismatch(base);
120
+ assert.equal(result.action, "none");
121
+ assert.equal(result.reason, "in-sync");
122
+ assert.deepEqual(result.markdown, { milestones: 1, slices: 1, tasks: 1 });
123
+ assert.deepEqual(result.afterDb, { milestones: 1, slices: 1, tasks: 1 });
124
+ } finally {
125
+ cleanup(base);
126
+ }
127
+ });
@@ -172,6 +172,56 @@ test('handlePlanSlice rejects invalid payloads', async () => {
172
172
  }
173
173
  });
174
174
 
175
+ test('handlePlanSlice rejects absolute task IO paths outside the active worktree', async () => {
176
+ const base = makeTmpBase();
177
+ openDatabase(join(base, '.gsd', 'gsd.db'));
178
+
179
+ try {
180
+ seedParentSlice();
181
+ const outside = join(tmpdir(), 'outside-checkout', 'index.html');
182
+ const result = await handlePlanSlice({
183
+ ...validParams(),
184
+ tasks: [
185
+ {
186
+ ...validParams().tasks[0],
187
+ inputs: [outside],
188
+ expectedOutput: [outside],
189
+ },
190
+ ],
191
+ }, base);
192
+
193
+ assert.ok('error' in result);
194
+ assert.match(result.error, /validation failed: tasks\[0\]\.inputs contains absolute path outside working directory/);
195
+ assert.equal(getSliceTasks('M001', 'S02').length, 0, 'invalid planning IO must not persist tasks');
196
+ } finally {
197
+ cleanup(base);
198
+ }
199
+ });
200
+
201
+ test('handlePlanSlice accepts absolute task IO paths inside the active worktree', async () => {
202
+ const base = makeTmpBase();
203
+ openDatabase(join(base, '.gsd', 'gsd.db'));
204
+
205
+ try {
206
+ seedParentSlice();
207
+ const inside = join(base, 'index.html');
208
+ const result = await handlePlanSlice({
209
+ ...validParams(),
210
+ tasks: [
211
+ {
212
+ ...validParams().tasks[0],
213
+ inputs: [inside],
214
+ expectedOutput: [inside],
215
+ },
216
+ ],
217
+ }, base);
218
+
219
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
220
+ } finally {
221
+ cleanup(base);
222
+ }
223
+ });
224
+
175
225
  test('handlePlanSlice rejects missing parent slice', async () => {
176
226
  const base = makeTmpBase();
177
227
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -79,6 +79,27 @@ test('handlePlanTask rejects invalid payloads', async () => {
79
79
  }
80
80
  });
81
81
 
82
+ test('handlePlanTask rejects absolute task IO paths outside the active worktree', async () => {
83
+ const base = makeTmpBase();
84
+ openDatabase(join(base, '.gsd', 'gsd.db'));
85
+
86
+ try {
87
+ seedParent();
88
+ const outside = join(tmpdir(), 'outside-checkout', 'index.html');
89
+ const result = await handlePlanTask({
90
+ ...validParams(),
91
+ inputs: [outside],
92
+ expectedOutput: [outside],
93
+ }, base);
94
+
95
+ assert.ok('error' in result);
96
+ assert.match(result.error, /validation failed: inputs contains absolute path outside working directory/);
97
+ assert.equal(getTask('M001', 'S02', 'T02'), null, 'invalid planning IO must not persist the task');
98
+ } finally {
99
+ cleanup(base);
100
+ }
101
+ });
102
+
82
103
  test('handlePlanTask rejects missing parent slice', async () => {
83
104
  const base = makeTmpBase();
84
105
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -0,0 +1,40 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync, readdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname } from "node:path";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const promptsDir = join(__dirname, "..", "prompts");
11
+
12
+ test("prompt templates do not reference legacy milestone-root .gsd paths", () => {
13
+ const offenders: string[] = [];
14
+ for (const file of readdirSync(promptsDir)) {
15
+ if (!file.endsWith(".md")) continue;
16
+ const content = readFileSync(join(promptsDir, file), "utf-8");
17
+ const legacyPatterns = [
18
+ /\.gsd\/\{\{(?:milestoneId|mid)\}\}\//g,
19
+ /\.gsd\/<milestone-id>\//g,
20
+ /\.gsd\/<ID>\//g,
21
+ ];
22
+ for (const pattern of legacyPatterns) {
23
+ if (pattern.test(content)) {
24
+ offenders.push(`${file}: ${pattern.source}`);
25
+ }
26
+ }
27
+ }
28
+
29
+ assert.deepEqual(
30
+ offenders,
31
+ [],
32
+ "Milestone artifacts must use .gsd/milestones/<MID>/..., not legacy .gsd/<MID>/...",
33
+ );
34
+ });
35
+
36
+ test("quick task prompt delegates commit policy to quick.ts", () => {
37
+ const content = readFileSync(join(promptsDir, "quick-task.md"), "utf-8");
38
+ assert.match(content, /\{\{commitInstruction\}\}/);
39
+ assert.doesNotMatch(content, /Stage only relevant files/);
40
+ });
@@ -0,0 +1,40 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { buildQuickCommitInstruction } from "../quick.ts";
8
+
9
+ function git(cwd: string, args: string[]): void {
10
+ const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
11
+ assert.equal(result.status, 0, result.stderr || result.stdout);
12
+ }
13
+
14
+ test("quick task commit instruction does not ask agents to stage external .gsd quick files", { skip: process.platform === "win32" }, () => {
15
+ const tempRoot = realpathSync(mkdtempSync(join(tmpdir(), "gsd-quick-ext-")));
16
+ const repo = join(tempRoot, "repo");
17
+ const externalGsd = join(tempRoot, "state");
18
+ mkdirSync(repo);
19
+ mkdirSync(externalGsd);
20
+
21
+ const previousCwd = process.cwd();
22
+ try {
23
+ git(repo, ["init"]);
24
+ git(repo, ["config", "user.email", "test@example.com"]);
25
+ git(repo, ["config", "user.name", "Test User"]);
26
+ writeFileSync(join(repo, "README.md"), "# Test\n", "utf-8");
27
+ git(repo, ["add", "README.md"]);
28
+ git(repo, ["commit", "-m", "init"]);
29
+ symlinkSync(externalGsd, join(repo, ".gsd"), "dir");
30
+
31
+ const instruction = buildQuickCommitInstruction(repo, join(repo, ".gsd"));
32
+
33
+ assert.match(instruction, /do not stage or commit `\.gsd\/quick\/\.\.\.`/);
34
+ assert.match(instruction, /nothing in the project repo to commit/);
35
+ assert.match(instruction, /Write the quick summary file directly/);
36
+ } finally {
37
+ process.chdir(previousCwd);
38
+ rmSync(tempRoot, { recursive: true, force: true });
39
+ }
40
+ });