gsd-pi 2.78.1-dev.b0759e59b → 2.78.1-dev.e9d88a536

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 (203) hide show
  1. package/README.md +8 -5
  2. package/dist/headless-recover.d.ts +23 -0
  3. package/dist/headless-recover.js +93 -0
  4. package/dist/headless.js +9 -0
  5. package/dist/help-text.js +1 -0
  6. package/dist/resources/.managed-resources-content-hash +1 -1
  7. package/dist/resources/extensions/browser-tools/tools/intent.js +8 -1
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +4 -56
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -27
  10. package/dist/resources/extensions/gsd/auto-start.js +1 -8
  11. package/dist/resources/extensions/gsd/auto-worktree.js +59 -176
  12. package/dist/resources/extensions/gsd/auto.js +24 -6
  13. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +9 -77
  14. package/dist/resources/extensions/gsd/commands-codebase.js +2 -2
  15. package/dist/resources/extensions/gsd/commands-handlers.js +5 -5
  16. package/dist/resources/extensions/gsd/commands-logs.js +2 -2
  17. package/dist/resources/extensions/gsd/commands-scan.js +2 -2
  18. package/dist/resources/extensions/gsd/commands-ship.js +2 -2
  19. package/dist/resources/extensions/gsd/commands-workflow-templates.js +5 -5
  20. package/dist/resources/extensions/gsd/db-writer.js +16 -85
  21. package/dist/resources/extensions/gsd/dispatch-guard.js +6 -10
  22. package/dist/resources/extensions/gsd/doctor-engine-checks.js +2 -2
  23. package/dist/resources/extensions/gsd/gsd-db.js +74 -8
  24. package/dist/resources/extensions/gsd/guided-flow.js +31 -8
  25. package/dist/resources/extensions/gsd/markdown-renderer.js +14 -51
  26. package/dist/resources/extensions/gsd/parallel-merge.js +14 -13
  27. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +5 -2
  28. package/dist/resources/extensions/gsd/paths.js +35 -1
  29. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
  30. package/dist/resources/extensions/gsd/queue-order.js +6 -1
  31. package/dist/resources/extensions/gsd/rethink.js +2 -2
  32. package/dist/resources/extensions/gsd/state.js +91 -372
  33. package/dist/resources/extensions/gsd/tools/complete-milestone.js +6 -5
  34. package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -12
  35. package/dist/resources/extensions/gsd/tools/complete-task.js +19 -31
  36. package/dist/resources/extensions/gsd/tools/validate-milestone.js +7 -5
  37. package/dist/resources/extensions/gsd/workflow-manifest.js +2 -1
  38. package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +3 -21
  39. package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -3
  40. package/dist/resources/extensions/gsd/worktree-command.js +4 -3
  41. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  42. package/dist/web/standalone/.next/BUILD_ID +1 -1
  43. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  44. package/dist/web/standalone/.next/build-manifest.json +2 -2
  45. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  46. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  63. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  64. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  65. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  66. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  67. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  68. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  69. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  70. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  71. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  72. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  73. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  74. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  75. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  76. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  77. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  78. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  79. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  80. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  81. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  82. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  83. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  84. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  85. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  88. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  89. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  91. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  92. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  94. package/dist/web/standalone/.next/server/app/index.html +1 -1
  95. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  99. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  100. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  101. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  102. package/dist/web/standalone/.next/server/chunks/6336.js +1 -0
  103. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  104. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  106. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  107. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  108. package/package.json +1 -1
  109. package/packages/mcp-server/dist/workflow-tools.d.ts +6 -0
  110. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  111. package/packages/mcp-server/dist/workflow-tools.js +56 -2
  112. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  113. package/packages/mcp-server/src/parse-workflow-args.test.ts +80 -0
  114. package/packages/mcp-server/src/workflow-tools.ts +61 -2
  115. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  116. package/src/resources/extensions/browser-tools/tools/intent.ts +13 -2
  117. package/src/resources/extensions/gsd/auto-dispatch.ts +4 -60
  118. package/src/resources/extensions/gsd/auto-post-unit.ts +7 -26
  119. package/src/resources/extensions/gsd/auto-start.ts +1 -8
  120. package/src/resources/extensions/gsd/auto-worktree.ts +61 -204
  121. package/src/resources/extensions/gsd/auto.ts +23 -6
  122. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +9 -84
  123. package/src/resources/extensions/gsd/commands-codebase.ts +2 -2
  124. package/src/resources/extensions/gsd/commands-handlers.ts +5 -5
  125. package/src/resources/extensions/gsd/commands-logs.ts +2 -2
  126. package/src/resources/extensions/gsd/commands-scan.ts +2 -2
  127. package/src/resources/extensions/gsd/commands-ship.ts +2 -2
  128. package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -5
  129. package/src/resources/extensions/gsd/db-writer.ts +16 -83
  130. package/src/resources/extensions/gsd/dispatch-guard.ts +6 -11
  131. package/src/resources/extensions/gsd/doctor-engine-checks.ts +2 -2
  132. package/src/resources/extensions/gsd/gsd-db.ts +85 -8
  133. package/src/resources/extensions/gsd/guided-flow.ts +35 -8
  134. package/src/resources/extensions/gsd/markdown-renderer.ts +13 -64
  135. package/src/resources/extensions/gsd/parallel-merge.ts +14 -13
  136. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +5 -2
  137. package/src/resources/extensions/gsd/paths.ts +55 -1
  138. package/src/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
  139. package/src/resources/extensions/gsd/queue-order.ts +6 -1
  140. package/src/resources/extensions/gsd/rethink.ts +2 -2
  141. package/src/resources/extensions/gsd/state.ts +91 -389
  142. package/src/resources/extensions/gsd/tests/artifact-corruption-2630.test.ts +1 -0
  143. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +6 -0
  144. package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +21 -34
  145. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +6 -7
  146. package/src/resources/extensions/gsd/tests/complete-task.test.ts +8 -6
  147. package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +12 -27
  148. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +18 -5
  149. package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +4 -4
  150. package/src/resources/extensions/gsd/tests/db-writer.test.ts +14 -16
  151. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +6 -5
  152. package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +10 -38
  153. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +136 -56
  154. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +3 -0
  155. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +119 -61
  156. package/src/resources/extensions/gsd/tests/derive-state.test.ts +4 -0
  157. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +6 -20
  158. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +4 -5
  159. package/src/resources/extensions/gsd/tests/dispatcher-stuck-planning.test.ts +14 -15
  160. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +11 -16
  161. package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -1
  162. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -1
  163. package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +15 -36
  164. package/src/resources/extensions/gsd/tests/handler-worktree-write-isolation.test.ts +57 -0
  165. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +15 -15
  166. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +15 -5
  167. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +14 -8
  168. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -1
  169. package/src/resources/extensions/gsd/tests/memory-store.test.ts +3 -2
  170. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +2 -0
  171. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +25 -16
  172. package/src/resources/extensions/gsd/tests/projection-regression.test.ts +1 -0
  173. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +184 -0
  174. package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +6 -1
  175. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +3 -0
  176. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +4 -0
  177. package/src/resources/extensions/gsd/tests/rogue-file-detection.test.ts +3 -4
  178. package/src/resources/extensions/gsd/tests/slice-disk-reconcile.test.ts +10 -56
  179. package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +15 -16
  180. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
  181. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +23 -27
  182. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +13 -14
  183. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +4 -3
  184. package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +10 -33
  185. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +7 -8
  186. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +9 -15
  187. package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +12 -7
  188. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +4 -4
  189. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +24 -1
  190. package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +13 -0
  191. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +65 -71
  192. package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +26 -151
  193. package/src/resources/extensions/gsd/tools/complete-milestone.ts +7 -5
  194. package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -14
  195. package/src/resources/extensions/gsd/tools/complete-task.ts +19 -34
  196. package/src/resources/extensions/gsd/tools/validate-milestone.ts +7 -5
  197. package/src/resources/extensions/gsd/workflow-manifest.ts +4 -1
  198. package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -18
  199. package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -3
  200. package/src/resources/extensions/gsd/worktree-command.ts +4 -3
  201. package/dist/web/standalone/.next/server/chunks/8527.js +0 -1
  202. /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → oZGTPvJBQX_IDKKnuV8Bt}/_buildManifest.js +0 -0
  203. /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → oZGTPvJBQX_IDKKnuV8Bt}/_ssgManifest.js +0 -0
@@ -16,7 +16,7 @@
16
16
  import { existsSync, mkdirSync } from "node:fs";
17
17
  import { join, relative } from "node:path";
18
18
  import { loadPrompt } from "./prompt-loader.js";
19
- import { projectRoot } from "./commands/context.js";
19
+ import { currentDirectoryRoot } from "./commands/context.js";
20
20
  // ─── Constants ────────────────────────────────────────────────────────────────
21
21
  export const DEFAULT_FOCUS = "tech+arch";
22
22
  export const VALID_FOCUS_AREAS = ["tech", "arch", "quality", "concerns", "tech+arch"];
@@ -67,7 +67,7 @@ export function checkExistingDocuments(paths) {
67
67
  }
68
68
  // ─── Command handler ──────────────────────────────────────────────────────────
69
69
  export async function handleScan(args, ctx, pi) {
70
- const basePath = projectRoot();
70
+ const basePath = currentDirectoryRoot();
71
71
  const { focus } = parseScanArgs(args);
72
72
  const outputDir = join(basePath, ".gsd", "codebase");
73
73
  const outputPaths = buildScanOutputPaths(focus, basePath);
@@ -13,7 +13,7 @@ import { getLedger, getProjectTotals, aggregateByModel, formatCost, formatTokenC
13
13
  import { nativeGetCurrentBranch, nativeDetectMainBranch } from "./native-git-bridge.js";
14
14
  import { formatDuration } from "../shared/format-utils.js";
15
15
  import { parseEvalReviewFrontmatter } from "./eval-review-schema.js";
16
- import { projectRoot } from "./commands/context.js";
16
+ import { currentDirectoryRoot } from "./commands/context.js";
17
17
  function git(basePath, args) {
18
18
  return execFileSync("git", args, { cwd: basePath, encoding: "utf-8" }).trim();
19
19
  }
@@ -176,7 +176,7 @@ function generatePRContent(basePath, milestoneId, milestoneTitle) {
176
176
  return { title, body: sections.join("\n") };
177
177
  }
178
178
  export async function handleShip(args, ctx, _pi) {
179
- const basePath = projectRoot();
179
+ const basePath = currentDirectoryRoot();
180
180
  const dryRun = args.includes("--dry-run");
181
181
  const draft = args.includes("--draft");
182
182
  const force = args.includes("--force");
@@ -13,7 +13,7 @@ import { createGitService, runGit } from "./git-service.js";
13
13
  import { isAutoActive, isAutoPaused } from "./auto.js";
14
14
  import { getErrorMessage } from "./error-utils.js";
15
15
  import { resolvePlugin } from "./workflow-plugins.js";
16
- import { projectRoot } from "./commands/context.js";
16
+ import { currentDirectoryRoot } from "./commands/context.js";
17
17
  // ─── Helpers ─────────────────────────────────────────────────────────────────
18
18
  /**
19
19
  * Generate a URL-friendly slug from text.
@@ -143,7 +143,7 @@ export async function handleStart(args, ctx, pi) {
143
143
  // ─── Resume detection ───────────────────────────────────────────────────
144
144
  // /gsd start --resume or /gsd start resume → resume in-progress workflow
145
145
  if (trimmed === "--resume" || trimmed === "resume") {
146
- const basePath = projectRoot();
146
+ const basePath = currentDirectoryRoot();
147
147
  const inProgress = findInProgressWorkflows(basePath);
148
148
  if (inProgress.length === 0) {
149
149
  ctx.ui.notify("No in-progress workflows found.", "info");
@@ -182,7 +182,7 @@ export async function handleStart(args, ctx, pi) {
182
182
  }
183
183
  // Show in-progress workflows when /gsd start is called with no args
184
184
  if (!trimmed) {
185
- const basePath = projectRoot();
185
+ const basePath = currentDirectoryRoot();
186
186
  const inProgress = findInProgressWorkflows(basePath);
187
187
  if (inProgress.length > 0) {
188
188
  const wf = inProgress[0];
@@ -257,7 +257,7 @@ export async function handleStart(args, ctx, pi) {
257
257
  // ─── Resolved template ───────────────────────────────────────────────────
258
258
  const templateId = match.id;
259
259
  const template = match.template;
260
- const basePath = projectRoot();
260
+ const basePath = currentDirectoryRoot();
261
261
  const date = new Date().toISOString().split("T")[0];
262
262
  // Load the workflow template content — prefer a project/global plugin
263
263
  // override if one exists (same name, .md format).
@@ -437,7 +437,7 @@ export function dispatchMarkdownPhasePlugin(plugin, description, ctx, pi) {
437
437
  return;
438
438
  }
439
439
  const templateId = plugin.name;
440
- const basePath = projectRoot();
440
+ const basePath = currentDirectoryRoot();
441
441
  const date = new Date().toISOString().split("T")[0];
442
442
  let workflowContent;
443
443
  try {
@@ -273,22 +273,6 @@ export async function saveRequirementToDb(fields, basePath) {
273
273
  ORDER BY id
274
274
  LIMIT 1`)
275
275
  .get({ ':description': fields.description });
276
- const previousRow = existingRow
277
- ? {
278
- id: existingRow['id'],
279
- class: existingRow['class'],
280
- status: existingRow['status'],
281
- description: existingRow['description'],
282
- why: existingRow['why'],
283
- source: existingRow['source'],
284
- primary_owner: existingRow['primary_owner'],
285
- supporting_slices: existingRow['supporting_slices'],
286
- validation: existingRow['validation'],
287
- notes: existingRow['notes'],
288
- full_content: existingRow['full_content'],
289
- superseded_by: existingRow['superseded_by'] ?? null,
290
- }
291
- : null;
292
276
  const row = adapter
293
277
  .prepare('SELECT MAX(CAST(SUBSTR(id, 2) AS INTEGER)) as max_num FROM requirements')
294
278
  .get();
@@ -313,9 +297,9 @@ export async function saveRequirementToDb(fields, basePath) {
313
297
  superseded_by: existingRow?.['superseded_by'] ?? null,
314
298
  };
315
299
  db.upsertRequirement(requirement);
316
- return { id: nextId, isNew: !existingRow, previousRow };
300
+ return { id: nextId };
317
301
  });
318
- const { id, isNew, previousRow } = txResult;
302
+ const { id } = txResult;
319
303
  // Fetch all requirements for full file regeneration
320
304
  const adapter = db._getAdapter();
321
305
  let allRequirements = [];
@@ -343,19 +327,7 @@ export async function saveRequirementToDb(fields, basePath) {
343
327
  await saveFile(filePath, md);
344
328
  }
345
329
  catch (diskErr) {
346
- logError('manifest', 'disk write failed, rolling back DB row', { fn: 'saveRequirementToDb', error: String(diskErr.message) });
347
- try {
348
- if (isNew) {
349
- db.deleteRequirementById(id);
350
- }
351
- else if (previousRow) {
352
- db.upsertRequirement(previousRow);
353
- }
354
- }
355
- catch (rollbackErr) {
356
- logError('manifest', 'SPLIT BRAIN: disk write failed AND DB rollback failed — DB has orphaned row', { fn: 'saveRequirementToDb', id, error: String(rollbackErr.message) });
357
- }
358
- throw diskErr;
330
+ logWarning('projection', 'REQUIREMENTS.md projection write failed; DB requirement remains committed', { fn: 'saveRequirementToDb', id, error: String(diskErr.message) });
359
331
  }
360
332
  invalidateStateCache();
361
333
  clearPathCache();
@@ -463,14 +435,7 @@ export async function saveDecisionToDb(fields, basePath) {
463
435
  await saveFile(filePath, md);
464
436
  }
465
437
  catch (diskErr) {
466
- logError('manifest', 'disk write failed, rolling back DB row', { fn: 'saveDecisionToDb', error: String(diskErr.message) });
467
- try {
468
- db.deleteDecisionById(id);
469
- }
470
- catch (rollbackErr) {
471
- logError('manifest', 'SPLIT BRAIN: disk write failed AND DB rollback failed — DB has orphaned row', { fn: 'saveDecisionToDb', id, error: String(rollbackErr.message) });
472
- }
473
- throw diskErr;
438
+ logWarning('projection', 'DECISIONS.md projection write failed; DB decision remains committed', { fn: 'saveDecisionToDb', id, error: String(diskErr.message) });
474
439
  }
475
440
  // #2661: When a decision defers a slice, update the slice status in the DB
476
441
  // so the dispatcher skips it. Without this, STATE.md and DECISIONS.md are
@@ -584,34 +549,7 @@ export function extractDeferredSliceRef(fields) {
584
549
  export async function updateRequirementInDb(id, updates, basePath) {
585
550
  try {
586
551
  const db = await import('./gsd-db.js');
587
- let existing = db.getRequirementById(id);
588
- // If requirement doesn't exist in DB, seed the entire requirements table
589
- // from REQUIREMENTS.md first (#3346). This handles the standard workflow
590
- // where requirements are authored in markdown during discussion but never
591
- // imported into the database — making gsd_requirement_update always fail
592
- // with "not_found" at milestone completion.
593
- if (!existing) {
594
- const reqFilePath = resolveGsdRootFile(basePath, 'REQUIREMENTS');
595
- try {
596
- const content = readFileSync(reqFilePath, 'utf-8');
597
- const { parseRequirementsSections } = await import('./md-importer.js');
598
- const parsed = parseRequirementsSections(content);
599
- if (parsed.length > 0) {
600
- logWarning('manifest', `Seeding ${parsed.length} requirements from REQUIREMENTS.md into DB (first update triggers import)`, { fn: 'updateRequirementInDb' });
601
- for (const req of parsed) {
602
- // Only seed if not already in DB (avoid overwriting concurrent inserts)
603
- if (!db.getRequirementById(req.id)) {
604
- db.upsertRequirement(req);
605
- }
606
- }
607
- // Re-check after seeding
608
- existing = db.getRequirementById(id);
609
- }
610
- }
611
- catch {
612
- // REQUIREMENTS.md missing or unparseable — fall through to skeleton
613
- }
614
- }
552
+ const existing = db.getRequirementById(id);
615
553
  const base = existing ?? {
616
554
  id,
617
555
  class: '',
@@ -662,11 +600,7 @@ export async function updateRequirementInDb(id, updates, basePath) {
662
600
  await saveFile(filePath, md);
663
601
  }
664
602
  catch (diskErr) {
665
- logError('manifest', 'disk write failed, reverting DB row', { fn: 'updateRequirementInDb', error: String(diskErr.message) });
666
- if (existing) {
667
- db.upsertRequirement(existing);
668
- }
669
- throw diskErr;
603
+ logWarning('projection', 'REQUIREMENTS.md projection write failed; DB requirement update remains committed', { fn: 'updateRequirementInDb', id, error: String(diskErr.message) });
670
604
  }
671
605
  // Invalidate file-read caches so deriveState() sees the updated markdown.
672
606
  // Do NOT clear the artifacts table — we just wrote to it intentionally.
@@ -701,20 +635,19 @@ export async function saveArtifactToDb(opts, basePath) {
701
635
  }
702
636
  contentToPersist = generateRequirementsMd(activeRequirements);
703
637
  }
704
- // Shrinkage guard: if the file already exists and the new content is
705
- // significantly smaller (<50%), preserve the richer file on disk and
706
- // store its content in the DB instead of the abbreviated version. Root
707
- // canonical artifacts are exempt because their content is rendered from
708
- // canonical DB state, and cleanup/consolidation is often intentionally much
709
- // smaller than a malformed accumulated file.
710
- let dbContent = contentToPersist;
638
+ // Shrinkage guard: if the projection file already exists and the new
639
+ // content is significantly smaller (<50%), preserve the richer file on
640
+ // disk, but keep the DB row authoritative with the caller-provided content.
641
+ // The disk file is a stale projection until the next explicit render.
642
+ // Root canonical artifacts are exempt because their content is rendered
643
+ // from canonical DB state, and cleanup/consolidation is often intentionally
644
+ // much smaller than a malformed accumulated file.
711
645
  let skipDiskWrite = false;
712
646
  if (!isRootCanonicalArtifact(opts) && existsSync(fullPath)) {
713
647
  const existingSize = statSync(fullPath).size;
714
648
  const newSize = Buffer.byteLength(contentToPersist, 'utf-8');
715
649
  if (existingSize > 0 && newSize < existingSize * 0.5) {
716
- logWarning('manifest', `new content (${newSize}B) is <50% of existing file (${existingSize}B), preserving disk file`, { fn: 'saveArtifactToDb', path: opts.path });
717
- dbContent = readFileSync(fullPath, 'utf-8');
650
+ logWarning('projection', `new content (${newSize}B) is <50% of existing projection (${existingSize}B), preserving disk file while DB remains authoritative`, { fn: 'saveArtifactToDb', path: opts.path });
718
651
  skipDiskWrite = true;
719
652
  }
720
653
  }
@@ -724,7 +657,7 @@ export async function saveArtifactToDb(opts, basePath) {
724
657
  milestone_id: opts.milestone_id ?? null,
725
658
  slice_id: opts.slice_id ?? null,
726
659
  task_id: opts.task_id ?? null,
727
- full_content: dbContent,
660
+ full_content: contentToPersist,
728
661
  });
729
662
  // Write the file to disk (only if we're not preserving a richer existing file)
730
663
  if (!skipDiskWrite) {
@@ -732,9 +665,7 @@ export async function saveArtifactToDb(opts, basePath) {
732
665
  await saveFile(fullPath, contentToPersist);
733
666
  }
734
667
  catch (diskErr) {
735
- logError('manifest', 'disk write failed, rolling back DB row', { fn: 'saveArtifactToDb', error: String(diskErr.message) });
736
- db.deleteArtifactByPath(opts.path);
737
- throw diskErr;
668
+ logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: 'saveArtifactToDb', path: opts.path, error: String(diskErr.message) });
738
669
  }
739
670
  }
740
671
  // Invalidate file-read caches so deriveState() sees the updated markdown.
@@ -45,25 +45,21 @@ export function getPriorSliceCompletionBlocker(base, _mainBranch, unitType, unit
45
45
  // completion, which is wrong when the SUMMARY is a failure-path report
46
46
  // (verification FAILED, blocker placeholder, etc.). Resolve as follows:
47
47
  // 1. When DB is available and status is closed → skip (authoritative).
48
- // 2. When SUMMARY exists but looks like a failure/blocker report →
49
- // do not short-circuit; fall through to the slice-level check so
50
- // the guard can still block dependents of an active milestone.
51
- // 3. Otherwise (SUMMARY without failure markers) → skip. Preserves
52
- // the #1716 contract where a completed milestone with unchecked
53
- // remediation slices is still treated as done.
54
- const summaryPath = resolveMilestoneFile(base, mid, "SUMMARY");
48
+ // 2. When DB is unavailable, legacy SUMMARY.md fallback may skip.
49
+ // DB-backed projects must not treat SUMMARY.md as authoritative.
55
50
  if (isDbAvailable()) {
56
51
  const milestoneRow = getMilestone(mid);
57
52
  if (milestoneRow && isClosedStatus(milestoneRow.status))
58
53
  continue;
59
54
  }
60
- if (summaryPath) {
55
+ else {
56
+ const summaryPath = resolveMilestoneFile(base, mid, "SUMMARY");
61
57
  let summaryContent = null;
62
58
  try {
63
- summaryContent = readFileSync(summaryPath, "utf-8");
59
+ summaryContent = summaryPath ? readFileSync(summaryPath, "utf-8") : null;
64
60
  }
65
61
  catch { /* ignore */ }
66
- if (!summaryContent || classifyMilestoneSummaryContent(summaryContent) !== "failure") {
62
+ if (summaryContent && classifyMilestoneSummaryContent(summaryContent) !== "failure") {
67
63
  continue;
68
64
  }
69
65
  }
@@ -1,12 +1,12 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { isDbAvailable, _getAdapter } from "./gsd-db.js";
4
- import { resolveMilestoneFile } from "./paths.js";
4
+ import { resolveGsdPathContract, resolveMilestoneFile } from "./paths.js";
5
5
  import { deriveState } from "./state.js";
6
6
  import { readEvents } from "./workflow-events.js";
7
7
  import { renderAllProjections } from "./workflow-projections.js";
8
8
  export async function checkEngineHealth(basePath, issues, fixesApplied) {
9
- const dbPath = join(basePath, ".gsd", "gsd.db");
9
+ const dbPath = resolveGsdPathContract(basePath).projectDb;
10
10
  if (!isDbAvailable() && existsSync(dbPath)) {
11
11
  issues.push({
12
12
  severity: "warning",
@@ -136,7 +136,7 @@ function openRawDb(path) {
136
136
  const Database = providerModule;
137
137
  return new Database(path);
138
138
  }
139
- export const SCHEMA_VERSION = 22;
139
+ export const SCHEMA_VERSION = 23;
140
140
  function indexExists(db, name) {
141
141
  return !!db.prepare("SELECT 1 as present FROM sqlite_master WHERE type = 'index' AND name = ?").get(name);
142
142
  }
@@ -299,7 +299,8 @@ function initSchema(db, fileBacked) {
299
299
  verification_uat TEXT NOT NULL DEFAULT '',
300
300
  definition_of_done TEXT NOT NULL DEFAULT '[]',
301
301
  requirement_coverage TEXT NOT NULL DEFAULT '',
302
- boundary_map_markdown TEXT NOT NULL DEFAULT ''
302
+ boundary_map_markdown TEXT NOT NULL DEFAULT '',
303
+ sequence INTEGER DEFAULT 0
303
304
  )
304
305
  `);
305
306
  db.exec(`
@@ -1131,6 +1132,16 @@ function migrateSchema(db) {
1131
1132
  ":applied_at": new Date().toISOString(),
1132
1133
  });
1133
1134
  }
1135
+ if (currentVersion < 23) {
1136
+ // v23: milestone queue ordering moves into the canonical DB. The
1137
+ // historical QUEUE-ORDER.json file remains a projection, but runtime
1138
+ // derivation must not read it as authoritative state.
1139
+ ensureColumn(db, "milestones", "sequence", "ALTER TABLE milestones ADD COLUMN sequence INTEGER DEFAULT 0");
1140
+ db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
1141
+ ":version": 23,
1142
+ ":applied_at": new Date().toISOString(),
1143
+ });
1144
+ }
1134
1145
  db.exec("COMMIT");
1135
1146
  }
1136
1147
  catch (err) {
@@ -1518,6 +1529,31 @@ export function getActiveRequirements() {
1518
1529
  superseded_by: null,
1519
1530
  }));
1520
1531
  }
1532
+ export function getRequirementCounts() {
1533
+ if (!currentDb) {
1534
+ return { active: 0, validated: 0, deferred: 0, outOfScope: 0, blocked: 0, total: 0 };
1535
+ }
1536
+ const rows = currentDb
1537
+ .prepare("SELECT lower(status) as status, COUNT(*) as count FROM requirements GROUP BY lower(status)")
1538
+ .all();
1539
+ const counts = { active: 0, validated: 0, deferred: 0, outOfScope: 0, blocked: 0, total: 0 };
1540
+ for (const row of rows) {
1541
+ const status = String(row["status"] ?? "");
1542
+ const count = Number(row["count"] ?? 0);
1543
+ counts.total += count;
1544
+ if (status === "active")
1545
+ counts.active += count;
1546
+ else if (status === "validated")
1547
+ counts.validated += count;
1548
+ else if (status === "deferred")
1549
+ counts.deferred += count;
1550
+ else if (status === "out-of-scope" || status === "out_of_scope")
1551
+ counts.outOfScope += count;
1552
+ else if (status === "blocked")
1553
+ counts.blocked += count;
1554
+ }
1555
+ return counts;
1556
+ }
1521
1557
  export function getDbOwnerPid() {
1522
1558
  return currentPid;
1523
1559
  }
@@ -2164,6 +2200,7 @@ function rowToMilestone(row) {
2164
2200
  definition_of_done: JSON.parse(row["definition_of_done"] || "[]"),
2165
2201
  requirement_coverage: row["requirement_coverage"] ?? "",
2166
2202
  boundary_map_markdown: row["boundary_map_markdown"] ?? "",
2203
+ sequence: Number(row["sequence"] ?? 0),
2167
2204
  };
2168
2205
  }
2169
2206
  function rowToArtifact(row) {
@@ -2180,7 +2217,7 @@ function rowToArtifact(row) {
2180
2217
  export function getAllMilestones() {
2181
2218
  if (!currentDb)
2182
2219
  return [];
2183
- const rows = currentDb.prepare("SELECT * FROM milestones ORDER BY id").all();
2220
+ const rows = currentDb.prepare("SELECT * FROM milestones ORDER BY CASE WHEN sequence > 0 THEN 0 ELSE 1 END, sequence, id").all();
2184
2221
  return rows.map(rowToMilestone);
2185
2222
  }
2186
2223
  export function getMilestone(id) {
@@ -2191,6 +2228,23 @@ export function getMilestone(id) {
2191
2228
  return null;
2192
2229
  return rowToMilestone(row);
2193
2230
  }
2231
+ export function setMilestoneQueueOrder(order) {
2232
+ if (!currentDb)
2233
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2234
+ currentDb.exec("BEGIN IMMEDIATE");
2235
+ try {
2236
+ currentDb.prepare("UPDATE milestones SET sequence = 0").run();
2237
+ const stmt = currentDb.prepare("UPDATE milestones SET sequence = :sequence WHERE id = :id");
2238
+ order.forEach((id, index) => {
2239
+ stmt.run({ ":id": id, ":sequence": index + 1 });
2240
+ });
2241
+ currentDb.exec("COMMIT");
2242
+ }
2243
+ catch (err) {
2244
+ currentDb.exec("ROLLBACK");
2245
+ throw err;
2246
+ }
2247
+ }
2194
2248
  /**
2195
2249
  * Update a milestone's status in the database.
2196
2250
  * Used by park/unpark to keep the DB in sync with the filesystem marker.
@@ -2358,6 +2412,8 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
2358
2412
  // fall through to the main DB's existing value (not a literal default)
2359
2413
  // so reconcile never silently clears state the main tree has recorded.
2360
2414
  const hasDecisionSource = wtInfo.some((col) => col["name"] === "source");
2415
+ const wtMilestoneInfo = adapter.prepare("PRAGMA wt.table_info('milestones')").all();
2416
+ const hasMilestoneSequence = wtMilestoneInfo.some((col) => col["name"] === "sequence");
2361
2417
  const wtSliceInfo = adapter.prepare("PRAGMA wt.table_info('slices')").all();
2362
2418
  const hasIsSketch = wtSliceInfo.some((col) => col["name"] === "is_sketch");
2363
2419
  const hasSketchScope = wtSliceInfo.some((col) => col["name"] === "sketch_scope");
@@ -2415,7 +2471,7 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
2415
2471
  id, title, status, depends_on, created_at, completed_at,
2416
2472
  vision, success_criteria, key_risks, proof_strategy,
2417
2473
  verification_contract, verification_integration, verification_operational, verification_uat,
2418
- definition_of_done, requirement_coverage, boundary_map_markdown
2474
+ definition_of_done, requirement_coverage, boundary_map_markdown, sequence
2419
2475
  )
2420
2476
  SELECT w.id, w.title,
2421
2477
  CASE
@@ -2433,7 +2489,8 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
2433
2489
  END,
2434
2490
  w.vision, w.success_criteria, w.key_risks, w.proof_strategy,
2435
2491
  w.verification_contract, w.verification_integration, w.verification_operational, w.verification_uat,
2436
- w.definition_of_done, w.requirement_coverage, w.boundary_map_markdown
2492
+ w.definition_of_done, w.requirement_coverage, w.boundary_map_markdown,
2493
+ ${hasMilestoneSequence ? "COALESCE(w.sequence, 0)" : "COALESCE(m.sequence, 0)"}
2437
2494
  FROM wt.milestones w
2438
2495
  LEFT JOIN milestones m ON m.id = w.id
2439
2496
  `).run());
@@ -2659,6 +2716,15 @@ export function getAssessment(path) {
2659
2716
  const row = currentDb.prepare(`SELECT * FROM assessments WHERE path = :path`).get({ ":path": path });
2660
2717
  return row ?? null;
2661
2718
  }
2719
+ export function getLatestAssessmentByScope(milestoneId, scope) {
2720
+ if (!currentDb)
2721
+ return null;
2722
+ const row = currentDb.prepare(`SELECT * FROM assessments
2723
+ WHERE milestone_id = :mid AND scope = :scope
2724
+ ORDER BY created_at DESC
2725
+ LIMIT 1`).get({ ":mid": milestoneId, ":scope": scope });
2726
+ return row ?? null;
2727
+ }
2662
2728
  // ─── Quality Gates ───────────────────────────────────────────────────────
2663
2729
  function rowToGate(row) {
2664
2730
  return {
@@ -3027,10 +3093,10 @@ export function restoreManifest(manifest) {
3027
3093
  const msStmt = db.prepare(`INSERT INTO milestones (id, title, status, depends_on, created_at, completed_at,
3028
3094
  vision, success_criteria, key_risks, proof_strategy,
3029
3095
  verification_contract, verification_integration, verification_operational, verification_uat,
3030
- definition_of_done, requirement_coverage, boundary_map_markdown)
3031
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
3096
+ definition_of_done, requirement_coverage, boundary_map_markdown, sequence)
3097
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
3032
3098
  for (const m of manifest.milestones) {
3033
- msStmt.run(m.id, m.title, m.status, JSON.stringify(m.depends_on), m.created_at, m.completed_at, m.vision, JSON.stringify(m.success_criteria), JSON.stringify(m.key_risks), JSON.stringify(m.proof_strategy), m.verification_contract, m.verification_integration, m.verification_operational, m.verification_uat, JSON.stringify(m.definition_of_done), m.requirement_coverage, m.boundary_map_markdown);
3099
+ msStmt.run(m.id, m.title, m.status, JSON.stringify(m.depends_on), m.created_at, m.completed_at, m.vision, JSON.stringify(m.success_criteria), JSON.stringify(m.key_risks), JSON.stringify(m.proof_strategy), m.verification_contract, m.verification_integration, m.verification_operational, m.verification_uat, JSON.stringify(m.definition_of_done), m.requirement_coverage, m.boundary_map_markdown, m.sequence ?? 0);
3034
3100
  }
3035
3101
  // Restore slices (ADR-011 Phase 1: includes is_sketch + sketch_scope)
3036
3102
  const slStmt = db.prepare(`INSERT INTO slices (milestone_id, id, title, status, risk, depends, demo,
@@ -19,7 +19,7 @@ import { assessInterruptedSession, formatInterruptedSessionRunningMessage, forma
19
19
  import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
20
20
  import { resolveExpectedArtifactPath } from "./auto.js";
21
21
  import { gsdHome } from "./gsd-home.js";
22
- import { gsdRoot, milestonesDir, resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, relSliceFile, } from "./paths.js";
22
+ import { gsdRoot, milestonesDir, resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, relSliceFile, clearPathCache, } from "./paths.js";
23
23
  import { join } from "node:path";
24
24
  import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs";
25
25
  import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
@@ -486,12 +486,35 @@ export function maybeHandleReadyPhraseWithoutFiles(event) {
486
486
  const text = extractAssistantText(lastMsg);
487
487
  if (!READY_PHRASE_RE.test(text))
488
488
  return false;
489
+ // Bust paths.ts cached dir listings before checking for fresh writes. The
490
+ // LLM's Write tool calls do not invalidate paths.ts caches, so a stale
491
+ // listing taken before the milestone dir or its CONTEXT/ROADMAP files
492
+ // existed would falsely report the artifacts as missing and trigger the
493
+ // 3-strike "ready without files" abort even though the writes succeeded.
494
+ clearPathCache();
489
495
  // Gate: artifacts must still be missing — if they exist, the happy path
490
496
  // already fired and we have nothing to do.
491
497
  const contextFile = resolveMilestoneFile(basePath, milestoneId, "CONTEXT");
492
498
  const roadmapFile = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
493
499
  if (contextFile || roadmapFile)
494
500
  return false;
501
+ // Diagnostic: when the cached resolver reports both files missing, also probe
502
+ // the canonical paths with uncached existsSync so we can tell whether the
503
+ // recovery is firing on real-missing files or a path-resolution miss
504
+ // (basePath/symlink mismatch, stale cache despite agent-end-recovery flush,
505
+ // legacy descriptor dir not matching, etc.).
506
+ try {
507
+ const mDir = resolveMilestonePath(basePath, milestoneId);
508
+ const canonicalCtx = mDir ? join(mDir, `${milestoneId}-CONTEXT.md`) : null;
509
+ const canonicalRoadmap = mDir ? join(mDir, `${milestoneId}-ROADMAP.md`) : null;
510
+ logWarning("guided", `ready-phrase-reject diagnostic mid=${milestoneId} basePath=${basePath} ` +
511
+ `mDir=${mDir ?? "null"} ` +
512
+ `canonical-ctx=${canonicalCtx ?? "null"} ctx-exists=${canonicalCtx ? existsSync(canonicalCtx) : "n/a"} ` +
513
+ `canonical-roadmap=${canonicalRoadmap ?? "null"} roadmap-exists=${canonicalRoadmap ? existsSync(canonicalRoadmap) : "n/a"}`);
514
+ }
515
+ catch (e) {
516
+ logWarning("guided", `ready-phrase-reject diagnostic failed: ${e.message}`);
517
+ }
495
518
  entry.readyRejectCount = (entry.readyRejectCount ?? 0) + 1;
496
519
  if (entry.readyRejectCount > MAX_READY_REJECTS) {
497
520
  // Give up: clear state and tell the user to re-run /gsd. Avoids an
@@ -576,14 +599,14 @@ export function maybeHandleEmptyIntentTurn(event, isAuto) {
576
599
  // path, handled by maybeHandleReadyPhraseWithoutFiles.
577
600
  if (READY_PHRASE_RE.test(text))
578
601
  return false;
579
- // Skip if the LLM is clearly handing back to the user. Last-line `?` is
580
- // the strongest signal, but discuss flows often end with a freeform
581
- // question followed by a closing remark ("…what should we build? I'll
582
- // pick one if you don't care."). Treat ANY non-empty line ending in `?`
583
- // as a question-asked signal false negatives here auto-reply to the
602
+ // Skip if the LLM is clearly handing back to the user. Discuss flows
603
+ // often pose a question and follow it with a conditional intent on the
604
+ // same line ("Did I capture that correctly? If so, I'll write the
605
+ // requirements."). A line-trailing `?` check misses these because the
606
+ // line ends in `.`. Match any sentence-terminating `?` (followed by
607
+ // whitespace or end-of-text) — false negatives here auto-reply to the
584
608
  // user, which is a much worse failure mode than a missed nudge.
585
- const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
586
- if (lines.some((l) => l.endsWith("?")))
609
+ if (/\?(?:\s|$)/.test(text))
587
610
  return false;
588
611
  // Must contain a commit-intent phrase — this is the stall we care about.
589
612
  if (!COMMIT_INTENT_RE.test(text))
@@ -1,9 +1,8 @@
1
1
  // GSD Markdown Renderer — DB → Markdown file generation
2
2
  //
3
3
  // Transforms DB state into correct markdown files on disk.
4
- // Each render function reads from DB (with disk fallback),
5
- // patches content to match DB status, writes atomically to disk,
6
- // stores updated content in the artifacts table, and invalidates caches.
4
+ // Each render function reads from DB, writes a markdown projection to disk,
5
+ // stores generated content in the artifacts table, and invalidates caches.
7
6
  //
8
7
  // Critical invariant: rendered markdown must round-trip through
9
8
  // parseRoadmap(), parsePlan(), parseSummary() in files.ts.
@@ -60,45 +59,16 @@ function taskSummaryForSlicePlan(description) {
60
59
  return firstBlock || beforeHeading;
61
60
  }
62
61
  /**
63
- * Load artifact content from DB first, falling back to reading from disk.
64
- * On disk fallback, stores the content in the artifacts table for future use.
65
- * Returns null if content is unavailable from both sources.
62
+ * Load artifact content from the DB. Markdown projections are not authoritative
63
+ * during runtime; when the artifact row is missing, callers regenerate from DB
64
+ * rows instead of patching disk fallback content and storing it back.
66
65
  */
67
- function loadArtifactContent(artifactPath, absPath, opts) {
68
- // Try DB first
66
+ function loadArtifactContent(artifactPath) {
69
67
  const artifact = getArtifact(artifactPath);
70
68
  if (artifact && artifact.full_content) {
71
69
  return artifact.full_content;
72
70
  }
73
- // Fall back to disk
74
- if (!absPath) {
75
- process.stderr.write(`markdown-renderer: artifact not found in DB or on disk: ${artifactPath}\n`);
76
- return null;
77
- }
78
- let content;
79
- try {
80
- content = readFileSync(absPath, "utf-8");
81
- }
82
- catch {
83
- logWarning("renderer", `cannot read file from disk: ${absPath}`);
84
- return null;
85
- }
86
- // Store in DB for future use (graceful degradation path)
87
- try {
88
- insertArtifact({
89
- path: artifactPath,
90
- artifact_type: opts.artifact_type,
91
- milestone_id: opts.milestone_id,
92
- slice_id: opts.slice_id ?? null,
93
- task_id: opts.task_id ?? null,
94
- full_content: content,
95
- });
96
- }
97
- catch {
98
- // Non-fatal: we have the content, DB storage is best-effort
99
- logWarning("renderer", `failed to store disk fallback in DB: ${artifactPath}`);
100
- }
101
- return content;
71
+ return null;
102
72
  }
103
73
  /**
104
74
  * Write rendered content to disk and update the artifacts table.
@@ -401,17 +371,14 @@ export async function renderRoadmapCheckboxes(basePath, milestoneId) {
401
371
  }
402
372
  const absPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
403
373
  const artifactPath = absPath ? toArtifactPath(absPath, basePath) : null;
404
- // Load content from DB (with disk fallback)
374
+ // Load content from DB; regenerate from DB rows when the artifact is absent.
405
375
  let content = null;
406
376
  if (artifactPath) {
407
- content = loadArtifactContent(artifactPath, absPath, {
408
- artifact_type: "ROADMAP",
409
- milestone_id: milestoneId,
410
- });
377
+ content = loadArtifactContent(artifactPath);
411
378
  }
412
379
  if (!content) {
413
- process.stderr.write(`markdown-renderer: no roadmap content available for ${milestoneId}\n`);
414
- return false;
380
+ await renderRoadmapFromDb(basePath, milestoneId);
381
+ return true;
415
382
  }
416
383
  // Apply checkbox patches for each slice
417
384
  let updated = content;
@@ -454,15 +421,11 @@ export async function renderPlanCheckboxes(basePath, milestoneId, sliceId) {
454
421
  const artifactPath = absPath ? toArtifactPath(absPath, basePath) : null;
455
422
  let content = null;
456
423
  if (artifactPath) {
457
- content = loadArtifactContent(artifactPath, absPath, {
458
- artifact_type: "PLAN",
459
- milestone_id: milestoneId,
460
- slice_id: sliceId,
461
- });
424
+ content = loadArtifactContent(artifactPath);
462
425
  }
463
426
  if (!content) {
464
- process.stderr.write(`markdown-renderer: no plan content available for ${milestoneId}/${sliceId}\n`);
465
- return false;
427
+ await renderPlanFromDb(basePath, milestoneId, sliceId);
428
+ return true;
466
429
  }
467
430
  // Apply checkbox patches for each task
468
431
  let updated = content;