gsd-pi 2.80.0-dev.8b3a59f5c → 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 (222) 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-start.js +2 -3
  9. package/dist/resources/extensions/gsd/auto.js +16 -3
  10. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
  11. package/dist/resources/extensions/gsd/clean-root-preflight.js +42 -4
  12. package/dist/resources/extensions/gsd/commands/dispatcher.js +5 -0
  13. package/dist/resources/extensions/gsd/crash-recovery.js +56 -10
  14. package/dist/resources/extensions/gsd/detection.js +106 -0
  15. package/dist/resources/extensions/gsd/guided-flow.js +63 -12
  16. package/dist/resources/extensions/gsd/migrate/command.js +40 -1
  17. package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
  18. package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
  19. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
  20. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  21. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  22. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  23. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  24. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  25. package/dist/resources/extensions/gsd/quick.js +34 -2
  26. package/dist/resources/extensions/gsd/safety/evidence-collector.js +10 -2
  27. package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
  28. package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
  29. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  30. package/dist/resources/extensions/gsd/worktree-manager.js +16 -14
  31. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  32. package/dist/web/standalone/.next/BUILD_ID +1 -1
  33. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  34. package/dist/web/standalone/.next/build-manifest.json +2 -2
  35. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  36. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.html +1 -1
  53. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  60. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  61. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  62. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  63. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  64. package/package.json +4 -2
  65. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/agent-session.js +3 -0
  67. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  68. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +32 -0
  69. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  71. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  72. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  73. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  74. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  75. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  76. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  77. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  78. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
  79. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
  80. package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
  81. package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
  82. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
  83. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
  84. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
  85. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
  86. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +3 -0
  87. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/extensions/runner.js +17 -1
  89. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +99 -0
  91. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  93. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  96. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  98. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.js +6 -4
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +25 -20
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js +2 -2
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js.map +1 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +2 -1
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.js +7 -6
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +26 -13
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  117. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +15 -5
  118. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  120. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  121. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  123. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +36 -27
  126. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js +21 -0
  128. package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
  130. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
  132. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
  133. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
  134. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
  135. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
  136. package/packages/pi-coding-agent/src/core/agent-session.ts +3 -0
  137. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +38 -0
  138. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  139. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  140. package/packages/pi-coding-agent/src/core/db-snapshot.test.ts +32 -0
  141. package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
  142. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +110 -0
  143. package/packages/pi-coding-agent/src/core/extensions/runner.ts +19 -1
  144. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  145. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  146. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.ts +7 -5
  147. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +25 -20
  148. package/packages/pi-coding-agent/src/modes/interactive/components/adaptive-layout.ts +2 -2
  149. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +1 -0
  150. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +2 -1
  151. package/packages/pi-coding-agent/src/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.ts +10 -9
  152. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +33 -13
  153. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  154. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +24 -7
  155. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  156. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +36 -27
  157. package/packages/pi-coding-agent/src/modes/interactive/tui-mode.test.ts +25 -0
  158. package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
  159. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
  160. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  161. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  162. package/packages/pi-tui/dist/tui.js +17 -7
  163. package/packages/pi-tui/dist/tui.js.map +1 -1
  164. package/packages/pi-tui/src/tui.ts +19 -7
  165. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  166. package/pkg/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  167. package/pkg/dist/modes/interactive/theme/themes.js +36 -27
  168. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  169. package/src/resources/GSD-WORKFLOW.md +2 -2
  170. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  171. package/src/resources/extensions/gsd/auto/loop.ts +50 -8
  172. package/src/resources/extensions/gsd/auto/phases.ts +42 -28
  173. package/src/resources/extensions/gsd/auto-dispatch.ts +17 -0
  174. package/src/resources/extensions/gsd/auto-post-unit.ts +10 -10
  175. package/src/resources/extensions/gsd/auto-prompts.ts +116 -1
  176. package/src/resources/extensions/gsd/auto-start.ts +7 -6
  177. package/src/resources/extensions/gsd/auto.ts +23 -3
  178. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  179. package/src/resources/extensions/gsd/clean-root-preflight.ts +41 -3
  180. package/src/resources/extensions/gsd/commands/dispatcher.ts +6 -0
  181. package/src/resources/extensions/gsd/crash-recovery.ts +67 -10
  182. package/src/resources/extensions/gsd/detection.ts +128 -0
  183. package/src/resources/extensions/gsd/guided-flow.ts +66 -12
  184. package/src/resources/extensions/gsd/migrate/command.ts +47 -1
  185. package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
  186. package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
  187. package/src/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
  188. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  189. package/src/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  190. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  191. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  192. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  193. package/src/resources/extensions/gsd/quick.ts +37 -2
  194. package/src/resources/extensions/gsd/safety/evidence-collector.ts +11 -2
  195. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1 -1
  196. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +88 -2
  197. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +55 -0
  198. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +22 -0
  199. package/src/resources/extensions/gsd/tests/detection.test.ts +140 -0
  200. package/src/resources/extensions/gsd/tests/has-pending-deep-stage.test.ts +33 -1
  201. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
  202. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
  203. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
  204. package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
  205. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  206. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  207. package/src/resources/extensions/gsd/tests/right-sized-workflow-prompts.test.ts +192 -0
  208. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +29 -0
  209. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +38 -0
  210. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +63 -2
  211. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  212. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +7 -0
  213. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +1 -1
  214. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +37 -6
  215. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +7 -0
  216. package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +9 -2
  217. package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
  218. package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
  219. package/src/resources/extensions/gsd/unit-runtime.ts +14 -0
  220. package/src/resources/extensions/gsd/worktree-manager.ts +15 -4
  221. /package/dist/web/standalone/.next/static/{k-MDjzPMF62Rg1-FR437h → FU0iL9dhdc2IBzEYBDOlY}/_buildManifest.js +0 -0
  222. /package/dist/web/standalone/.next/static/{k-MDjzPMF62Rg1-FR437h → FU0iL9dhdc2IBzEYBDOlY}/_ssgManifest.js +0 -0
@@ -38,6 +38,7 @@ import { _getAdapter, isDbAvailable } from "./gsd-db.js";
38
38
  import { gsdRoot, normalizeRealPath } from "./paths.js";
39
39
  import { atomicWriteSync } from "./atomic-write.js";
40
40
  import { effectiveLockFile } from "./session-lock.js";
41
+ import { isInFlightRuntimePhase, listUnitRuntimeRecords, type AutoUnitRuntimeRecord } from "./unit-runtime.js";
41
42
 
42
43
  export interface LockData {
43
44
  pid: number;
@@ -103,10 +104,40 @@ function getLatestDispatchForWorker(workerId: string):
103
104
  return row ?? null;
104
105
  }
105
106
 
106
- function workerToLockData(worker: AutoWorkerRow): LockData {
107
+ function latestInFlightRuntimeRecord(basePath: string): AutoUnitRuntimeRecord | null {
108
+ const records = listUnitRuntimeRecords(basePath).filter((record) =>
109
+ isInFlightRuntimePhase(record.phase),
110
+ );
111
+ if (records.length === 0) return null;
112
+ return records.sort((a, b) => {
113
+ const bTime = b.updatedAt || b.startedAt || 0;
114
+ const aTime = a.updatedAt || a.startedAt || 0;
115
+ return bTime - aTime;
116
+ })[0] ?? null;
117
+ }
118
+
119
+ function runtimeRecordToLockData(worker: AutoWorkerRow, record: AutoUnitRuntimeRecord, sessionFile?: string): LockData {
120
+ const startedAt = Number.isFinite(record.startedAt)
121
+ ? new Date(record.startedAt).toISOString()
122
+ : worker.started_at;
123
+ return {
124
+ pid: worker.pid,
125
+ startedAt: worker.started_at,
126
+ unitType: record.unitType,
127
+ unitId: record.unitId,
128
+ unitStartedAt: startedAt,
129
+ sessionFile,
130
+ };
131
+ }
132
+
133
+ function workerToLockData(basePath: string, worker: AutoWorkerRow): LockData {
107
134
  const dispatch = getLatestDispatchForWorker(worker.worker_id);
108
135
  const sessionFile =
109
136
  getRuntimeKv<string>("worker", worker.worker_id, SESSION_FILE_KV_KEY) ?? undefined;
137
+ if (!dispatch) {
138
+ const runtimeRecord = latestInFlightRuntimeRecord(basePath);
139
+ if (runtimeRecord) return runtimeRecordToLockData(worker, runtimeRecord, sessionFile);
140
+ }
110
141
  return {
111
142
  pid: worker.pid,
112
143
  startedAt: worker.started_at,
@@ -204,7 +235,7 @@ export function readCrashLock(basePath: string): LockData | null {
204
235
  try {
205
236
  const projectRoot = normalizeRealPath(basePath);
206
237
  const stale = findStaleWorkerForProject(projectRoot);
207
- if (stale) return workerToLockData(stale);
238
+ if (stale) return workerToLockData(basePath, stale);
208
239
  } catch {
209
240
  // Fall through to the legacy lock-file compatibility path.
210
241
  }
@@ -260,25 +291,48 @@ export function formatCrashInfo(lock: LockData): string {
260
291
  */
261
292
  export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): void {
262
293
  if (!lock.unitType || !lock.unitId || lock.unitType === "starting") return;
294
+ emitOpenUnitEndForUnit(basePath, lock.unitType, lock.unitId, "crash-recovered");
295
+ }
263
296
 
297
+ export function emitOpenUnitEndForUnit(
298
+ basePath: string,
299
+ unitType: string,
300
+ unitId: string,
301
+ status: string,
302
+ errorContext?: { message: string; category: string; stopReason?: string; isTransient?: boolean; retryAfterMs?: number },
303
+ ): boolean {
264
304
  try {
265
305
  const all = queryJournal(basePath);
266
306
 
267
307
  const starts = all.filter(
268
- (e) => e.eventType === "unit-start" && e.data?.unitId === lock.unitId,
308
+ (e) =>
309
+ e.eventType === "unit-start" &&
310
+ e.data?.unitType === unitType &&
311
+ e.data?.unitId === unitId,
269
312
  );
270
- if (starts.length === 0) return;
313
+ if (starts.length === 0) return false;
271
314
 
272
- const lastStart = starts[starts.length - 1];
315
+ const lastStart = [...starts].reverse().find((start) => {
316
+ return !all.some(
317
+ (e) =>
318
+ e.eventType === "unit-end" &&
319
+ e.data?.unitType === unitType &&
320
+ e.data?.unitId === unitId &&
321
+ e.causedBy?.flowId === start.flowId &&
322
+ e.causedBy?.seq === start.seq,
323
+ );
324
+ });
325
+ if (!lastStart) return false;
273
326
 
274
327
  const alreadyClosed = all.some(
275
328
  (e) =>
276
329
  e.eventType === "unit-end" &&
277
- e.data?.unitId === lock.unitId &&
330
+ e.data?.unitType === unitType &&
331
+ e.data?.unitId === unitId &&
278
332
  e.causedBy?.flowId === lastStart.flowId &&
279
333
  e.causedBy?.seq === lastStart.seq,
280
334
  );
281
- if (alreadyClosed) return;
335
+ if (alreadyClosed) return false;
282
336
 
283
337
  const maxSeq = all
284
338
  .filter((e) => e.flowId === lastStart.flowId)
@@ -290,15 +344,18 @@ export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): voi
290
344
  seq: maxSeq + 1,
291
345
  eventType: "unit-end",
292
346
  data: {
293
- unitType: lock.unitType,
294
- unitId: lock.unitId,
295
- status: "crash-recovered",
347
+ unitType,
348
+ unitId,
349
+ status,
296
350
  artifactVerified: false,
351
+ ...(errorContext ? { errorContext } : {}),
297
352
  },
298
353
  causedBy: { flowId: lastStart.flowId, seq: lastStart.seq },
299
354
  });
355
+ return true;
300
356
  } catch {
301
357
  // Never throw from crash recovery path.
358
+ return false;
302
359
  }
303
360
  }
304
361
 
@@ -6,6 +6,7 @@
6
6
  * flow to show when entering a project directory.
7
7
  */
8
8
 
9
+ import { execFileSync } from "node:child_process";
9
10
  import { existsSync, openSync, readSync, closeSync, readdirSync, readFileSync, statSync } from "node:fs";
10
11
  import { dirname, join, parse as parsePath } from "node:path";
11
12
  import { homedir } from "node:os";
@@ -72,6 +73,22 @@ export interface ProjectSignals {
72
73
  verificationCommands: string[];
73
74
  }
74
75
 
76
+ export type ProjectClassificationKind =
77
+ | "invalid-repo"
78
+ | "greenfield"
79
+ | "untyped-existing"
80
+ | "typed-existing";
81
+
82
+ export interface ProjectClassification {
83
+ kind: ProjectClassificationKind;
84
+ signals: ProjectSignals;
85
+ trackedFiles: string[];
86
+ untrackedFiles: string[];
87
+ contentFiles: string[];
88
+ markers: string[];
89
+ reason: string;
90
+ }
91
+
75
92
  // ─── Project File Markers ───────────────────────────────────────────────────────
76
93
 
77
94
  export const PROJECT_FILES = [
@@ -243,6 +260,7 @@ const TEST_MARKERS = [
243
260
  const RECURSIVE_SCAN_IGNORED_DIRS = new Set([
244
261
  ".git",
245
262
  ".gsd",
263
+ ".bg-shell",
246
264
  ".planning",
247
265
  ".plans",
248
266
  ".claude",
@@ -267,6 +285,8 @@ const RECURSIVE_SCAN_IGNORED_DIRS = new Set([
267
285
  "out",
268
286
  ]) as ReadonlySet<string>;
269
287
 
288
+ const PROJECT_CONTENT_EXCLUDE_DIRS = RECURSIVE_SCAN_IGNORED_DIRS;
289
+
270
290
  /** Project file markers safe to detect recursively via suffix matching. */
271
291
  const ROOT_ONLY_PROJECT_FILES = new Set<string>([
272
292
  ".github/workflows",
@@ -536,6 +556,114 @@ export function detectProjectSignals(basePath: string): ProjectSignals {
536
556
  };
537
557
  }
538
558
 
559
+ function normalizeGitPath(file: string): string {
560
+ return file.replaceAll("\\", "/").replace(/^\.\//, "");
561
+ }
562
+
563
+ function isProjectContentFile(file: string): boolean {
564
+ const normalized = normalizeGitPath(file);
565
+ if (!normalized || normalized.endsWith("/")) return false;
566
+ if (normalized === ".gitignore" || normalized === ".gitattributes") return false;
567
+ const parts = normalized.split("/");
568
+ if (parts.some((part) => PROJECT_CONTENT_EXCLUDE_DIRS.has(part))) return false;
569
+ if (normalized.endsWith(".DS_Store")) return false;
570
+ return true;
571
+ }
572
+
573
+ function runGitLines(basePath: string, args: string[]): string[] {
574
+ try {
575
+ const output = execFileSync("git", args, {
576
+ cwd: basePath,
577
+ stdio: ["ignore", "pipe", "ignore"],
578
+ encoding: "utf-8",
579
+ }).trim();
580
+ return output ? output.split("\n").map((line) => line.trim()).filter(Boolean) : [];
581
+ } catch {
582
+ return [];
583
+ }
584
+ }
585
+
586
+ function listTrackedProjectFiles(basePath: string): string[] {
587
+ return runGitLines(basePath, ["ls-files"])
588
+ .map(normalizeGitPath)
589
+ .filter(isProjectContentFile);
590
+ }
591
+
592
+ function listUntrackedProjectFiles(basePath: string): string[] {
593
+ return runGitLines(basePath, ["ls-files", "--others", "--exclude-standard"])
594
+ .map(normalizeGitPath)
595
+ .filter(isProjectContentFile);
596
+ }
597
+
598
+ function hasKnownProjectMarkers(basePath: string, signals: ProjectSignals): boolean {
599
+ if (signals.detectedFiles.length > 0) return true;
600
+ if (signals.xcodePlatforms.length > 0) return true;
601
+ return false;
602
+ }
603
+
604
+ /**
605
+ * Classify repo presence separately from ecosystem/tooling markers.
606
+ *
607
+ * Known project files identify tooling. Git-tracked/non-ignored content
608
+ * identifies whether this is an existing project at all. This keeps small
609
+ * static or documentation repos from being mislabeled as greenfield.
610
+ */
611
+ export function classifyProject(basePath: string): ProjectClassification {
612
+ const signals = detectProjectSignals(basePath);
613
+ const markers = [...signals.detectedFiles];
614
+
615
+ if (!signals.isGitRepo) {
616
+ return {
617
+ kind: "invalid-repo",
618
+ signals,
619
+ trackedFiles: [],
620
+ untrackedFiles: [],
621
+ contentFiles: [],
622
+ markers,
623
+ reason: "missing .git",
624
+ };
625
+ }
626
+
627
+ const trackedFiles = listTrackedProjectFiles(basePath);
628
+ const untrackedFiles = listUntrackedProjectFiles(basePath);
629
+ const contentFiles = [...new Set([...trackedFiles, ...untrackedFiles])];
630
+ const hasMarkers = hasKnownProjectMarkers(basePath, signals);
631
+
632
+ if (hasMarkers) {
633
+ return {
634
+ kind: "typed-existing",
635
+ signals,
636
+ trackedFiles,
637
+ untrackedFiles,
638
+ contentFiles,
639
+ markers,
640
+ reason: markers.length > 0 ? `detected markers: ${markers.join(", ")}` : "detected project structure",
641
+ };
642
+ }
643
+
644
+ if (contentFiles.length > 0) {
645
+ return {
646
+ kind: "untyped-existing",
647
+ signals,
648
+ trackedFiles,
649
+ untrackedFiles,
650
+ contentFiles,
651
+ markers,
652
+ reason: "project content exists but no recognized tooling markers were found",
653
+ };
654
+ }
655
+
656
+ return {
657
+ kind: "greenfield",
658
+ signals,
659
+ trackedFiles,
660
+ untrackedFiles,
661
+ contentFiles,
662
+ markers,
663
+ reason: "no tracked or non-ignored project content",
664
+ };
665
+ }
666
+
539
667
  // ─── Xcode Platform Detection ───────────────────────────────────────────────────
540
668
 
541
669
  /** Known SDKROOT values → canonical platform names. */
@@ -30,7 +30,7 @@ import {
30
30
  formatInterruptedSessionRunningMessage,
31
31
  formatInterruptedSessionSummary,
32
32
  } from "./interrupted-session.js";
33
- import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
33
+ import { listUnitRuntimeRecords, clearUnitRuntimeRecord, isInFlightRuntimePhase } from "./unit-runtime.js";
34
34
  import { resolveExpectedArtifactPath } from "./auto.js";
35
35
  import { gsdHome } from "./gsd-home.js";
36
36
  import {
@@ -118,6 +118,22 @@ export function resolveExpectedArtifactPathForScope(
118
118
  return resolveExpectedArtifactPath(unitType, unitId, scope.workspace.projectRoot);
119
119
  }
120
120
 
121
+ async function runQuickTaskChoice(ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
122
+ if (!ctx.hasUI) {
123
+ ctx.ui.notify("Run /gsd quick <task> for small bounded work, or /gsd do <task> for natural-language routing.", "info");
124
+ return;
125
+ }
126
+
127
+ const task = (await ctx.ui.input("Quick task", "Describe the small task to run with /gsd quick"))?.trim();
128
+ if (!task) {
129
+ ctx.ui.notify("Quick task cancelled.", "info");
130
+ return;
131
+ }
132
+
133
+ const { handleQuick } = await import("./quick.js");
134
+ await handleQuick(task, ctx, pi);
135
+ }
136
+
121
137
  /**
122
138
  * Scope-based overload of isGhostMilestone.
123
139
  * Binds basePath and milestoneId from the scope, ensuring path resolution
@@ -1786,8 +1802,8 @@ function selfHealRuntimeRecords(basePath: string, ctx: ExtensionContext): { clea
1786
1802
  cleared++;
1787
1803
  continue;
1788
1804
  }
1789
- // Clear records stuck in dispatched or timeout phase (process died mid-unit)
1790
- if (phase === "dispatched" || phase === "timeout") {
1805
+ // Clear records stuck in an in-flight phase (process died mid-unit).
1806
+ if (isInFlightRuntimePhase(phase)) {
1791
1807
  clearUnitRuntimeRecord(basePath, unitType, unitId);
1792
1808
  cleared++;
1793
1809
  }
@@ -2042,6 +2058,23 @@ export async function showSmartEntry(
2042
2058
  }
2043
2059
  }
2044
2060
 
2061
+ if (interrupted.classification !== "recoverable") {
2062
+ try {
2063
+ const { autoImportMarkdownHierarchyIfDbMismatch } = await import("./migration-auto-check.js");
2064
+ const result = await autoImportMarkdownHierarchyIfDbMismatch(basePath);
2065
+ if (result.action === "imported") {
2066
+ ctx.ui.notify(
2067
+ `Recovered migrated planning state into gsd.db (${result.reason}): ${result.afterDb.milestones} milestone(s), ${result.afterDb.slices} slice(s), ${result.afterDb.tasks} task(s).`,
2068
+ "info",
2069
+ );
2070
+ }
2071
+ } catch (err) {
2072
+ const message = err instanceof Error ? err.message : String(err);
2073
+ ctx.ui.notify(`GSD could not auto-import existing planning state into gsd.db: ${message}`, "warning");
2074
+ logWarning("guided", `planning state auto-import failed: ${message}`, { file: "guided-flow.ts" });
2075
+ }
2076
+ }
2077
+
2045
2078
  // Always derive from the project root — the assessment may have derived
2046
2079
  // state from a worktree path that was cleaned up in the stale branch above.
2047
2080
  const state = await deriveState(basePath);
@@ -2064,8 +2097,8 @@ export async function showSmartEntry(
2064
2097
  // standard wizard below.
2065
2098
  {
2066
2099
  const prefs = loadEffectiveGSDPreferences(basePath)?.preferences;
2067
- const { hasPendingDeepStage } = await import("./auto-dispatch.js");
2068
- if (hasPendingDeepStage(prefs, basePath)) {
2100
+ const { shouldRunDeepProjectSetup } = await import("./auto-dispatch.js");
2101
+ if (shouldRunDeepProjectSetup(state, prefs, basePath)) {
2069
2102
  await startDeepProjectSetupForeground(ctx, pi, basePath, stepMode);
2070
2103
  return;
2071
2104
  }
@@ -2134,17 +2167,24 @@ export async function showSmartEntry(
2134
2167
  title: "GSD — Get Shit Done",
2135
2168
  summary: ["No active milestone."],
2136
2169
  actions: [
2170
+ {
2171
+ id: "quick_task",
2172
+ label: "Quick task",
2173
+ description: "For small bounded work, run /gsd quick <task> or /gsd do <task>.",
2174
+ recommended: true,
2175
+ },
2137
2176
  {
2138
2177
  id: "new_milestone",
2139
2178
  label: "Create next milestone",
2140
- description: "Define what to build next.",
2141
- recommended: true,
2179
+ description: "Define a larger body of work with planning artifacts.",
2142
2180
  },
2143
2181
  ],
2144
2182
  notYetMessage: "Run /gsd when ready.",
2145
2183
  });
2146
2184
 
2147
- if (choice === "new_milestone") {
2185
+ if (choice === "quick_task") {
2186
+ await runQuickTaskChoice(ctx, pi);
2187
+ } else if (choice === "new_milestone") {
2148
2188
  setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
2149
2189
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
2150
2190
  `New milestone ${nextId}.`,
@@ -2181,11 +2221,16 @@ export async function showSmartEntry(
2181
2221
  title: `GSD — ${milestoneId}: ${milestoneTitle}`,
2182
2222
  summary: ["All milestones complete."],
2183
2223
  actions: [
2224
+ {
2225
+ id: "quick_task",
2226
+ label: "Quick task",
2227
+ description: "Do a small bounded task without opening a milestone.",
2228
+ recommended: true,
2229
+ },
2184
2230
  {
2185
2231
  id: "new_milestone",
2186
2232
  label: "Start new milestone",
2187
2233
  description: "Define and plan the next milestone.",
2188
- recommended: true,
2189
2234
  },
2190
2235
  {
2191
2236
  id: "status",
@@ -2196,7 +2241,9 @@ export async function showSmartEntry(
2196
2241
  notYetMessage: "Run /gsd when ready.",
2197
2242
  });
2198
2243
 
2199
- if (choice === "new_milestone") {
2244
+ if (choice === "quick_task") {
2245
+ await runQuickTaskChoice(ctx, pi);
2246
+ } else if (choice === "new_milestone") {
2200
2247
  const milestoneIds = findMilestoneIds(basePath);
2201
2248
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
2202
2249
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds, basePath);
@@ -2300,13 +2347,18 @@ export async function showSmartEntry(
2300
2347
  const hasContext = !!(contextFile && await loadFile(contextFile));
2301
2348
 
2302
2349
  const actions = [
2350
+ {
2351
+ id: "quick_task",
2352
+ label: "Quick task instead",
2353
+ description: "Use this when the work is small and should not become a milestone.",
2354
+ recommended: true,
2355
+ },
2303
2356
  {
2304
2357
  id: "plan",
2305
2358
  label: "Create roadmap",
2306
2359
  description: hasContext
2307
2360
  ? "Context captured. Decompose into slices with a boundary map."
2308
2361
  : "Decompose the milestone into slices with a boundary map.",
2309
- recommended: true,
2310
2362
  },
2311
2363
  ...(!hasContext ? [{
2312
2364
  id: "discuss",
@@ -2332,7 +2384,9 @@ export async function showSmartEntry(
2332
2384
  notYetMessage: "Run /gsd when ready.",
2333
2385
  });
2334
2386
 
2335
- if (choice === "plan") {
2387
+ if (choice === "quick_task") {
2388
+ await runQuickTaskChoice(ctx, pi);
2389
+ } else if (choice === "plan") {
2336
2390
  setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
2337
2391
  await dispatchWorkflow(
2338
2392
  pi,
@@ -25,6 +25,50 @@ import {
25
25
 
26
26
  import type { MigrationPreview } from "./writer.js";
27
27
  import { homedir } from "node:os";
28
+ import { ensureDbOpen } from "../bootstrap/dynamic-tools.js";
29
+ import { clearEngineHierarchy, transaction } from "../gsd-db.js";
30
+ import { migrateFromMarkdown } from "../md-importer.js";
31
+ import { invalidateStateCache } from "../state.js";
32
+
33
+ export type MigrationImportCounts = ReturnType<typeof migrateFromMarkdown>;
34
+
35
+ function assertMigrationImportMatchesPreview(imported: MigrationImportCounts, preview: MigrationPreview): void {
36
+ const mismatches: string[] = [];
37
+ if (imported.hierarchy.milestones !== preview.milestoneCount) {
38
+ mismatches.push(`milestones ${imported.hierarchy.milestones}/${preview.milestoneCount}`);
39
+ }
40
+ if (imported.hierarchy.slices !== preview.totalSlices) {
41
+ mismatches.push(`slices ${imported.hierarchy.slices}/${preview.totalSlices}`);
42
+ }
43
+ if (imported.hierarchy.tasks !== preview.totalTasks) {
44
+ mismatches.push(`tasks ${imported.hierarchy.tasks}/${preview.totalTasks}`);
45
+ }
46
+ if (imported.requirements !== preview.requirements.total) {
47
+ mismatches.push(`requirements ${imported.requirements}/${preview.requirements.total}`);
48
+ }
49
+ if (mismatches.length > 0) {
50
+ throw new Error(`migration DB import verification failed: ${mismatches.join(", ")}`);
51
+ }
52
+ }
53
+
54
+ export async function importWrittenMigrationToDb(
55
+ basePath: string,
56
+ preview?: MigrationPreview,
57
+ ): Promise<MigrationImportCounts> {
58
+ const opened = await ensureDbOpen(basePath);
59
+ if (!opened) {
60
+ throw new Error(`failed to open or create the GSD database at ${basePath}`);
61
+ }
62
+
63
+ const counts = transaction(() => {
64
+ clearEngineHierarchy();
65
+ const imported = migrateFromMarkdown(basePath);
66
+ if (preview) assertMigrationImportMatchesPreview(imported, preview);
67
+ return imported;
68
+ });
69
+ invalidateStateCache();
70
+ return counts;
71
+ }
28
72
 
29
73
  /** Format preview stats for embedding in the review prompt. */
30
74
  function formatPreviewStats(preview: MigrationPreview): string {
@@ -182,9 +226,10 @@ export async function handleMigrate(
182
226
 
183
227
  const result = await writeGSDDirectory(project, process.cwd());
184
228
  const gsdPath = gsdRoot(process.cwd());
229
+ const imported = await importWrittenMigrationToDb(process.cwd(), preview);
185
230
 
186
231
  ctx.ui.notify(
187
- `✓ Migration complete — ${result.paths.length} file(s) written to .gsd/`,
232
+ `✓ Migration complete — ${result.paths.length} file(s) written to .gsd/ and ${imported.hierarchy.milestones}M/${imported.hierarchy.slices}S/${imported.hierarchy.tasks}T imported to the database`,
188
233
  "info",
189
234
  );
190
235
 
@@ -193,6 +238,7 @@ export async function handleMigrate(
193
238
  title: "Migration written",
194
239
  summary: [
195
240
  `${result.paths.length} files written to .gsd/`,
241
+ `${imported.hierarchy.milestones} milestone(s), ${imported.hierarchy.slices} slice(s), and ${imported.hierarchy.tasks} task(s) imported to gsd.db`,
196
242
  "",
197
243
  "The agent can now review the migrated output against GSD-2 standards —",
198
244
  "checking structure, content quality, deriveState() round-trip, and",
@@ -0,0 +1,129 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+
3
+ import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
4
+ import {
5
+ clearEngineHierarchy,
6
+ getAllMilestones,
7
+ getMilestoneSlices,
8
+ getSliceTasks,
9
+ isDbAvailable,
10
+ transaction,
11
+ } from "./gsd-db.js";
12
+ import { migrateHierarchyToDb } from "./md-importer.js";
13
+ import { parsePlan, parseRoadmap } from "./parsers-legacy.js";
14
+ import {
15
+ milestonesDir,
16
+ resolveMilestoneFile,
17
+ resolveSliceFile,
18
+ } from "./paths.js";
19
+ import { invalidateStateCache } from "./state.js";
20
+
21
+ export interface HierarchyCounts {
22
+ milestones: number;
23
+ slices: number;
24
+ tasks: number;
25
+ }
26
+
27
+ export interface MigrationAutoCheckResult {
28
+ action: "none" | "imported";
29
+ reason: "no-markdown" | "in-sync" | "db-empty" | "count-mismatch";
30
+ markdown: HierarchyCounts;
31
+ beforeDb: HierarchyCounts;
32
+ afterDb: HierarchyCounts;
33
+ }
34
+
35
+ function zeroCounts(): HierarchyCounts {
36
+ return { milestones: 0, slices: 0, tasks: 0 };
37
+ }
38
+
39
+ function sameCounts(a: HierarchyCounts, b: HierarchyCounts): boolean {
40
+ return a.milestones === b.milestones && a.slices === b.slices && a.tasks === b.tasks;
41
+ }
42
+
43
+ export function countMarkdownHierarchy(basePath: string): HierarchyCounts {
44
+ const root = milestonesDir(basePath);
45
+ if (!existsSync(root)) return zeroCounts();
46
+
47
+ const counts = zeroCounts();
48
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
49
+ if (!entry.isDirectory() || !/^M\d+/.test(entry.name)) continue;
50
+ counts.milestones++;
51
+
52
+ const roadmapPath = resolveMilestoneFile(basePath, entry.name, "ROADMAP");
53
+ if (!roadmapPath || !existsSync(roadmapPath)) continue;
54
+
55
+ const roadmap = parseRoadmap(readFileSync(roadmapPath, "utf-8"));
56
+ counts.slices += roadmap.slices.length;
57
+
58
+ for (const slice of roadmap.slices) {
59
+ const planPath = resolveSliceFile(basePath, entry.name, slice.id, "PLAN");
60
+ if (!planPath || !existsSync(planPath)) continue;
61
+ const plan = parsePlan(readFileSync(planPath, "utf-8"));
62
+ counts.tasks += plan.tasks.length;
63
+ }
64
+ }
65
+
66
+ return counts;
67
+ }
68
+
69
+ export function countDbHierarchy(): HierarchyCounts {
70
+ if (!isDbAvailable()) return zeroCounts();
71
+ const counts = zeroCounts();
72
+ const milestones = getAllMilestones();
73
+ counts.milestones = milestones.length;
74
+
75
+ for (const milestone of milestones) {
76
+ const slices = getMilestoneSlices(milestone.id);
77
+ counts.slices += slices.length;
78
+ for (const slice of slices) {
79
+ counts.tasks += getSliceTasks(milestone.id, slice.id).length;
80
+ }
81
+ }
82
+
83
+ return counts;
84
+ }
85
+
86
+ export async function autoImportMarkdownHierarchyIfDbMismatch(
87
+ basePath: string,
88
+ ): Promise<MigrationAutoCheckResult> {
89
+ const markdown = countMarkdownHierarchy(basePath);
90
+ if (sameCounts(markdown, zeroCounts())) {
91
+ return {
92
+ action: "none",
93
+ reason: "no-markdown",
94
+ markdown,
95
+ beforeDb: zeroCounts(),
96
+ afterDb: zeroCounts(),
97
+ };
98
+ }
99
+
100
+ const opened = await ensureDbOpen(basePath);
101
+ if (!opened || !isDbAvailable()) {
102
+ throw new Error(`failed to open or create the GSD database at ${basePath}`);
103
+ }
104
+
105
+ const beforeDb = countDbHierarchy();
106
+ if (sameCounts(markdown, beforeDb)) {
107
+ return { action: "none", reason: "in-sync", markdown, beforeDb, afterDb: beforeDb };
108
+ }
109
+
110
+ const reason = sameCounts(beforeDb, zeroCounts()) ? "db-empty" : "count-mismatch";
111
+ const imported = transaction(() => {
112
+ clearEngineHierarchy();
113
+ return migrateHierarchyToDb(basePath);
114
+ });
115
+ invalidateStateCache();
116
+
117
+ const afterDb = {
118
+ milestones: imported.milestones,
119
+ slices: imported.slices,
120
+ tasks: imported.tasks,
121
+ };
122
+ if (!sameCounts(markdown, afterDb)) {
123
+ throw new Error(
124
+ `migration auto-import verification failed: markdown ${markdown.milestones}M/${markdown.slices}S/${markdown.tasks}T, db ${afterDb.milestones}M/${afterDb.slices}S/${afterDb.tasks}T`,
125
+ );
126
+ }
127
+
128
+ return { action: "imported", reason, markdown, beforeDb, afterDb };
129
+ }
@@ -0,0 +1,35 @@
1
+ import { isAbsolute, relative, resolve } from "node:path";
2
+ import { normalizePlannedFileReference } from "./files.js";
3
+
4
+ export interface PlanningPathScopeField {
5
+ field: string;
6
+ values: string[];
7
+ }
8
+
9
+ function isInsideBase(basePath: string, candidate: string): boolean {
10
+ const base = resolve(basePath);
11
+ const abs = resolve(candidate);
12
+ const rel = relative(base, abs);
13
+ return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
14
+ }
15
+
16
+ /**
17
+ * Planning IO fields are execution contracts. Absolute paths are only safe when
18
+ * they stay inside the active working directory; in worktree mode, an absolute
19
+ * path to the original checkout makes executors edit the wrong tree.
20
+ */
21
+ export function validatePlanningPathScope(
22
+ basePath: string,
23
+ fields: PlanningPathScopeField[],
24
+ ): string | null {
25
+ for (const { field, values } of fields) {
26
+ for (const raw of values) {
27
+ const candidate = normalizePlannedFileReference(raw);
28
+ if (!isAbsolute(candidate)) continue;
29
+ if (isInsideBase(basePath, candidate)) continue;
30
+ return `${field} contains absolute path outside working directory: ${candidate}. Use a path relative to ${basePath}.`;
31
+ }
32
+ }
33
+
34
+ return null;
35
+ }