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
@@ -107,6 +107,7 @@ function makeMilestoneRow(overrides?: Partial<MilestoneRow>): MilestoneRow {
107
107
  definition_of_done: [],
108
108
  requirement_coverage: '',
109
109
  boundary_map_markdown: '',
110
+ sequence: 0,
110
111
  ...overrides,
111
112
  };
112
113
  }
@@ -40,6 +40,12 @@ test("auto.ts validates milestone before restoring paused session (#1664)", () =
40
40
  "auto.ts must check for SUMMARY file to detect completed milestones",
41
41
  );
42
42
 
43
+ assert.ok(
44
+ source.includes("await ensureDbOpen(base)") &&
45
+ source.indexOf("await ensureDbOpen(base)") < source.indexOf('resolveMilestoneFile(base, meta.milestoneId, "SUMMARY")'),
46
+ "auto.ts must open the canonical DB before using SUMMARY as a paused-session fallback",
47
+ );
48
+
43
49
  // Resume path must sanitize paused session file metadata before unlink/recovery.
44
50
  assert.ok(
45
51
  source.includes("normalizeSessionFilePath(meta.sessionFile ?? null)"),
@@ -1,13 +1,12 @@
1
1
  /**
2
- * Regression test for #3673 — auto-remediate stale slice DB status
2
+ * Regression test for DB-authoritative rogue detection.
3
3
  *
4
- * When complete-slice fails after writing SUMMARY.md but before calling
5
- * updateSliceStatus(), the DB stays stale and the post-unit check
6
- * previously reported this as a "rogue" artifact, causing infinite
7
- * re-dispatch. The fix calls updateSliceStatus() to sync the DB.
4
+ * A SUMMARY.md on disk is a projection/diagnostic. Runtime post-unit checks
5
+ * must not use it to mark the DB slice complete; explicit import/recovery
6
+ * commands own markdown-to-DB behavior.
8
7
  *
9
- * This structural test verifies updateSliceStatus is imported and called
10
- * in the complete-slice branch of auto-post-unit.ts.
8
+ * This structural test verifies the complete-slice rogue branch reports the
9
+ * stale projection without calling updateSliceStatus().
11
10
  */
12
11
 
13
12
  import { describe, test } from 'node:test';
@@ -22,38 +21,26 @@ const __dirname = dirname(__filename);
22
21
 
23
22
  const source = readFileSync(join(__dirname, '..', 'auto-post-unit.ts'), 'utf-8');
24
23
 
25
- describe('auto-remediate stale slice status (#3673)', () => {
26
- test('updateSliceStatus is imported from gsd-db', () => {
27
- assert.match(source, /import\s*\{[^}]*updateSliceStatus[^}]*\}\s*from\s*["']\.\/gsd-db/,
28
- 'updateSliceStatus should be imported from gsd-db');
24
+ describe('DB-authoritative slice rogue detection', () => {
25
+ test('updateSliceStatus is not imported for post-unit rogue reconciliation', () => {
26
+ assert.doesNotMatch(source, /import\s*\{[^}]*updateSliceStatus[^}]*\}\s*from\s*["']\.\/gsd-db/,
27
+ 'auto-post-unit must not import updateSliceStatus for disk-to-DB reconciliation');
29
28
  });
30
29
 
31
- test('updateSliceStatus is called with "complete" status', () => {
32
- assert.match(source, /updateSliceStatus\(mid,\s*sid,\s*["']complete["']/,
33
- 'updateSliceStatus should be called with "complete" status');
30
+ test('complete-slice rogue branch does not mark DB complete from disk', () => {
31
+ assert.doesNotMatch(source, /updateSliceStatus\(mid,\s*sid,\s*["']complete["']/,
32
+ 'SUMMARY.md on disk must not mark slice complete in DB');
34
33
  });
35
34
 
36
- test('remediation is wrapped in try-catch for fallback to rogue detection', () => {
37
- // The updateSliceStatus call should be in a try block with a catch
38
- // that falls back to rogues.push
39
- const updateIdx = source.indexOf('updateSliceStatus(mid, sid');
40
- assert.ok(updateIdx > 0, 'updateSliceStatus call should exist');
41
-
42
- // Find surrounding try-catch
43
- const before = source.slice(Math.max(0, updateIdx - 200), updateIdx);
44
- assert.match(before, /try\s*\{/,
45
- 'updateSliceStatus should be inside a try block');
46
-
47
- // Bound the region to stop before the rogue fallback so /catch/ only
48
- // matches this try block's catch, not an unrelated later one.
49
- const after = extractSourceRegion(source, 'updateSliceStatus(mid, sid', 'rogues.push({');
50
- assert.match(after, /catch/,
51
- 'try block should have a catch for fallback');
35
+ test('explicit rogue diagnostic reports stale slice summary projection', () => {
36
+ const branch = extractSourceRegion(source, 'unitType === "complete-slice"', 'unitType === "plan-milestone"');
37
+ assert.match(branch, /rogues\.push\(\{\s*path:\s*summaryPath,\s*unitType,\s*unitId\s*\}\)/,
38
+ 'complete-slice branch should report stale SUMMARY.md as rogue');
52
39
  });
53
40
 
54
- test('rogue detection still exists as fallback', () => {
55
- // rogues.push should appear in the catch block
56
- assert.match(source, /rogues\.push\(\{.*path:\s*summaryPath/,
57
- 'rogues.push fallback should still exist');
41
+ test('post-unit runtime does not call rogue diagnostics automatically', () => {
42
+ const postUnit = extractSourceRegion(source, 'export async function postUnitPostVerification');
43
+ assert.doesNotMatch(postUnit, /detectRogueFileWrites\(/,
44
+ 'runtime post-unit path must not scan disk projections for rogue files');
58
45
  });
59
46
  });
@@ -41,7 +41,7 @@ const VALID_PARAMS = {
41
41
  ],
42
42
  };
43
43
 
44
- describe("complete-task rollback cleans up verification_evidence (#2724)", () => {
44
+ describe("complete-task projection failures keep DB completion committed", () => {
45
45
  let base: string;
46
46
 
47
47
  afterEach(() => {
@@ -75,7 +75,7 @@ describe("complete-task rollback cleans up verification_evidence (#2724)", () =>
75
75
  assert.equal(rows.length, 2, "should have 2 evidence rows after success");
76
76
  });
77
77
 
78
- it("deletes verification_evidence rows on disk-render rollback", async () => {
78
+ it("keeps task completion and verification_evidence when disk projection write fails", async () => {
79
79
  base = makeTmpBase();
80
80
  openDatabase(join(base, ".gsd", "gsd.db"));
81
81
  insertMilestone({ id: "M001" });
@@ -87,20 +87,19 @@ describe("complete-task rollback cleans up verification_evidence (#2724)", () =>
87
87
  writeFileSync(tasksDir, "not-a-directory");
88
88
 
89
89
  const result = await handleCompleteTask(VALID_PARAMS, base);
90
- assert.ok("error" in result, "should return error when disk write fails");
90
+ assert.ok(!("error" in result), `unexpected error: ${"error" in result ? result.error : ""}`);
91
+ assert.equal(result.stale, true, "result should report stale projection");
91
92
 
92
- // Task should be rolled back to pending
93
93
  const adapter = _getAdapter()!;
94
94
  const task = adapter.prepare(
95
95
  `SELECT status FROM tasks WHERE milestone_id = 'M001' AND slice_id = 'S01' AND id = 'T01'`,
96
96
  ).get() as { status: string } | undefined;
97
97
  assert.ok(task, "task row should still exist");
98
- assert.equal(task!.status, "pending", "task status should be rolled back to pending");
98
+ assert.equal(task!.status, "complete", "task status should remain complete");
99
99
 
100
- // Verification evidence should be cleaned up — no orphaned rows
101
100
  const evidenceRows = adapter.prepare(
102
101
  `SELECT * FROM verification_evidence WHERE task_id = 'T01' AND slice_id = 'S01' AND milestone_id = 'M001'`,
103
102
  ).all();
104
- assert.equal(evidenceRows.length, 0, "verification_evidence should be empty after rollback");
103
+ assert.equal(evidenceRows.length, 2, "verification_evidence should remain committed");
105
104
  });
106
105
  });
@@ -403,12 +403,11 @@ console.log('\n=== complete-task: handler idempotency ===');
403
403
  const r1 = await handleCompleteTask(params, basePath);
404
404
  assertTrue(!('error' in r1), 'first call should succeed');
405
405
 
406
- // Verify complete-task did not duplicate T01. State reconciliation may import
407
- // the remaining plan task from disk so the DB stays aligned with S01-PLAN.md.
406
+ // Verify complete-task did not duplicate T01. S01-PLAN.md is a projection,
407
+ // so the remaining plan task is not imported implicitly.
408
408
  const tasks = getSliceTasks('M001', 'S01');
409
- assertEq(tasks.length, 2, 'should have T01 plus reconciled T02 after first call');
409
+ assertEq(tasks.length, 1, 'should only have the completed DB task after first call');
410
410
  assertEq(tasks.filter(t => t.id === 'T01').length, 1, 'should have exactly one T01 row after first call');
411
- assertEq(tasks.find(t => t.id === 'T02')?.status, 'pending', 'T02 should be reconciled as pending');
412
411
 
413
412
  // Second call with same params — state machine guard rejects (task is already complete)
414
413
  const r2 = await handleCompleteTask(params, basePath);
@@ -419,7 +418,7 @@ console.log('\n=== complete-task: handler idempotency ===');
419
418
 
420
419
  // Still no duplicate rows from the rejected second call.
421
420
  const tasksAfter = getSliceTasks('M001', 'S01');
422
- assertEq(tasksAfter.length, 2, 'should still have T01 plus reconciled T02 after rejected second call');
421
+ assertEq(tasksAfter.length, 1, 'should still only have T01 after rejected second call');
423
422
  assertEq(tasksAfter.filter(t => t.id === 'T01').length, 1, 'should still have exactly one T01 row');
424
423
 
425
424
  cleanupDir(basePath);
@@ -447,10 +446,13 @@ console.log('\n=== complete-task: handler with missing plan file ===');
447
446
  const params = makeValidParams();
448
447
  const result = await handleCompleteTask(params, basePath);
449
448
 
450
- // Should succeed even without plan file just skip checkbox toggle
449
+ // Should succeed and regenerate the missing plan projection from DB.
451
450
  assertTrue(!('error' in result), 'handler should succeed without plan file');
452
451
  if (!('error' in result)) {
453
452
  assertTrue(fs.existsSync(result.summaryPath), 'summary should be written even without plan file');
453
+ const planPath = path.join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'S01-PLAN.md');
454
+ assertTrue(fs.existsSync(planPath), 'missing plan projection should be regenerated from DB');
455
+ assertTrue(fs.readFileSync(planPath, 'utf-8').includes('[x] **T01:'), 'regenerated plan should reflect DB task completion');
454
456
  }
455
457
 
456
458
  cleanupDir(basePath);
@@ -1,15 +1,9 @@
1
1
  /**
2
- * Behavioural regression test for #4129.
2
+ * Behavioural regression test for DB-authoritative task completion.
3
3
  *
4
- * When deriveStateFromDb's reconcileSliceTasks finds a SUMMARY.md on disk
5
- * for a task whose DB row is still pending, it flips the row to "complete".
6
- * Before #4129, the call to updateTaskStatus omitted the completedAt
7
- * timestamp, leaving completed_at NULL forever.
8
- *
9
- * The fix passes new Date().toISOString() as the 5th argument; this test
10
- * exercises that path end-to-end and asserts the column is populated.
11
- *
12
- * Refs #4829 (rewrite from positional source-grep).
4
+ * A task SUMMARY.md on disk is a projection, not a completion command.
5
+ * deriveStateFromDb must not flip a pending DB task to complete or invent a
6
+ * completed_at timestamp from disk evidence.
13
7
  */
14
8
 
15
9
  import { describe, test, beforeEach, afterEach } from 'node:test';
@@ -46,27 +40,26 @@ function setupProject(): void {
46
40
  `# M001\n\n## Slices\n\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n - After this: works\n`,
47
41
  );
48
42
 
49
- // Plan file for the slice so reconcile can populate task list if DB is empty.
43
+ // Plan file for the slice. It is a projection and must not drive DB state.
50
44
  writeFileSync(
51
45
  join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'S01-PLAN.md'),
52
46
  `# S01: Slice\n\n## Tasks\n\n- [ ] **T01: Test task** \`est:30m\`\n - Do: x\n - Verify: y\n`,
53
47
  );
54
48
 
55
- // The summary file: this is the on-disk evidence that flips the task
56
- // status to "complete" inside reconcileSliceTasks.
49
+ // The summary file is a projection and must not complete the task.
57
50
  writeFileSync(
58
51
  join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks', 'T01-SUMMARY.md'),
59
52
  '---\nid: T01\nparent: S01\nmilestone: M001\nblocker_discovered: false\n---\n# T01\n',
60
53
  );
61
54
  }
62
55
 
63
- describe('completed_at reconcile (#4129)', () => {
56
+ describe('completed_at DB-authoritative derivation', () => {
64
57
  beforeEach(() => {
65
58
  setupProject();
66
59
  openDatabase(join(basePath, '.gsd', 'gsd.db'));
67
60
  insertMilestone({ id: 'M001', title: 'M001', status: 'active' });
68
61
  insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Slice', status: 'active' });
69
- // Task is "pending" in DB, but SUMMARY.md exists on disk → reconcile flips it.
62
+ // Task is "pending" in DB, even though SUMMARY.md exists on disk.
70
63
  insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Test task', status: 'pending' });
71
64
  invalidateStateCache();
72
65
  });
@@ -76,24 +69,16 @@ describe('completed_at reconcile (#4129)', () => {
76
69
  try { rmSync(basePath, { recursive: true, force: true }); } catch { /* */ }
77
70
  });
78
71
 
79
- test('reconcileSliceTasks sets completed_at when flipping a pending task to complete via SUMMARY.md', async () => {
72
+ test('deriveStateFromDb does not set completed_at from a disk SUMMARY projection', async () => {
80
73
  const before = getTask('M001', 'S01', 'T01');
81
74
  assert.strictEqual(before?.status, 'pending', 'task starts pending');
82
75
  assert.strictEqual(before?.completed_at, null, 'task starts with completed_at NULL');
83
76
 
84
- // Trigger the reconcile path (state.ts reconcileSliceTasks).
77
+ // Derive runtime state. Disk SUMMARY.md must not mutate the DB row.
85
78
  await deriveStateFromDb(basePath);
86
79
 
87
80
  const after = getTask('M001', 'S01', 'T01');
88
- assert.strictEqual(after?.status, 'complete', 'task should be flipped to complete');
89
- assert.ok(
90
- typeof after?.completed_at === 'string' && after.completed_at.length > 0,
91
- `completed_at must be populated by reconcileSliceTasks (#4129); got ${JSON.stringify(after?.completed_at)}`,
92
- );
93
- // Sanity: timestamp parses as a valid ISO date.
94
- assert.ok(
95
- !Number.isNaN(Date.parse(after!.completed_at!)),
96
- `completed_at should be a valid ISO timestamp, got ${after!.completed_at}`,
97
- );
81
+ assert.strictEqual(after?.status, 'pending', 'task remains pending');
82
+ assert.strictEqual(after?.completed_at, null, 'completed_at remains NULL');
98
83
  });
99
84
  });
@@ -58,13 +58,26 @@ test("#2313: syncStateToProjectRoot should sync metrics.json", () => {
58
58
  );
59
59
  });
60
60
 
61
- test("#2313: syncWorktreeStateBack should include metrics.json in ROOT_STATE_FILES", () => {
61
+ test("syncStateToProjectRoot should back-sync completed-units.json", () => {
62
+ const syncSrcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
63
+ const syncSrc = readFileSync(syncSrcPath, "utf-8");
64
+ const fnIdx = syncSrc.indexOf("export function syncStateToProjectRoot(");
65
+ assert.ok(fnIdx !== -1, "syncStateToProjectRoot exists");
66
+ const fnBlock = syncSrc.slice(fnIdx, syncSrc.indexOf("// ─── Resource Staleness", fnIdx));
67
+
68
+ assert.ok(
69
+ fnBlock.includes('"completed-units.json"'),
70
+ "syncStateToProjectRoot should copy completed-units.json back to the project root",
71
+ );
72
+ });
73
+
74
+ test("#2313: syncWorktreeStateBack should include metrics.json in ROOT_DIAGNOSTIC_FILES", () => {
62
75
  const autoWorktreeSrcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
63
76
  const autoWorktreeSrc = readFileSync(autoWorktreeSrcPath, "utf-8");
64
77
 
65
- // Find the ROOT_STATE_FILES constant (single source of truth for both sync directions)
66
- const constIdx = autoWorktreeSrc.indexOf("ROOT_STATE_FILES");
67
- assert.ok(constIdx !== -1, "ROOT_STATE_FILES constant exists");
78
+ // Find the ROOT_DIAGNOSTIC_FILES constant used for worktree copy-back.
79
+ const constIdx = autoWorktreeSrc.indexOf("ROOT_DIAGNOSTIC_FILES");
80
+ assert.ok(constIdx !== -1, "ROOT_DIAGNOSTIC_FILES constant exists");
68
81
 
69
82
  // Get the array content
70
83
  const arrayStart = autoWorktreeSrc.indexOf("[", constIdx);
@@ -73,7 +86,7 @@ test("#2313: syncWorktreeStateBack should include metrics.json in ROOT_STATE_FIL
73
86
 
74
87
  assert.ok(
75
88
  rootFilesBlock.includes("metrics.json"),
76
- "metrics.json should be in ROOT_STATE_FILES list",
89
+ "metrics.json should be in ROOT_DIAGNOSTIC_FILES list",
77
90
  );
78
91
  });
79
92
 
@@ -47,7 +47,7 @@ const symlinkResult = resolveProjectRootDbPath(symlinkPath);
47
47
  assertEq(
48
48
  symlinkResult,
49
49
  join("/home/user/myproject/.gsd/projects/abc123def", "gsd.db"),
50
- "/.gsd/projects/<hash>/worktrees/ resolves to hash-level DB (#2517, updated for #2952)",
50
+ "/.gsd/projects/<hash>/worktrees/ resolves to external project state DB",
51
51
  );
52
52
 
53
53
  // Windows-style separators for symlink layout
@@ -57,7 +57,7 @@ if (sep === "\\") {
57
57
  assertEq(
58
58
  winResult,
59
59
  join("C:\\Users\\dev\\project\\.gsd\\projects\\abc123def", "gsd.db"),
60
- "Windows /.gsd/projects/<hash>/worktrees/ resolves to hash-level DB",
60
+ "Windows /.gsd/projects/<hash>/worktrees/ resolves to external project state DB",
61
61
  );
62
62
  } else {
63
63
  // On non-Windows, test forward-slash variant explicitly
@@ -66,7 +66,7 @@ if (sep === "\\") {
66
66
  assertEq(
67
67
  fwdResult,
68
68
  join("/home/user/myproject/.gsd/projects/abc123def", "gsd.db"),
69
- "Forward-slash /.gsd/projects/<hash>/worktrees/ resolves to hash-level DB on POSIX",
69
+ "Forward-slash /.gsd/projects/<hash>/worktrees/ resolves to external project state DB on POSIX",
70
70
  );
71
71
  }
72
72
 
@@ -76,7 +76,7 @@ const deepResult = resolveProjectRootDbPath(deepSymlinkPath);
76
76
  assertEq(
77
77
  deepResult,
78
78
  join("/home/user/myproject/.gsd/projects/deadbeef42", "gsd.db"),
79
- "Deep /.gsd/projects/<hash>/worktrees/ path resolves to hash-level DB (#2952)",
79
+ "Deep /.gsd/projects/<hash>/worktrees/ path resolves to external project state DB",
80
80
  );
81
81
 
82
82
  // Non-worktree path should be unchanged
@@ -481,7 +481,7 @@ describe('db-writer', () => {
481
481
  }
482
482
  });
483
483
 
484
- test('updateRequirementInDb — seeds from REQUIREMENTS.md when DB empty (#3346)', async () => {
484
+ test('updateRequirementInDb — ignores REQUIREMENTS.md projection when DB empty', async () => {
485
485
  const tmpDir = makeTmpDir();
486
486
  const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
487
487
  openDatabase(dbPath);
@@ -515,31 +515,28 @@ describe('db-writer', () => {
515
515
  ].join('\n');
516
516
  fs.writeFileSync(path.join(tmpDir, '.gsd', 'REQUIREMENTS.md'), reqContent);
517
517
 
518
- // DB is empty no requirements seeded. Update R005 to "validated".
519
- // Before #3346 fix: this would create a skeleton with empty fields.
520
- // After fix: this seeds all 3 requirements from REQUIREMENTS.md first.
518
+ // DB is empty. REQUIREMENTS.md is a projection and must not be imported
519
+ // implicitly by a runtime DB write.
521
520
  await updateRequirementInDb('R005', {
522
521
  status: 'validated',
523
522
  validation: 'S02 — auth flow verified',
524
523
  }, tmpDir);
525
524
 
526
- // R005 should have the update AND the original content from markdown
525
+ // R005 should have the requested update only; disk projection content is ignored.
527
526
  const r005 = getRequirementById('R005');
528
527
  assert.ok(r005, 'R005 should exist');
529
528
  assert.equal(r005!.status, 'validated', 'status should be updated');
530
529
  assert.equal(r005!.validation, 'S02 — auth flow verified', 'validation should be updated');
531
- assert.equal(r005!.class, 'functional', 'class should be preserved from REQUIREMENTS.md');
532
- assert.ok(r005!.description?.includes('authentication') || r005!.full_content?.includes('authentication'),
533
- 'original content should be preserved');
530
+ assert.equal(r005!.class, '', 'class should not be imported from REQUIREMENTS.md');
531
+ assert.ok(!r005!.description?.includes('authentication'), 'description should not be imported');
532
+ assert.ok(!r005!.full_content?.includes('authentication'), 'full content should not be imported');
534
533
 
535
- // R007 and R001 should also be seeded (not just the one being updated)
534
+ // Other requirements in the projection are not seeded.
536
535
  const r007 = getRequirementById('R007');
537
- assert.ok(r007, 'R007 should be seeded from REQUIREMENTS.md');
538
- assert.equal(r007!.status, 'active', 'R007 status should be active');
536
+ assert.equal(r007, null, 'R007 should not be imported from REQUIREMENTS.md');
539
537
 
540
538
  const r001 = getRequirementById('R001');
541
- assert.ok(r001, 'R001 should be seeded from REQUIREMENTS.md');
542
- assert.equal(r001!.status, 'validated', 'R001 status should be validated (from section heading)');
539
+ assert.equal(r001, null, 'R001 should not be imported from REQUIREMENTS.md');
543
540
  } finally {
544
541
  closeDatabase();
545
542
  cleanupDir(tmpDir);
@@ -663,15 +660,16 @@ describe('db-writer', () => {
663
660
  'disk file preserved — shrinkage guard prevented overwrite',
664
661
  );
665
662
 
666
- // DB should contain the full disk content, not the abbreviated content
663
+ // DB should keep the caller-provided content. The larger disk file is a
664
+ // stale projection, not runtime authority.
667
665
  const adapter = _getAdapter();
668
666
  const row = adapter!
669
667
  .prepare('SELECT full_content FROM artifacts WHERE path = ?')
670
668
  .get(relPath);
671
669
  assert.deepStrictEqual(
672
670
  row!['full_content'],
673
- fullContent,
674
- 'DB stores the richer disk content instead of abbreviated content',
671
+ abbreviatedContent,
672
+ 'DB stores caller-provided content instead of importing disk projection content',
675
673
  );
676
674
  } finally {
677
675
  closeDatabase();
@@ -20,7 +20,7 @@ import {
20
20
  insertSlice,
21
21
  insertTask,
22
22
  } from '../gsd-db.ts';
23
- import { migrateHierarchyToDb } from '../md-importer.ts';
23
+ import { migrateFromMarkdown, migrateHierarchyToDb } from '../md-importer.ts';
24
24
  import type { GSDState } from '../types.ts';
25
25
 
26
26
  // ─── Fixture Helpers ───────────────────────────────────────────────────────
@@ -479,12 +479,13 @@ skills_used: []
479
479
 
480
480
  // Step 2: Migrate markdown to DB
481
481
  openDatabase(':memory:');
482
- const counts = migrateHierarchyToDb(base);
482
+ const counts = migrateFromMarkdown(base);
483
483
 
484
484
  // Verify migration populated correctly
485
- assert.ok(counts.milestones >= 1, 'G-roundtrip: migrated milestones');
486
- assert.ok(counts.slices >= 2, 'G-roundtrip: migrated slices');
487
- assert.ok(counts.tasks >= 3, 'G-roundtrip: migrated tasks');
485
+ assert.ok(counts.hierarchy.milestones >= 1, 'G-roundtrip: migrated milestones');
486
+ assert.ok(counts.hierarchy.slices >= 2, 'G-roundtrip: migrated slices');
487
+ assert.ok(counts.hierarchy.tasks >= 3, 'G-roundtrip: migrated tasks');
488
+ assert.equal(counts.requirements, 3, 'G-roundtrip: migrated requirements');
488
489
 
489
490
  // Step 3: Get DB-backed state
490
491
  invalidateStateCache();
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * derive-state-db-disk-reconcile.test.ts — #2416
3
3
  *
4
- * After migration to DB-backed state, milestones that exist on disk
5
- * (in .gsd/milestones/) but were never imported into the DB become
6
- * invisible to deriveStateFromDb(). This test verifies that
7
- * deriveStateFromDb reconciles disk milestones with DB milestones.
4
+ * DB-authoritative state: milestones that exist only as markdown projections
5
+ * are not imported by deriveStateFromDb(). Explicit migration/import is the
6
+ * only markdown-to-DB path.
8
7
  */
9
8
 
10
9
  import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
@@ -17,7 +16,6 @@ import {
17
16
  closeDatabase,
18
17
  insertMilestone,
19
18
  insertSlice,
20
- insertTask,
21
19
  } from "../gsd-db.ts";
22
20
  import { createTestContext } from "./test-helpers.ts";
23
21
 
@@ -58,7 +56,7 @@ const ROADMAP_CONTENT = `# M002: Disk-Only Milestone
58
56
  `;
59
57
 
60
58
  async function main(): Promise<void> {
61
- console.log("\n=== #2416: deriveStateFromDb reconciles disk milestones ===");
59
+ console.log("\n=== deriveStateFromDb does not reconcile disk milestones ===");
62
60
 
63
61
  // Set up: M001 in DB, M002 on disk only
64
62
  const base = createFixtureBase();
@@ -81,12 +79,9 @@ async function main(): Promise<void> {
81
79
  invalidateStateCache();
82
80
  const state = await deriveStateFromDb(base);
83
81
 
84
- // M002 should be visible in the registry
82
+ // M002 is disk-only and should not be visible in DB-backed state.
85
83
  const m002Entry = state.registry.find((m) => m.id === "M002");
86
- assertTrue(
87
- m002Entry !== undefined,
88
- "M002 (disk-only milestone) should appear in state.registry (#2416)",
89
- );
84
+ assertEq(m002Entry, undefined, "M002 disk-only milestone should not appear in DB-backed state");
90
85
 
91
86
  // M001 should still be in the registry
92
87
  const m001Entry = state.registry.find((m) => m.id === "M001");
@@ -95,24 +90,14 @@ async function main(): Promise<void> {
95
90
  "M001 (DB milestone) should still appear in state.registry",
96
91
  );
97
92
 
98
- // The active milestone should be M002 (since M001 is complete)
99
- assertTrue(
100
- state.activeMilestone !== null,
101
- "There should be an active milestone",
102
- );
103
- if (state.activeMilestone) {
104
- assertEq(
105
- state.activeMilestone.id,
106
- "M002",
107
- "Active milestone should be M002 (disk-only, not complete) (#2416)",
108
- );
109
- }
93
+ assertEq(state.activeMilestone, null, "No active milestone should be inferred from disk-only markdown");
94
+ assertEq(state.phase, "complete", "DB-only complete milestone drives complete state");
110
95
  } finally {
111
96
  closeDatabase();
112
97
  cleanup(base);
113
98
  }
114
99
 
115
- console.log("\n=== #4974: summary-only disk milestones keep parsed title ===");
100
+ console.log("\n=== summary-only disk milestones are not imported ===");
116
101
 
117
102
  {
118
103
  const summaryOnlyBase = createFixtureBase();
@@ -132,20 +117,7 @@ async function main(): Promise<void> {
132
117
  const state = await deriveStateFromDb(summaryOnlyBase);
133
118
  const m002Entry = state.registry.find((m) => m.id === "M002");
134
119
 
135
- assertTrue(
136
- m002Entry !== undefined,
137
- "M002 summary-only disk milestone should appear in state.registry (#4974)",
138
- );
139
- assertEq(
140
- m002Entry?.title,
141
- "Summary-Only Milestone",
142
- "M002 summary-only disk milestone should use parsed SUMMARY title (#4974)",
143
- );
144
- assertEq(
145
- m002Entry?.status,
146
- "complete",
147
- "M002 summary-only disk milestone should reconcile as complete (#4974)",
148
- );
120
+ assertEq(m002Entry, undefined, "M002 summary-only disk milestone should not appear without explicit import");
149
121
  } finally {
150
122
  closeDatabase();
151
123
  cleanup(summaryOnlyBase);