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

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 (197) hide show
  1. package/README.md +4 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/phases.js +29 -15
  4. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
  6. package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
  7. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  8. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  9. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  10. package/dist/resources/extensions/gsd/auto.js +66 -4
  11. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
  12. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
  14. package/dist/resources/extensions/gsd/context-budget.js +37 -2
  15. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  16. package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
  17. package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
  18. package/dist/resources/extensions/gsd/gsd-db.js +46 -13
  19. package/dist/resources/extensions/gsd/guided-flow.js +33 -4
  20. package/dist/resources/extensions/gsd/memory-store.js +69 -12
  21. package/dist/resources/extensions/gsd/migrate/command.js +40 -1
  22. package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
  23. package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
  24. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
  25. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  26. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  27. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  28. package/dist/resources/extensions/gsd/quick.js +34 -2
  29. package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
  30. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
  31. package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
  32. package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
  33. package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
  34. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
  35. package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
  36. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  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 +18 -18
  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 +18 -18
  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 +3 -3
  71. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  72. package/packages/mcp-server/dist/workflow-tools.js +22 -17
  73. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  74. package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
  75. package/packages/mcp-server/src/workflow-tools.ts +30 -16
  76. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  77. package/packages/native/tsconfig.tsbuildinfo +1 -1
  78. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  79. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/agent-session.js +8 -0
  82. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
  84. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  85. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  86. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  88. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  90. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  92. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  94. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
  96. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
  98. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  100. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  103. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  105. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
  112. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  114. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  117. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  118. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  119. package/packages/pi-coding-agent/src/core/agent-session.ts +8 -0
  120. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
  121. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  122. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  123. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
  124. package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
  125. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  126. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  127. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
  128. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  129. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
  130. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  131. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  132. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  133. package/packages/pi-tui/dist/tui.js +18 -8
  134. package/packages/pi-tui/dist/tui.js.map +1 -1
  135. package/packages/pi-tui/src/tui.ts +20 -8
  136. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  137. package/src/resources/extensions/gsd/auto/phases.ts +35 -20
  138. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  139. package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
  140. package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
  141. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  142. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  143. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  144. package/src/resources/extensions/gsd/auto.ts +78 -3
  145. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
  146. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
  147. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  148. package/src/resources/extensions/gsd/context-budget.ts +44 -2
  149. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  150. package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
  151. package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
  152. package/src/resources/extensions/gsd/gsd-db.ts +50 -13
  153. package/src/resources/extensions/gsd/guided-flow.ts +49 -4
  154. package/src/resources/extensions/gsd/memory-store.ts +77 -12
  155. package/src/resources/extensions/gsd/migrate/command.ts +47 -1
  156. package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
  157. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  158. package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
  159. package/src/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
  160. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  161. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  162. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  163. package/src/resources/extensions/gsd/quick.ts +37 -2
  164. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +71 -0
  165. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
  166. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  167. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +14 -1
  168. package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
  169. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
  170. package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
  171. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
  172. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
  173. package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
  174. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
  175. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
  176. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  177. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
  178. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  179. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
  180. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  181. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  182. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +55 -0
  183. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  184. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
  185. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  186. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  187. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
  188. package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
  189. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
  190. package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
  191. package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
  192. package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
  193. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
  194. package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
  195. package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
  196. /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 8F5YpnZNBaooIWGF4GBV3}/_buildManifest.js +0 -0
  197. /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 8F5YpnZNBaooIWGF4GBV3}/_ssgManifest.js +0 -0
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @see D001 (module location), D002 (200K fallback), D003 (section-boundary truncation)
9
9
  */
10
- import { getCharsPerToken } from "./token-counter.js";
10
+ import { getCharsPerToken, isAccurateCountingAvailable, countTokensSync, } from "./token-counter.js";
11
11
  // ─── Budget ratio constants ──────────────────────────────────────────────────
12
12
  // Percentages of total context window allocated to each budget category.
13
13
  // These are applied after tokens→chars conversion.
@@ -23,6 +23,22 @@ const CHARS_PER_TOKEN = 4;
23
23
  const DEFAULT_CONTEXT_WINDOW = 200_000;
24
24
  /** Conservative effective context for Claude Code subscription routing (#4676) */
25
25
  const CLAUDE_CODE_EFFECTIVE_CONTEXT_WINDOW = 200_000;
26
+ /**
27
+ * Cached empirical chars-per-token from a tiktoken probe, keyed by provider.
28
+ * countTokensSync's fallback path is provider-aware, so we cache per-provider
29
+ * to preserve that distinction once the encoder warms. The cl100k_base encoder
30
+ * itself gives a stable ratio for ASCII English so a single probe per provider
31
+ * key is sufficient. Empty map means "not yet probed" or "encoder unavailable".
32
+ */
33
+ const _empiricalCharsPerTokenByProvider = new Map();
34
+ /**
35
+ * Test hook — clears the empirical chars-per-token cache so test cases that
36
+ * assert against the static char-ratio fallback aren't polluted by a prior
37
+ * tiktoken-warmed run in the same process. Production code must not call this.
38
+ */
39
+ export function _resetEmpiricalCacheForTest() {
40
+ _empiricalCharsPerTokenByProvider.clear();
41
+ }
26
42
  /** Percentage of context consumed before suggesting a continue-here checkpoint */
27
43
  const CONTINUE_THRESHOLD_PERCENT = 70;
28
44
  // ─── Task count bounds ───────────────────────────────────────────────────────
@@ -46,7 +62,26 @@ const TASK_COUNT_TIERS = [
46
62
  export function computeBudgets(contextWindow, provider) {
47
63
  const effectiveWindow = contextWindow > 0 ? contextWindow : DEFAULT_CONTEXT_WINDOW;
48
64
  const charsPerToken = provider ? getCharsPerToken(provider) : CHARS_PER_TOKEN;
49
- const totalChars = effectiveWindow * charsPerToken;
65
+ // Prefer the tiktoken encoder for total-char estimation when it has been
66
+ // warmed (initTokenCounter resolved). The cl100k_base ratio is stable for
67
+ // ASCII English, so probe once per provider and cache — computeBudgets is
68
+ // called multiple times per prompt build and the probe encode is otherwise
69
+ // wasted work.
70
+ let totalChars;
71
+ if (isAccurateCountingAvailable()) {
72
+ const providerKey = provider ?? "__default__";
73
+ let empirical = _empiricalCharsPerTokenByProvider.get(providerKey);
74
+ if (empirical === undefined) {
75
+ const probe = "the quick brown fox jumps over the lazy dog ".repeat(64);
76
+ const probeTokens = countTokensSync(probe, provider);
77
+ empirical = probeTokens > 0 ? probe.length / probeTokens : charsPerToken;
78
+ _empiricalCharsPerTokenByProvider.set(providerKey, empirical);
79
+ }
80
+ totalChars = effectiveWindow * empirical;
81
+ }
82
+ else {
83
+ totalChars = effectiveWindow * charsPerToken;
84
+ }
50
85
  return {
51
86
  summaryBudgetChars: Math.floor(totalChars * SUMMARY_RATIO),
52
87
  inlineContextBudgetChars: Math.floor(totalChars * INLINE_CONTEXT_RATIO),
@@ -253,6 +253,45 @@ export function markCanceled(dispatchId, reason) {
253
253
  SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
254
254
  WHERE id = :id AND status IN ('pending','claimed','running')`).run({ ":id": dispatchId, ":ended_at": now, ":reason": reason });
255
255
  }
256
+ /**
257
+ * Best-effort signal/crash cleanup: cancel the latest active dispatch owned by
258
+ * a worker when the process is exiting before the normal loop can settle it.
259
+ */
260
+ export function markLatestActiveForWorkerCanceled(workerId, reason) {
261
+ if (!isDbAvailable())
262
+ return false;
263
+ const now = new Date().toISOString();
264
+ const db = _getAdapter();
265
+ const result = transaction(() => {
266
+ return db.prepare(`UPDATE unit_dispatches
267
+ SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
268
+ WHERE id = (
269
+ SELECT id FROM unit_dispatches
270
+ WHERE worker_id = :worker_id
271
+ AND status IN ('pending','claimed','running')
272
+ ORDER BY id DESC
273
+ LIMIT 1
274
+ )`).run({
275
+ ":ended_at": now,
276
+ ":reason": reason,
277
+ ":worker_id": workerId,
278
+ });
279
+ });
280
+ const changes = typeof result.changes === "number"
281
+ ? result.changes
282
+ : 0;
283
+ if (changes <= 0)
284
+ return false;
285
+ insertAuditEvent({
286
+ eventId: randomUUID(),
287
+ traceId: workerId,
288
+ category: "orchestration",
289
+ type: "dispatch-canceled",
290
+ ts: now,
291
+ payload: { workerId, reason },
292
+ });
293
+ return true;
294
+ }
256
295
  /**
257
296
  * Fetch the most recent N dispatches for a unit. Used by recordDispatchClaim
258
297
  * callers to compute attempt_n and by detect-stuck.ts (B3) to consult
@@ -46,7 +46,8 @@ export function createBaseSchemaObjects(db, hooks) {
46
46
  slice_id TEXT DEFAULT NULL,
47
47
  task_id TEXT DEFAULT NULL,
48
48
  full_content TEXT NOT NULL DEFAULT '',
49
- imported_at TEXT NOT NULL DEFAULT ''
49
+ imported_at TEXT NOT NULL DEFAULT '',
50
+ content_hash TEXT DEFAULT NULL
50
51
  )
51
52
  `);
52
53
  db.exec(`
@@ -64,7 +65,8 @@ export function createBaseSchemaObjects(db, hooks) {
64
65
  hit_count INTEGER NOT NULL DEFAULT 0,
65
66
  scope TEXT NOT NULL DEFAULT 'project',
66
67
  tags TEXT NOT NULL DEFAULT '[]',
67
- structured_fields TEXT DEFAULT NULL
68
+ structured_fields TEXT DEFAULT NULL,
69
+ last_hit_at TEXT DEFAULT NULL
68
70
  )
69
71
  `);
70
72
  db.exec(`
@@ -379,6 +379,12 @@ export function applyMigrationV26MilestoneCommitAttributions(db) {
379
379
  `);
380
380
  db.exec("CREATE INDEX IF NOT EXISTS idx_milestone_commit_attr_milestone ON milestone_commit_attributions(milestone_id)");
381
381
  }
382
+ export function applyMigrationV27ArtifactHash(db) {
383
+ ensureColumn(db, "artifacts", "content_hash", "ALTER TABLE artifacts ADD COLUMN content_hash TEXT DEFAULT NULL");
384
+ }
385
+ export function applyMigrationV28MemoryLastHitAt(db) {
386
+ ensureColumn(db, "memories", "last_hit_at", "ALTER TABLE memories ADD COLUMN last_hit_at TEXT DEFAULT NULL");
387
+ }
382
388
  export function applyMigrationV22QualityGateRepair(db, hooks) {
383
389
  const qgInfo = db.prepare("PRAGMA table_info(quality_gates)").all();
384
390
  const taskIdCol = qgInfo.find((r) => r["name"] === "task_id");
@@ -22,6 +22,7 @@
22
22
  // intentionally independent store for cross-worktree claim races and is
23
23
  // excluded from this invariant.
24
24
  import { createRequire } from "node:module";
25
+ import { createHash } from "node:crypto";
25
26
  import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
26
27
  import { dirname } from "node:path";
27
28
  import { GSDError, GSD_STALE_STATE } from "./errors.js";
@@ -36,7 +37,7 @@ import { rowToActiveDecision, rowToActiveRequirement, rowToDecision, rowToRequir
36
37
  import { rowToGate } from "./db-gate-rows.js";
37
38
  import { rowToArtifact, rowToMilestone } from "./db-milestone-artifact-rows.js";
38
39
  import { backupDatabaseBeforeMigration } from "./db-migration-backup.js";
39
- import { applyMigrationV2Artifacts, applyMigrationV3Memories, applyMigrationV4DecisionMadeBy, applyMigrationV5HierarchyTables, applyMigrationV6SliceSummaries, applyMigrationV7Dependencies, applyMigrationV8PlanningFields, applyMigrationV9Ordering, applyMigrationV10ReplanTrigger, applyMigrationV11TaskPlanning, applyMigrationV12QualityGates, applyMigrationV13HotPathIndexes, applyMigrationV14SliceDependencies, applyMigrationV15AuditTables, applyMigrationV16EscalationSource, applyMigrationV17TaskEscalation, applyMigrationV18MemorySources, applyMigrationV19MemoryFts, applyMigrationV20MemoryRelations, applyMigrationV21StructuredMemories, applyMigrationV22QualityGateRepair, applyMigrationV23MilestoneQueue, applyMigrationV26MilestoneCommitAttributions, } from "./db-migration-steps.js";
40
+ import { applyMigrationV2Artifacts, applyMigrationV3Memories, applyMigrationV4DecisionMadeBy, applyMigrationV5HierarchyTables, applyMigrationV6SliceSummaries, applyMigrationV7Dependencies, applyMigrationV8PlanningFields, applyMigrationV9Ordering, applyMigrationV10ReplanTrigger, applyMigrationV11TaskPlanning, applyMigrationV12QualityGates, applyMigrationV13HotPathIndexes, applyMigrationV14SliceDependencies, applyMigrationV15AuditTables, applyMigrationV16EscalationSource, applyMigrationV17TaskEscalation, applyMigrationV18MemorySources, applyMigrationV19MemoryFts, applyMigrationV20MemoryRelations, applyMigrationV21StructuredMemories, applyMigrationV22QualityGateRepair, applyMigrationV23MilestoneQueue, applyMigrationV26MilestoneCommitAttributions, applyMigrationV27ArtifactHash, applyMigrationV28MemoryLastHitAt, } from "./db-migration-steps.js";
40
41
  import { isMemoriesFtsAvailableSchema, tryCreateMemoriesFtsSchema } from "./db-memory-fts-schema.js";
41
42
  import { createDbOpenState } from "./db-open-state.js";
42
43
  import { createRuntimeKvTableV25 } from "./db-runtime-kv-schema.js";
@@ -52,7 +53,7 @@ const providerLoader = createSqliteProviderLoader({
52
53
  nodeVersion: process.versions.node,
53
54
  writeStderr: (message) => process.stderr.write(message),
54
55
  });
55
- export const SCHEMA_VERSION = 26;
56
+ export const SCHEMA_VERSION = 28;
56
57
  function initSchema(db, fileBacked) {
57
58
  if (fileBacked)
58
59
  db.exec("PRAGMA journal_mode=WAL");
@@ -250,6 +251,14 @@ function migrateSchema(db) {
250
251
  applyMigrationV26MilestoneCommitAttributions(db);
251
252
  recordSchemaVersion(db, 26);
252
253
  }
254
+ if (currentVersion < 27) {
255
+ applyMigrationV27ArtifactHash(db);
256
+ recordSchemaVersion(db, 27);
257
+ }
258
+ if (currentVersion < 28) {
259
+ applyMigrationV28MemoryLastHitAt(db);
260
+ recordSchemaVersion(db, 28);
261
+ }
253
262
  db.exec("COMMIT");
254
263
  }
255
264
  catch (err) {
@@ -821,8 +830,9 @@ export function clearArtifacts() {
821
830
  export function insertArtifact(a) {
822
831
  if (!currentDb)
823
832
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
824
- currentDb.prepare(`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
825
- VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at)`).run({
833
+ const contentHash = createHash("sha256").update(a.full_content).digest("hex");
834
+ currentDb.prepare(`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash)
835
+ VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at, :content_hash)`).run({
826
836
  ":path": a.path,
827
837
  ":artifact_type": a.artifact_type,
828
838
  ":milestone_id": a.milestone_id,
@@ -830,6 +840,7 @@ export function insertArtifact(a) {
830
840
  ":task_id": a.task_id,
831
841
  ":full_content": a.full_content,
832
842
  ":imported_at": new Date().toISOString(),
843
+ ":content_hash": contentHash,
833
844
  });
834
845
  }
835
846
  export function insertMilestone(m) {
@@ -1521,6 +1532,13 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
1521
1532
  const hasEscalationAwaiting = wtTaskInfo.some((col) => col["name"] === "escalation_awaiting_review");
1522
1533
  const hasEscalationArtifact = wtTaskInfo.some((col) => col["name"] === "escalation_artifact_path");
1523
1534
  const hasEscalationOverride = wtTaskInfo.some((col) => col["name"] === "escalation_override_applied_at");
1535
+ const wtArtifactInfo = adapter.prepare("PRAGMA wt.table_info('artifacts')").all();
1536
+ const hasArtifactContentHash = wtArtifactInfo.some((col) => col["name"] === "content_hash");
1537
+ const wtMemoryInfo = adapter.prepare("PRAGMA wt.table_info('memories')").all();
1538
+ const hasMemoryScope = wtMemoryInfo.some((col) => col["name"] === "scope");
1539
+ const hasMemoryTags = wtMemoryInfo.some((col) => col["name"] === "tags");
1540
+ const hasMemoryStructuredFields = wtMemoryInfo.some((col) => col["name"] === "structured_fields");
1541
+ const hasMemoryLastHitAt = wtMemoryInfo.some((col) => col["name"] === "last_hit_at");
1524
1542
  const decConf = adapter.prepare(`SELECT m.id FROM decisions m INNER JOIN wt.decisions w ON m.id = w.id WHERE m.decision != w.decision OR m.choice != w.choice OR m.rationale != w.rationale OR ${hasMadeBy ? "m.made_by != w.made_by" : "'agent' != 'agent'"} OR m.superseded_by IS NOT w.superseded_by`).all();
1525
1543
  for (const row of decConf)
1526
1544
  conflicts.push(`decision ${row["id"]}: modified in both`);
@@ -1553,12 +1571,17 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
1553
1571
  supporting_slices, validation, notes, full_content, superseded_by
1554
1572
  FROM wt.requirements
1555
1573
  `).run());
1574
+ // V27: preserve content_hash. If the worktree predates V27 (no column),
1575
+ // fall back to the main DB's existing hash so reconcile doesn't null
1576
+ // out integrity fingerprints on artifacts that were unchanged in wt.
1556
1577
  merged.artifacts = countChanges(adapter.prepare(`
1557
1578
  INSERT OR REPLACE INTO artifacts (
1558
- path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
1579
+ path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash
1559
1580
  )
1560
- SELECT path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
1561
- FROM wt.artifacts
1581
+ SELECT w.path, w.artifact_type, w.milestone_id, w.slice_id, w.task_id, w.full_content, w.imported_at,
1582
+ ${hasArtifactContentHash ? "w.content_hash" : "m.content_hash"}
1583
+ FROM wt.artifacts w
1584
+ LEFT JOIN artifacts m ON m.path = w.path
1562
1585
  `).run());
1563
1586
  // Merge milestones — worktree may have updated status/planning fields.
1564
1587
  // Never downgrade status: complete > active > pre-planning (#4372).
@@ -1656,15 +1679,25 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
1656
1679
  FROM wt.tasks w
1657
1680
  LEFT JOIN tasks m ON m.milestone_id = w.milestone_id AND m.slice_id = w.slice_id AND m.id = w.id
1658
1681
  `).run());
1659
- // Merge memories — keep worktree-learned insights
1682
+ // Merge memories — keep worktree-learned insights.
1683
+ // V18 (scope, tags), V21 (structured_fields), V28 (last_hit_at): for each
1684
+ // column the wt may not yet have (older worktree DB), fall back to the
1685
+ // main DB's existing value via LEFT JOIN so reconcile never silently
1686
+ // resets these fields to defaults on rows that already had them.
1660
1687
  merged.memories = countChanges(adapter.prepare(`
1661
1688
  INSERT OR REPLACE INTO memories (
1662
1689
  seq, id, category, content, confidence, source_unit_type, source_unit_id,
1663
- created_at, updated_at, superseded_by, hit_count
1690
+ created_at, updated_at, superseded_by, hit_count,
1691
+ scope, tags, structured_fields, last_hit_at
1664
1692
  )
1665
- SELECT seq, id, category, content, confidence, source_unit_type, source_unit_id,
1666
- created_at, updated_at, superseded_by, hit_count
1667
- FROM wt.memories
1693
+ SELECT w.seq, w.id, w.category, w.content, w.confidence, w.source_unit_type, w.source_unit_id,
1694
+ w.created_at, w.updated_at, w.superseded_by, w.hit_count,
1695
+ ${hasMemoryScope ? "w.scope" : "COALESCE(m.scope, 'project')"},
1696
+ ${hasMemoryTags ? "w.tags" : "COALESCE(m.tags, '[]')"},
1697
+ ${hasMemoryStructuredFields ? "w.structured_fields" : "m.structured_fields"},
1698
+ ${hasMemoryLastHitAt ? "w.last_hit_at" : "m.last_hit_at"}
1699
+ FROM wt.memories w
1700
+ LEFT JOIN memories m ON m.id = w.id
1668
1701
  `).run());
1669
1702
  // Merge verification evidence — append-only, use INSERT OR IGNORE to avoid duplicates
1670
1703
  merged.verification_evidence = countChanges(adapter.prepare(`
@@ -2423,7 +2456,7 @@ export function updateMemoryContentRow(id, content, confidence, updatedAt) {
2423
2456
  export function incrementMemoryHitCount(id, updatedAt) {
2424
2457
  if (!currentDb)
2425
2458
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2426
- currentDb.prepare("UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at WHERE id = :id").run({ ":updated_at": updatedAt, ":id": id });
2459
+ currentDb.prepare("UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at, last_hit_at = :last_hit_at WHERE id = :id").run({ ":updated_at": updatedAt, ":last_hit_at": updatedAt, ":id": id });
2427
2460
  }
2428
2461
  export function supersedeMemoryRow(oldId, newId, updatedAt) {
2429
2462
  if (!currentDb)
@@ -51,6 +51,21 @@ export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesCon
51
51
  import { logWarning } from "./workflow-logger.js";
52
52
  import { deleteRuntimeKv } from "./db/runtime-kv.js";
53
53
  import { PAUSED_SESSION_KV_KEY } from "./interrupted-session.js";
54
+ function scheduleAutoStartAfterIdle(ctx, pi, basePath, verboseMode, options, launch = startAutoDetached) {
55
+ const waitForIdle = typeof ctx.waitForIdle === "function"
56
+ ? ctx.waitForIdle.bind(ctx)
57
+ : async () => { };
58
+ void waitForIdle()
59
+ .then(() => {
60
+ setTimeout(() => launch(ctx, pi, basePath, verboseMode, options), 0);
61
+ })
62
+ .catch((err) => {
63
+ const message = err instanceof Error ? err.message : String(err);
64
+ ctx.ui.notify(`Auto-start failed while waiting for the prior turn to settle: ${message}`, "error");
65
+ logWarning("guided", `auto-start idle wait failed: ${message}`);
66
+ });
67
+ }
68
+ export const _scheduleAutoStartAfterIdleForTest = scheduleAutoStartAfterIdle;
54
69
  // ─── Scope-based validator wrappers ──────────────────────────────────────────
55
70
  // These thin wrappers accept a MilestoneScope so callers that already hold a
56
71
  // pinned scope never have to re-derive (basePath, milestoneId) separately.
@@ -325,7 +340,7 @@ async function dispatchNextDeepProjectSetupStage(entry) {
325
340
  const { DISPATCH_RULES, hasPendingDeepStage } = await import("./auto-dispatch.js");
326
341
  if (!hasPendingDeepStage(prefs, entry.basePath)) {
327
342
  pendingDeepProjectSetupMap.delete(entry.basePath);
328
- startAutoDetached(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
343
+ scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
329
344
  return true;
330
345
  }
331
346
  const state = await deriveState(entry.basePath);
@@ -359,14 +374,14 @@ async function dispatchNextDeepProjectSetupStage(entry) {
359
374
  }
360
375
  else if (hasPendingDeepStage(prefs, entry.basePath)) {
361
376
  pendingDeepProjectSetupMap.delete(entry.basePath);
362
- startAutoDetached(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
377
+ scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
363
378
  return true;
364
379
  }
365
380
  return false;
366
381
  }
367
382
  if (!USER_DRIVEN_DEEP_SETUP_UNITS.has(result.unitType)) {
368
383
  pendingDeepProjectSetupMap.delete(entry.basePath);
369
- startAutoDetached(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
384
+ scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
370
385
  return true;
371
386
  }
372
387
  entry.currentUnitType = result.unitType;
@@ -544,7 +559,7 @@ export function checkAutoStartAfterDiscuss() {
544
559
  }
545
560
  pendingAutoStartMap.delete(basePath);
546
561
  ctx.ui.notify(`Milestone ${milestoneId} ready.`, "success");
547
- startAutoDetached(ctx, pi, basePath, false, { step });
562
+ scheduleAutoStartAfterIdle(ctx, pi, basePath, false, { step });
548
563
  return true;
549
564
  }
550
565
  /**
@@ -1708,6 +1723,20 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1708
1723
  return;
1709
1724
  }
1710
1725
  }
1726
+ if (interrupted.classification !== "recoverable") {
1727
+ try {
1728
+ const { autoImportMarkdownHierarchyIfDbMismatch } = await import("./migration-auto-check.js");
1729
+ const result = await autoImportMarkdownHierarchyIfDbMismatch(basePath);
1730
+ if (result.action === "imported") {
1731
+ ctx.ui.notify(`Recovered migrated planning state into gsd.db (${result.reason}): ${result.afterDb.milestones} milestone(s), ${result.afterDb.slices} slice(s), ${result.afterDb.tasks} task(s).`, "info");
1732
+ }
1733
+ }
1734
+ catch (err) {
1735
+ const message = err instanceof Error ? err.message : String(err);
1736
+ ctx.ui.notify(`GSD could not auto-import existing planning state into gsd.db: ${message}`, "warning");
1737
+ logWarning("guided", `planning state auto-import failed: ${message}`, { file: "guided-flow.ts" });
1738
+ }
1739
+ }
1711
1740
  // Always derive from the project root — the assessment may have derived
1712
1741
  // state from a worktree path that was cleaned up in the stale branch above.
1713
1742
  const state = await deriveState(basePath);
@@ -14,6 +14,27 @@ const CATEGORY_PRIORITY = {
14
14
  environment: 4,
15
15
  preference: 5,
16
16
  };
17
+ // ─── Scoring Helpers ─────────────────────────────────────────────────────────
18
+ /**
19
+ * Time-decay factor for memory relevance scoring.
20
+ * Returns 1.0 for never-hit or recently-hit memories, decaying linearly to
21
+ * 0.7 for memories not accessed in 90+ days. Floor at 0.7 keeps old-but-valid
22
+ * knowledge from being fully suppressed.
23
+ *
24
+ * Defensive parsing: invalid timestamp strings (NaN from Date.parse) are
25
+ * treated as "no decay" rather than propagating NaN into score arithmetic.
26
+ * Future timestamps (clock skew, manual DB edits) clamp to daysAgo=0 so the
27
+ * factor stays in the documented [0.7, 1.0] contract.
28
+ */
29
+ export function memoryDecayFactor(lastHitAt) {
30
+ if (!lastHitAt)
31
+ return 1.0;
32
+ const ts = Date.parse(lastHitAt);
33
+ if (!Number.isFinite(ts))
34
+ return 1.0;
35
+ const daysAgo = Math.max(0, (Date.now() - ts) / 86_400_000);
36
+ return Math.max(0.7, 1.0 - 0.3 * Math.min(1.0, daysAgo / 90));
37
+ }
17
38
  // ─── Row Mapping ────────────────────────────────────────────────────────────
18
39
  function rowToMemory(row) {
19
40
  return {
@@ -31,6 +52,7 @@ function rowToMemory(row) {
31
52
  scope: row['scope'] ?? 'project',
32
53
  tags: parseTags(row['tags']),
33
54
  structured_fields: parseStructuredFields(row['structured_fields']),
55
+ last_hit_at: row['last_hit_at'] ?? null,
34
56
  };
35
57
  }
36
58
  function parseStructuredFields(raw) {
@@ -114,15 +136,37 @@ export function queryMemoriesRanked(opts) {
114
136
  ? semanticSearch(adapter, opts.queryVector, activeClause, 50)
115
137
  : [];
116
138
  if (keywordHits.length === 0 && semanticHits.length === 0 && !trimmedQuery) {
117
- // No query at all — fall back to the existing ranked-by-score listing.
118
- return getActiveMemoriesRanked(k).map((memory) => ({
119
- memory,
120
- score: memory.confidence * (1 + memory.hit_count * 0.1),
121
- keywordRank: null,
122
- semanticRank: null,
123
- confidenceBoost: memory.confidence * (1 + memory.hit_count * 0.1),
124
- reason: 'ranked',
125
- })).filter((hit) => passesFilters(hit.memory, opts));
139
+ // No query at all — return top-k by decay-aware ranked score.
140
+ //
141
+ // Build the candidate pool from a direct SQL query that honors the
142
+ // request's activeClause (i.e. include_superseded). Using
143
+ // getActiveMemoriesRanked here would silently drop superseded rows even
144
+ // when the caller explicitly opted in, and would slice by raw score
145
+ // before decay/filters had a chance to reorder.
146
+ const candidatePool = Math.min(Math.max(k * 5, 50), 500);
147
+ const rows = adapter
148
+ .prepare(`SELECT * FROM memories ${activeClause}
149
+ ORDER BY (confidence * (1.0 + hit_count * 0.1)) DESC
150
+ LIMIT :limit`)
151
+ .all({ ':limit': candidatePool });
152
+ const ranked = [];
153
+ for (const row of rows) {
154
+ const memory = rowToMemory(row);
155
+ if (!passesFilters(memory, opts))
156
+ continue;
157
+ const decay = memoryDecayFactor(memory.last_hit_at);
158
+ const score = memory.confidence * (1 + memory.hit_count * 0.1) * decay;
159
+ ranked.push({
160
+ memory,
161
+ score,
162
+ keywordRank: null,
163
+ semanticRank: null,
164
+ confidenceBoost: score,
165
+ reason: 'ranked',
166
+ });
167
+ }
168
+ ranked.sort((a, b) => b.score - a.score);
169
+ return ranked.slice(0, k);
126
170
  }
127
171
  // 3) Reciprocal rank fusion — each hit contributes 1/(rrfK + rank).
128
172
  const fused = new Map();
@@ -155,7 +199,7 @@ export function queryMemoriesRanked(opts) {
155
199
  for (const entry of fused.values()) {
156
200
  if (!passesFilters(entry.memory, opts))
157
201
  continue;
158
- const boost = entry.memory.confidence * (1 + entry.memory.hit_count * 0.1);
202
+ const boost = entry.memory.confidence * (1 + entry.memory.hit_count * 0.1) * memoryDecayFactor(entry.memory.last_hit_at);
159
203
  const reason = entry.kwRank != null && entry.semRank != null
160
204
  ? 'both'
161
205
  : entry.kwRank != null
@@ -194,6 +238,7 @@ function passesFilters(memory, filters) {
194
238
  }
195
239
  return true;
196
240
  }
241
+ let ftsWarningEmitted = false;
197
242
  function keywordSearch(adapter, rawQuery, activeClause, limit) {
198
243
  const ftsAvailable = isFtsAvailable(adapter);
199
244
  if (ftsAvailable) {
@@ -215,14 +260,26 @@ function keywordSearch(adapter, rawQuery, activeClause, limit) {
215
260
  // fall through to LIKE
216
261
  }
217
262
  }
218
- // LIKE fallback — scans the candidate pool.
263
+ // LIKE fallback — scans a capped candidate pool.
264
+ if (!ftsWarningEmitted) {
265
+ ftsWarningEmitted = true;
266
+ logWarning('memory-store', 'FTS5 unavailable — using LIKE fallback scan (consider enabling FTS5)');
267
+ }
219
268
  const terms = rawQuery
220
269
  .toLowerCase()
221
270
  .split(/[^a-z0-9_]+/)
222
271
  .filter((t) => t.length >= 2);
223
272
  if (terms.length === 0)
224
273
  return [];
225
- const rows = adapter.prepare(`SELECT * FROM memories ${activeClause}`).all();
274
+ const preScanCap = Math.min(limit * 20, 2000);
275
+ // ORDER BY confidence-weighted hit_count DESC so the cap keeps the most
276
+ // valuable candidates instead of the oldest-by-rowid (which would silently
277
+ // exclude recently-stored memories on tables larger than preScanCap).
278
+ const rows = adapter
279
+ .prepare(`SELECT * FROM memories ${activeClause}
280
+ ORDER BY (confidence * (1.0 + hit_count * 0.1)) DESC
281
+ LIMIT :preScanCap`)
282
+ .all({ ':preScanCap': preScanCap });
226
283
  const scored = [];
227
284
  for (const row of rows) {
228
285
  const memory = rowToMemory(row);
@@ -15,6 +15,43 @@ import { fileURLToPath } from "node:url";
15
15
  import { showNextAction } from "../../shared/tui.js";
16
16
  import { validatePlanningDirectory, parsePlanningDirectory, transformToGSD, generatePreview, writeGSDDirectory, } from "./index.js";
17
17
  import { homedir } from "node:os";
18
+ import { ensureDbOpen } from "../bootstrap/dynamic-tools.js";
19
+ import { clearEngineHierarchy, transaction } from "../gsd-db.js";
20
+ import { migrateFromMarkdown } from "../md-importer.js";
21
+ import { invalidateStateCache } from "../state.js";
22
+ function assertMigrationImportMatchesPreview(imported, preview) {
23
+ const mismatches = [];
24
+ if (imported.hierarchy.milestones !== preview.milestoneCount) {
25
+ mismatches.push(`milestones ${imported.hierarchy.milestones}/${preview.milestoneCount}`);
26
+ }
27
+ if (imported.hierarchy.slices !== preview.totalSlices) {
28
+ mismatches.push(`slices ${imported.hierarchy.slices}/${preview.totalSlices}`);
29
+ }
30
+ if (imported.hierarchy.tasks !== preview.totalTasks) {
31
+ mismatches.push(`tasks ${imported.hierarchy.tasks}/${preview.totalTasks}`);
32
+ }
33
+ if (imported.requirements !== preview.requirements.total) {
34
+ mismatches.push(`requirements ${imported.requirements}/${preview.requirements.total}`);
35
+ }
36
+ if (mismatches.length > 0) {
37
+ throw new Error(`migration DB import verification failed: ${mismatches.join(", ")}`);
38
+ }
39
+ }
40
+ export async function importWrittenMigrationToDb(basePath, preview) {
41
+ const opened = await ensureDbOpen(basePath);
42
+ if (!opened) {
43
+ throw new Error(`failed to open or create the GSD database at ${basePath}`);
44
+ }
45
+ const counts = transaction(() => {
46
+ clearEngineHierarchy();
47
+ const imported = migrateFromMarkdown(basePath);
48
+ if (preview)
49
+ assertMigrationImportMatchesPreview(imported, preview);
50
+ return imported;
51
+ });
52
+ invalidateStateCache();
53
+ return counts;
54
+ }
18
55
  /** Format preview stats for embedding in the review prompt. */
19
56
  function formatPreviewStats(preview) {
20
57
  const lines = [
@@ -126,12 +163,14 @@ export async function handleMigrate(args, ctx, pi) {
126
163
  ctx.ui.notify("Writing .gsd directory…", "info");
127
164
  const result = await writeGSDDirectory(project, process.cwd());
128
165
  const gsdPath = gsdRoot(process.cwd());
129
- ctx.ui.notify(`✓ Migration complete ${result.paths.length} file(s) written to .gsd/`, "info");
166
+ const imported = await importWrittenMigrationToDb(process.cwd(), preview);
167
+ ctx.ui.notify(`✓ 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`, "info");
130
168
  // ── Post-write review offer ────────────────────────────────────────────────
131
169
  const reviewChoice = await showNextAction(ctx, {
132
170
  title: "Migration written",
133
171
  summary: [
134
172
  `${result.paths.length} files written to .gsd/`,
173
+ `${imported.hierarchy.milestones} milestone(s), ${imported.hierarchy.slices} slice(s), and ${imported.hierarchy.tasks} task(s) imported to gsd.db`,
135
174
  "",
136
175
  "The agent can now review the migrated output against GSD-2 standards —",
137
176
  "checking structure, content quality, deriveState() round-trip, and",
@@ -0,0 +1,87 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
3
+ import { clearEngineHierarchy, getAllMilestones, getMilestoneSlices, getSliceTasks, isDbAvailable, transaction, } from "./gsd-db.js";
4
+ import { migrateHierarchyToDb } from "./md-importer.js";
5
+ import { parsePlan, parseRoadmap } from "./parsers-legacy.js";
6
+ import { milestonesDir, resolveMilestoneFile, resolveSliceFile, } from "./paths.js";
7
+ import { invalidateStateCache } from "./state.js";
8
+ function zeroCounts() {
9
+ return { milestones: 0, slices: 0, tasks: 0 };
10
+ }
11
+ function sameCounts(a, b) {
12
+ return a.milestones === b.milestones && a.slices === b.slices && a.tasks === b.tasks;
13
+ }
14
+ export function countMarkdownHierarchy(basePath) {
15
+ const root = milestonesDir(basePath);
16
+ if (!existsSync(root))
17
+ return zeroCounts();
18
+ const counts = zeroCounts();
19
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
20
+ if (!entry.isDirectory() || !/^M\d+/.test(entry.name))
21
+ continue;
22
+ counts.milestones++;
23
+ const roadmapPath = resolveMilestoneFile(basePath, entry.name, "ROADMAP");
24
+ if (!roadmapPath || !existsSync(roadmapPath))
25
+ continue;
26
+ const roadmap = parseRoadmap(readFileSync(roadmapPath, "utf-8"));
27
+ counts.slices += roadmap.slices.length;
28
+ for (const slice of roadmap.slices) {
29
+ const planPath = resolveSliceFile(basePath, entry.name, slice.id, "PLAN");
30
+ if (!planPath || !existsSync(planPath))
31
+ continue;
32
+ const plan = parsePlan(readFileSync(planPath, "utf-8"));
33
+ counts.tasks += plan.tasks.length;
34
+ }
35
+ }
36
+ return counts;
37
+ }
38
+ export function countDbHierarchy() {
39
+ if (!isDbAvailable())
40
+ return zeroCounts();
41
+ const counts = zeroCounts();
42
+ const milestones = getAllMilestones();
43
+ counts.milestones = milestones.length;
44
+ for (const milestone of milestones) {
45
+ const slices = getMilestoneSlices(milestone.id);
46
+ counts.slices += slices.length;
47
+ for (const slice of slices) {
48
+ counts.tasks += getSliceTasks(milestone.id, slice.id).length;
49
+ }
50
+ }
51
+ return counts;
52
+ }
53
+ export async function autoImportMarkdownHierarchyIfDbMismatch(basePath) {
54
+ const markdown = countMarkdownHierarchy(basePath);
55
+ if (sameCounts(markdown, zeroCounts())) {
56
+ return {
57
+ action: "none",
58
+ reason: "no-markdown",
59
+ markdown,
60
+ beforeDb: zeroCounts(),
61
+ afterDb: zeroCounts(),
62
+ };
63
+ }
64
+ const opened = await ensureDbOpen(basePath);
65
+ if (!opened || !isDbAvailable()) {
66
+ throw new Error(`failed to open or create the GSD database at ${basePath}`);
67
+ }
68
+ const beforeDb = countDbHierarchy();
69
+ if (sameCounts(markdown, beforeDb)) {
70
+ return { action: "none", reason: "in-sync", markdown, beforeDb, afterDb: beforeDb };
71
+ }
72
+ const reason = sameCounts(beforeDb, zeroCounts()) ? "db-empty" : "count-mismatch";
73
+ const imported = transaction(() => {
74
+ clearEngineHierarchy();
75
+ return migrateHierarchyToDb(basePath);
76
+ });
77
+ invalidateStateCache();
78
+ const afterDb = {
79
+ milestones: imported.milestones,
80
+ slices: imported.slices,
81
+ tasks: imported.tasks,
82
+ };
83
+ if (!sameCounts(markdown, afterDb)) {
84
+ throw new Error(`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`);
85
+ }
86
+ return { action: "imported", reason, markdown, beforeDb, afterDb };
87
+ }