gsd-pi 2.44.0-dev.62b5d6c → 2.44.0-dev.848dd4c

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 (190) hide show
  1. package/README.md +30 -12
  2. package/dist/resources/extensions/gsd/auto-start.js +10 -0
  3. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +5 -0
  4. package/dist/web/standalone/.next/BUILD_ID +1 -1
  5. package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
  6. package/dist/web/standalone/.next/build-manifest.json +2 -2
  7. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  8. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  9. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  10. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  17. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/index.html +1 -1
  25. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
  32. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  33. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  34. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  35. package/package.json +1 -1
  36. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +6 -8
  37. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  38. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +24 -26
  39. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  40. package/packages/pi-coding-agent/dist/core/fs-utils.test.js +29 -48
  41. package/packages/pi-coding-agent/dist/core/fs-utils.test.js.map +1 -1
  42. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +34 -44
  43. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
  44. package/packages/pi-coding-agent/dist/core/session-manager.test.js +30 -34
  45. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  46. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +10 -12
  47. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
  48. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js +43 -47
  49. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js.map +1 -1
  50. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +7 -7
  51. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +26 -26
  52. package/packages/pi-coding-agent/src/core/fs-utils.test.ts +31 -43
  53. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +40 -45
  54. package/packages/pi-coding-agent/src/core/session-manager.test.ts +33 -33
  55. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +17 -17
  56. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts +74 -74
  57. package/src/resources/extensions/gsd/auto-start.ts +14 -0
  58. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +8 -0
  59. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +99 -99
  60. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +14 -16
  61. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +43 -57
  62. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +11 -13
  63. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +465 -523
  64. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +73 -75
  65. package/src/resources/extensions/gsd/tests/auto-start-needs-discussion.test.ts +34 -56
  66. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +533 -656
  67. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +165 -143
  68. package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +29 -52
  69. package/src/resources/extensions/gsd/tests/captures.test.ts +148 -176
  70. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +32 -33
  71. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +141 -143
  72. package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +25 -25
  73. package/src/resources/extensions/gsd/tests/commands-logs.test.ts +81 -81
  74. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +38 -59
  75. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +228 -263
  76. package/src/resources/extensions/gsd/tests/complete-task.test.ts +250 -302
  77. package/src/resources/extensions/gsd/tests/context-store.test.ts +354 -367
  78. package/src/resources/extensions/gsd/tests/continue-here.test.ts +68 -72
  79. package/src/resources/extensions/gsd/tests/cost-projection.test.ts +92 -106
  80. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +27 -35
  81. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +220 -237
  82. package/src/resources/extensions/gsd/tests/db-writer.test.ts +390 -420
  83. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +76 -92
  84. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +68 -83
  85. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +152 -183
  86. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +78 -101
  87. package/src/resources/extensions/gsd/tests/derive-state.test.ts +192 -227
  88. package/src/resources/extensions/gsd/tests/detection.test.ts +232 -278
  89. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +30 -34
  90. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +164 -180
  91. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +43 -49
  92. package/src/resources/extensions/gsd/tests/dispatch-uat-last-completed.test.ts +28 -32
  93. package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +27 -29
  94. package/src/resources/extensions/gsd/tests/doctor-delimiter-fix.test.ts +34 -38
  95. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +54 -75
  96. package/src/resources/extensions/gsd/tests/doctor-environment-worktree.test.ts +21 -32
  97. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +72 -97
  98. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +38 -44
  99. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +104 -145
  100. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +84 -106
  101. package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +54 -60
  102. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +72 -93
  103. package/src/resources/extensions/gsd/tests/doctor.test.ts +104 -134
  104. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +123 -131
  105. package/src/resources/extensions/gsd/tests/exit-command.test.ts +20 -24
  106. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +48 -57
  107. package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +5 -7
  108. package/src/resources/extensions/gsd/tests/flag-file-db.test.ts +30 -42
  109. package/src/resources/extensions/gsd/tests/freeform-decisions.test.ts +198 -206
  110. package/src/resources/extensions/gsd/tests/git-locale.test.ts +13 -27
  111. package/src/resources/extensions/gsd/tests/git-service.test.ts +285 -388
  112. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +31 -39
  113. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +63 -69
  114. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +255 -264
  115. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +108 -119
  116. package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +81 -103
  117. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +229 -262
  118. package/src/resources/extensions/gsd/tests/headless-answers.test.ts +13 -13
  119. package/src/resources/extensions/gsd/tests/health-widget.test.ts +29 -37
  120. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +81 -102
  121. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +16 -18
  122. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +41 -46
  123. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +42 -53
  124. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +75 -91
  125. package/src/resources/extensions/gsd/tests/integration-proof.test.ts +18 -18
  126. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +150 -194
  127. package/src/resources/extensions/gsd/tests/md-importer.test.ts +101 -125
  128. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +45 -54
  129. package/src/resources/extensions/gsd/tests/memory-store.test.ts +80 -93
  130. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +57 -66
  131. package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +83 -93
  132. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +161 -170
  133. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +125 -141
  134. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +107 -131
  135. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +87 -96
  136. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +125 -164
  137. package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +81 -94
  138. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +35 -36
  139. package/src/resources/extensions/gsd/tests/overrides.test.ts +99 -106
  140. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +40 -47
  141. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +25 -28
  142. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +66 -83
  143. package/src/resources/extensions/gsd/tests/park-edge-cases.test.ts +54 -77
  144. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +68 -115
  145. package/src/resources/extensions/gsd/tests/parsers.test.ts +546 -611
  146. package/src/resources/extensions/gsd/tests/paths.test.ts +72 -87
  147. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +77 -117
  148. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +56 -56
  149. package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +93 -119
  150. package/src/resources/extensions/gsd/tests/queue-order.test.ts +70 -82
  151. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +42 -55
  152. package/src/resources/extensions/gsd/tests/quick-auto-guard.test.ts +100 -0
  153. package/src/resources/extensions/gsd/tests/quick-branch-lifecycle.test.ts +45 -73
  154. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +28 -38
  155. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +73 -80
  156. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +71 -74
  157. package/src/resources/extensions/gsd/tests/requirements.test.ts +70 -75
  158. package/src/resources/extensions/gsd/tests/retry-state-reset.test.ts +44 -66
  159. package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +114 -181
  160. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +63 -65
  161. package/src/resources/extensions/gsd/tests/run-uat.test.ts +66 -128
  162. package/src/resources/extensions/gsd/tests/session-lock-multipath.test.ts +18 -25
  163. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +37 -44
  164. package/src/resources/extensions/gsd/tests/shared-wal.test.ts +19 -26
  165. package/src/resources/extensions/gsd/tests/sqlite-unavailable-gate.test.ts +63 -0
  166. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +6 -8
  167. package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +22 -28
  168. package/src/resources/extensions/gsd/tests/token-savings.test.ts +54 -56
  169. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +23 -25
  170. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +9 -11
  171. package/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +66 -82
  172. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +46 -47
  173. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +20 -22
  174. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +84 -86
  175. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +41 -43
  176. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +94 -96
  177. package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +11 -13
  178. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +27 -29
  179. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +50 -52
  180. package/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts +10 -13
  181. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +14 -18
  182. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +38 -39
  183. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -21
  184. package/src/resources/extensions/gsd/tests/worktree-health.test.ts +25 -30
  185. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +30 -37
  186. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +15 -22
  187. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +59 -66
  188. package/src/resources/extensions/gsd/tests/worktree.test.ts +44 -50
  189. /package/dist/web/standalone/.next/static/{fOnWQBjWXMKUs4bqTg530 → -zps1Q9mQmioAKLcQiCr8}/_buildManifest.js +0 -0
  190. /package/dist/web/standalone/.next/static/{fOnWQBjWXMKUs4bqTg530 → -zps1Q9mQmioAKLcQiCr8}/_ssgManifest.js +0 -0
@@ -6,15 +6,13 @@ import fs from "node:fs";
6
6
 
7
7
  import { loadFile } from "../files.ts";
8
8
 
9
- test("loadFile returns null for directory paths instead of throwing EISDIR", async () => {
9
+ test("loadFile returns null for directory paths instead of throwing EISDIR", async (t) => {
10
10
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-loadfile-eisdir-"));
11
11
  const dirPath = path.join(tmp, "tasks");
12
12
  fs.mkdirSync(dirPath);
13
13
 
14
- try {
15
- const result = await loadFile(dirPath);
16
- assert.equal(result, null);
17
- } finally {
18
- fs.rmSync(tmp, { recursive: true, force: true });
19
- }
14
+ t.after(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
15
+
16
+ const result = await loadFile(dirPath);
17
+ assert.equal(result, null);
20
18
  });
@@ -1,3 +1,5 @@
1
+ import { describe, test } from 'node:test';
2
+ import assert from 'node:assert/strict';
1
3
  /**
2
4
  * flag-file-db.test.ts — Verify that REPLAN.md and REPLAN-TRIGGER.md
3
5
  * flag-file detection in deriveStateFromDb() works from DB-only data
@@ -24,10 +26,6 @@ import {
24
26
  insertReplanHistory,
25
27
  _getAdapter,
26
28
  } from '../gsd-db.ts';
27
- import { createTestContext } from './test-helpers.ts';
28
-
29
- const { assertEq, assertTrue, report } = createTestContext();
30
-
31
29
  // ─── Fixture Helpers ───────────────────────────────────────────────────────
32
30
 
33
31
  function createFixtureBase(): string {
@@ -78,11 +76,10 @@ const TASK_SUMMARY_STUB = `---\nblocker_discovered: false\n---\n# T01 Summary\nD
78
76
  // Tests
79
77
  // ═══════════════════════════════════════════════════════════════════════════
80
78
 
81
- async function main(): Promise<void> {
79
+ describe('flag-file-db', async () => {
82
80
 
83
81
  // ─── Test 1: blocker_discovered + no replan_history → replanning-slice ──
84
- console.log('\n=== flag-file-db: blocker + no history → replanning ===');
85
- {
82
+ test('flag-file-db: blocker + no history → replanning', async () => {
86
83
  const base = createFixtureBase();
87
84
  try {
88
85
  // Write disk files needed by deriveStateFromDb (roadmap check, task dir check)
@@ -91,7 +88,7 @@ async function main(): Promise<void> {
91
88
  writeFile(base, 'milestones/M001/slices/S01/tasks/T02-PLAN.md', TASK_PLAN_STUB);
92
89
 
93
90
  openDatabase(':memory:');
94
- assertTrue(isDbAvailable(), 'test1: DB is available');
91
+ assert.ok(isDbAvailable(), 'test1: DB is available');
95
92
 
96
93
  insertMilestone({ id: 'M001', title: 'Flag-File DB Test', status: 'active' });
97
94
  insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', status: 'active', risk: 'low', depends: [] });
@@ -102,20 +99,19 @@ async function main(): Promise<void> {
102
99
  invalidateStateCache();
103
100
  const state = await deriveStateFromDb(base);
104
101
 
105
- assertEq(state.phase, 'replanning-slice', 'test1: phase is replanning-slice');
106
- assertTrue(state.blockers.length > 0, 'test1: has blockers');
107
- assertTrue(state.blockers[0]?.includes('blocker'), 'test1: blocker message mentions blocker');
102
+ assert.deepStrictEqual(state.phase, 'replanning-slice', 'test1: phase is replanning-slice');
103
+ assert.ok(state.blockers.length > 0, 'test1: has blockers');
104
+ assert.ok(state.blockers[0]?.includes('blocker'), 'test1: blocker message mentions blocker');
108
105
 
109
106
  closeDatabase();
110
107
  } finally {
111
108
  closeDatabase();
112
109
  cleanup(base);
113
110
  }
114
- }
111
+ });
115
112
 
116
113
  // ─── Test 2: blocker_discovered + replan_history exists → loop protection → executing ──
117
- console.log('\n=== flag-file-db: blocker + history → loop protection ===');
118
- {
114
+ test('flag-file-db: blocker + history → loop protection', async () => {
119
115
  const base = createFixtureBase();
120
116
  try {
121
117
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
@@ -139,18 +135,17 @@ async function main(): Promise<void> {
139
135
  invalidateStateCache();
140
136
  const state = await deriveStateFromDb(base);
141
137
 
142
- assertEq(state.phase, 'executing', 'test2: phase is executing (loop protection)');
138
+ assert.deepStrictEqual(state.phase, 'executing', 'test2: phase is executing (loop protection)');
143
139
 
144
140
  closeDatabase();
145
141
  } finally {
146
142
  closeDatabase();
147
143
  cleanup(base);
148
144
  }
149
- }
145
+ });
150
146
 
151
147
  // ─── Test 3: replan_triggered_at set + no replan_history → replanning-slice ──
152
- console.log('\n=== flag-file-db: trigger column + no history → replanning ===');
153
- {
148
+ test('flag-file-db: trigger column + no history → replanning', async () => {
154
149
  const base = createFixtureBase();
155
150
  try {
156
151
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
@@ -173,20 +168,19 @@ async function main(): Promise<void> {
173
168
  invalidateStateCache();
174
169
  const state = await deriveStateFromDb(base);
175
170
 
176
- assertEq(state.phase, 'replanning-slice', 'test3: phase is replanning-slice');
177
- assertTrue(state.blockers.length > 0, 'test3: has blockers');
178
- assertTrue(state.blockers[0]?.includes('Triage replan trigger'), 'test3: blocker message mentions triage trigger');
171
+ assert.deepStrictEqual(state.phase, 'replanning-slice', 'test3: phase is replanning-slice');
172
+ assert.ok(state.blockers.length > 0, 'test3: has blockers');
173
+ assert.ok(state.blockers[0]?.includes('Triage replan trigger'), 'test3: blocker message mentions triage trigger');
179
174
 
180
175
  closeDatabase();
181
176
  } finally {
182
177
  closeDatabase();
183
178
  cleanup(base);
184
179
  }
185
- }
180
+ });
186
181
 
187
182
  // ─── Test 4: replan_triggered_at set + replan_history exists → loop protection ──
188
- console.log('\n=== flag-file-db: trigger column + history → loop protection ===');
189
- {
183
+ test('flag-file-db: trigger column + history → loop protection', async () => {
190
184
  const base = createFixtureBase();
191
185
  try {
192
186
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
@@ -216,18 +210,17 @@ async function main(): Promise<void> {
216
210
  invalidateStateCache();
217
211
  const state = await deriveStateFromDb(base);
218
212
 
219
- assertEq(state.phase, 'executing', 'test4: phase is executing (loop protection)');
213
+ assert.deepStrictEqual(state.phase, 'executing', 'test4: phase is executing (loop protection)');
220
214
 
221
215
  closeDatabase();
222
216
  } finally {
223
217
  closeDatabase();
224
218
  cleanup(base);
225
219
  }
226
- }
220
+ });
227
221
 
228
222
  // ─── Test 5: no blocker, no trigger → phase is executing ──────────────
229
- console.log('\n=== flag-file-db: no blocker, no trigger → executing ===');
230
- {
223
+ test('flag-file-db: no blocker, no trigger → executing', async () => {
231
224
  const base = createFixtureBase();
232
225
  try {
233
226
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
@@ -245,20 +238,19 @@ async function main(): Promise<void> {
245
238
  invalidateStateCache();
246
239
  const state = await deriveStateFromDb(base);
247
240
 
248
- assertEq(state.phase, 'executing', 'test5: phase is executing');
249
- assertEq(state.activeTask?.id, 'T02', 'test5: activeTask is T02');
250
- assertEq(state.blockers.length, 0, 'test5: no blockers');
241
+ assert.deepStrictEqual(state.phase, 'executing', 'test5: phase is executing');
242
+ assert.deepStrictEqual(state.activeTask?.id, 'T02', 'test5: activeTask is T02');
243
+ assert.deepStrictEqual(state.blockers.length, 0, 'test5: no blockers');
251
244
 
252
245
  closeDatabase();
253
246
  } finally {
254
247
  closeDatabase();
255
248
  cleanup(base);
256
249
  }
257
- }
250
+ });
258
251
 
259
252
  // ─── Diagnostic test: DB column inspection ──────────────────────────
260
- console.log('\n=== flag-file-db: replan_triggered_at column is queryable ===');
261
- {
253
+ test('flag-file-db: replan_triggered_at column is queryable', () => {
262
254
  openDatabase(':memory:');
263
255
 
264
256
  insertMilestone({ id: 'M001', title: 'Diagnostic', status: 'active' });
@@ -269,7 +261,7 @@ async function main(): Promise<void> {
269
261
  const before = adapter!.prepare(
270
262
  "SELECT id, replan_triggered_at FROM slices WHERE milestone_id = :mid",
271
263
  ).get({ ":mid": "M001" }) as Record<string, unknown>;
272
- assertEq(before["replan_triggered_at"], null, 'diagnostic: replan_triggered_at initially null');
264
+ assert.deepStrictEqual(before["replan_triggered_at"], null, 'diagnostic: replan_triggered_at initially null');
273
265
 
274
266
  // After setting
275
267
  adapter!.prepare(
@@ -279,12 +271,8 @@ async function main(): Promise<void> {
279
271
  const after = adapter!.prepare(
280
272
  "SELECT id, replan_triggered_at FROM slices WHERE milestone_id = :mid",
281
273
  ).get({ ":mid": "M001" }) as Record<string, unknown>;
282
- assertEq(after["replan_triggered_at"], "2025-01-01T00:00:00Z", 'diagnostic: replan_triggered_at is set');
274
+ assert.deepStrictEqual(after["replan_triggered_at"], "2025-01-01T00:00:00Z", 'diagnostic: replan_triggered_at is set');
283
275
 
284
276
  closeDatabase();
285
- }
286
-
287
- report();
288
- }
289
-
290
- main();
277
+ });
278
+ });
@@ -1,4 +1,5 @@
1
- import { createTestContext } from './test-helpers.ts';
1
+ import { describe, test } from 'node:test';
2
+ import assert from 'node:assert/strict';
2
3
  import * as path from 'node:path';
3
4
  import * as os from 'node:os';
4
5
  import * as fs from 'node:fs';
@@ -13,8 +14,6 @@ import {
13
14
  saveDecisionToDb,
14
15
  } from '../db-writer.ts';
15
16
 
16
- const { assertEq, assertTrue, report } = createTestContext();
17
-
18
17
  // ═══════════════════════════════════════════════════════════════════════════
19
18
  // Helpers
20
19
  // ═══════════════════════════════════════════════════════════════════════════
@@ -35,206 +34,199 @@ function cleanupDir(dir: string): void {
35
34
  // Bug reproduction: freeform DECISIONS.md content destroyed (#2301)
36
35
  // ═══════════════════════════════════════════════════════════════════════════
37
36
 
38
- console.log('\n── parseDecisionsTable silently drops freeform content ──');
39
-
40
- {
41
- const freeform = `# Project Decisions
42
-
43
- ## Architecture
44
- We decided to use a microservices architecture because monoliths don't scale.
45
-
46
- ## Database
47
- PostgreSQL was chosen for its reliability and JSONB support.
48
-
49
- ## Deployment
50
- - Kubernetes for orchestration
51
- - Helm charts for packaging
52
- `;
53
-
54
- const parsed = parseDecisionsTable(freeform);
55
- assertEq(parsed.length, 0, 'freeform content yields zero parsed decisions (expected — it is not a table)');
56
- }
57
-
58
- console.log('\n── saveDecisionToDb destroys freeform DECISIONS.md content ──');
59
-
60
- {
61
- const tmpDir = makeTmpDir();
62
- const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
63
- const mdPath = path.join(tmpDir, '.gsd', 'DECISIONS.md');
64
- openDatabase(dbPath);
65
-
66
- const freeformContent = `# Project Decisions
67
-
68
- ## Architecture
69
- We decided to use a microservices architecture because monoliths don't scale.
70
-
71
- ## Database
72
- PostgreSQL was chosen for its reliability and JSONB support.
73
-
74
- ## Deployment
75
- - Kubernetes for orchestration
76
- - Helm charts for packaging
77
- `;
78
-
79
- // Pre-populate DECISIONS.md with freeform content
80
- fs.writeFileSync(mdPath, freeformContent, 'utf-8');
81
-
82
- try {
83
- // Save a new decision this should NOT destroy the freeform content
84
- const result = await saveDecisionToDb({
85
- scope: 'testing',
86
- decision: 'Use Jest for unit tests',
87
- choice: 'Jest',
88
- rationale: 'Well-known, good DX',
89
- when_context: 'M001',
90
- }, tmpDir);
91
-
92
- assertEq(result.id, 'D001', 'decision ID assigned correctly');
93
-
94
- // Read back the file
95
- const afterContent = fs.readFileSync(mdPath, 'utf-8');
96
-
97
- // The freeform content MUST still be present
98
- assertTrue(
99
- afterContent.includes('microservices architecture'),
100
- 'freeform architecture section preserved after saveDecisionToDb',
101
- );
102
- assertTrue(
103
- afterContent.includes('PostgreSQL was chosen'),
104
- 'freeform database section preserved after saveDecisionToDb',
105
- );
106
- assertTrue(
107
- afterContent.includes('Kubernetes for orchestration'),
108
- 'freeform deployment section preserved after saveDecisionToDb',
109
- );
110
-
111
- // The new decision MUST also be present
112
- assertTrue(
113
- afterContent.includes('D001'),
114
- 'new decision D001 present in file',
115
- );
116
- assertTrue(
117
- afterContent.includes('Use Jest for unit tests'),
118
- 'new decision text present in file',
119
- );
120
-
121
- // Save a second decision freeform content must still survive
122
- const result2 = await saveDecisionToDb({
123
- scope: 'ci',
124
- decision: 'Use GitHub Actions for CI',
125
- choice: 'GitHub Actions',
126
- rationale: 'Native integration',
127
- when_context: 'M001',
128
- }, tmpDir);
129
-
130
- assertEq(result2.id, 'D002', 'second decision ID assigned correctly');
131
-
132
- const afterContent2 = fs.readFileSync(mdPath, 'utf-8');
133
-
134
- assertTrue(
135
- afterContent2.includes('microservices architecture'),
136
- 'freeform content still preserved after second save',
137
- );
138
- assertTrue(
139
- afterContent2.includes('D001'),
140
- 'first decision still present after second save',
141
- );
142
- assertTrue(
143
- afterContent2.includes('D002'),
144
- 'second decision present after second save',
145
- );
146
- assertTrue(
147
- afterContent2.includes('Use GitHub Actions for CI'),
148
- 'second decision text present in file',
149
- );
150
- } finally {
151
- closeDatabase();
152
- cleanupDir(tmpDir);
153
- }
154
- }
155
-
156
- console.log('\n── saveDecisionToDb with table-format DECISIONS.md still regenerates normally ──');
157
-
158
- {
159
- const tmpDir = makeTmpDir();
160
- const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
161
- const mdPath = path.join(tmpDir, '.gsd', 'DECISIONS.md');
162
- openDatabase(dbPath);
163
-
164
- // Pre-populate with canonical table format
165
- const tableContent = `# Decisions Register
166
-
167
- <!-- Append-only. Never edit or remove existing rows.
168
- To reverse a decision, add a new row that supersedes it.
169
- Read this file at the start of any planning or research phase. -->
170
-
171
- | # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By |
172
- |---|------|-------|----------|--------|-----------|------------|---------|
173
- | D001 | M001 | arch | Use REST API | REST | Simpler | Yes | human |
174
- `;
175
-
176
- fs.writeFileSync(mdPath, tableContent, 'utf-8');
177
-
178
- try {
179
- const result = await saveDecisionToDb({
180
- scope: 'testing',
181
- decision: 'Use Vitest',
182
- choice: 'Vitest',
183
- rationale: 'Fast',
184
- when_context: 'M001',
185
- }, tmpDir);
186
-
187
- // The pre-existing table decision was NOT in DB, so it won't appear after regen.
188
- // But the new decision should be there.
189
- assertEq(result.id, 'D001', 'gets D001 since DB was empty');
190
-
191
- const afterContent = fs.readFileSync(mdPath, 'utf-8');
192
- // Table-format file gets fully regenerated — this is the normal path
193
- assertTrue(
194
- afterContent.includes('# Decisions Register'),
195
- 'table-format file still has header after save',
196
- );
197
- assertTrue(
198
- afterContent.includes('Use Vitest'),
199
- 'new decision present in regenerated table',
200
- );
201
- } finally {
202
- closeDatabase();
203
- cleanupDir(tmpDir);
204
- }
205
- }
206
-
207
- console.log('\n── saveDecisionToDb with no existing DECISIONS.md creates table ──');
208
-
209
- {
210
- const tmpDir = makeTmpDir();
211
- const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
212
- const mdPath = path.join(tmpDir, '.gsd', 'DECISIONS.md');
213
- openDatabase(dbPath);
214
-
215
- // No DECISIONS.md exists at all
216
- assertTrue(!fs.existsSync(mdPath), 'DECISIONS.md does not exist initially');
217
-
218
- try {
219
- const result = await saveDecisionToDb({
220
- scope: 'arch',
221
- decision: 'Brand new decision',
222
- choice: 'Option A',
223
- rationale: 'Best fit',
224
- }, tmpDir);
225
-
226
- assertEq(result.id, 'D001', 'first decision gets D001');
227
- assertTrue(fs.existsSync(mdPath), 'DECISIONS.md created');
228
-
229
- const content = fs.readFileSync(mdPath, 'utf-8');
230
- assertTrue(content.includes('# Decisions Register'), 'new file has header');
231
- assertTrue(content.includes('Brand new decision'), 'new file has decision');
232
- } finally {
233
- closeDatabase();
234
- cleanupDir(tmpDir);
235
- }
236
- }
237
-
238
- // ═══════════════════════════════════════════════════════════════════════════
239
-
240
- report();
37
+ describe('freeform-decisions', () => {
38
+ test('parseDecisionsTable silently drops freeform content', () => {
39
+ const freeform = `# Project Decisions
40
+
41
+ ## Architecture
42
+ We decided to use a microservices architecture because monoliths don't scale.
43
+
44
+ ## Database
45
+ PostgreSQL was chosen for its reliability and JSONB support.
46
+
47
+ ## Deployment
48
+ - Kubernetes for orchestration
49
+ - Helm charts for packaging
50
+ `;
51
+
52
+ const parsed = parseDecisionsTable(freeform);
53
+ assert.deepStrictEqual(parsed.length, 0, 'freeform content yields zero parsed decisions (expected — it is not a table)');
54
+ });
55
+
56
+ test('saveDecisionToDb destroys freeform DECISIONS.md content', async () => {
57
+ const tmpDir = makeTmpDir();
58
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
59
+ const mdPath = path.join(tmpDir, '.gsd', 'DECISIONS.md');
60
+ openDatabase(dbPath);
61
+
62
+ const freeformContent = `# Project Decisions
63
+
64
+ ## Architecture
65
+ We decided to use a microservices architecture because monoliths don't scale.
66
+
67
+ ## Database
68
+ PostgreSQL was chosen for its reliability and JSONB support.
69
+
70
+ ## Deployment
71
+ - Kubernetes for orchestration
72
+ - Helm charts for packaging
73
+ `;
74
+
75
+ // Pre-populate DECISIONS.md with freeform content
76
+ fs.writeFileSync(mdPath, freeformContent, 'utf-8');
77
+
78
+ try {
79
+ // Save a new decision — this should NOT destroy the freeform content
80
+ const result = await saveDecisionToDb({
81
+ scope: 'testing',
82
+ decision: 'Use Jest for unit tests',
83
+ choice: 'Jest',
84
+ rationale: 'Well-known, good DX',
85
+ when_context: 'M001',
86
+ }, tmpDir);
87
+
88
+ assert.deepStrictEqual(result.id, 'D001', 'decision ID assigned correctly');
89
+
90
+ // Read back the file
91
+ const afterContent = fs.readFileSync(mdPath, 'utf-8');
92
+
93
+ // The freeform content MUST still be present
94
+ assert.ok(
95
+ afterContent.includes('microservices architecture'),
96
+ 'freeform architecture section preserved after saveDecisionToDb',
97
+ );
98
+ assert.ok(
99
+ afterContent.includes('PostgreSQL was chosen'),
100
+ 'freeform database section preserved after saveDecisionToDb',
101
+ );
102
+ assert.ok(
103
+ afterContent.includes('Kubernetes for orchestration'),
104
+ 'freeform deployment section preserved after saveDecisionToDb',
105
+ );
106
+
107
+ // The new decision MUST also be present
108
+ assert.ok(
109
+ afterContent.includes('D001'),
110
+ 'new decision D001 present in file',
111
+ );
112
+ assert.ok(
113
+ afterContent.includes('Use Jest for unit tests'),
114
+ 'new decision text present in file',
115
+ );
116
+
117
+ // Save a second decision freeform content must still survive
118
+ const result2 = await saveDecisionToDb({
119
+ scope: 'ci',
120
+ decision: 'Use GitHub Actions for CI',
121
+ choice: 'GitHub Actions',
122
+ rationale: 'Native integration',
123
+ when_context: 'M001',
124
+ }, tmpDir);
125
+
126
+ assert.deepStrictEqual(result2.id, 'D002', 'second decision ID assigned correctly');
127
+
128
+ const afterContent2 = fs.readFileSync(mdPath, 'utf-8');
129
+
130
+ assert.ok(
131
+ afterContent2.includes('microservices architecture'),
132
+ 'freeform content still preserved after second save',
133
+ );
134
+ assert.ok(
135
+ afterContent2.includes('D001'),
136
+ 'first decision still present after second save',
137
+ );
138
+ assert.ok(
139
+ afterContent2.includes('D002'),
140
+ 'second decision present after second save',
141
+ );
142
+ assert.ok(
143
+ afterContent2.includes('Use GitHub Actions for CI'),
144
+ 'second decision text present in file',
145
+ );
146
+ } finally {
147
+ closeDatabase();
148
+ cleanupDir(tmpDir);
149
+ }
150
+ });
151
+
152
+ test('saveDecisionToDb with table-format DECISIONS.md still regenerates normally', async () => {
153
+ const tmpDir = makeTmpDir();
154
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
155
+ const mdPath = path.join(tmpDir, '.gsd', 'DECISIONS.md');
156
+ openDatabase(dbPath);
157
+
158
+ // Pre-populate with canonical table format
159
+ const tableContent = `# Decisions Register
160
+
161
+ <!-- Append-only. Never edit or remove existing rows.
162
+ To reverse a decision, add a new row that supersedes it.
163
+ Read this file at the start of any planning or research phase. -->
164
+
165
+ | # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By |
166
+ |---|------|-------|----------|--------|-----------|------------|---------|
167
+ | D001 | M001 | arch | Use REST API | REST | Simpler | Yes | human |
168
+ `;
169
+
170
+ fs.writeFileSync(mdPath, tableContent, 'utf-8');
171
+
172
+ try {
173
+ const result = await saveDecisionToDb({
174
+ scope: 'testing',
175
+ decision: 'Use Vitest',
176
+ choice: 'Vitest',
177
+ rationale: 'Fast',
178
+ when_context: 'M001',
179
+ }, tmpDir);
180
+
181
+ // The pre-existing table decision was NOT in DB, so it won't appear after regen.
182
+ // But the new decision should be there.
183
+ assert.deepStrictEqual(result.id, 'D001', 'gets D001 since DB was empty');
184
+
185
+ const afterContent = fs.readFileSync(mdPath, 'utf-8');
186
+ // Table-format file gets fully regenerated this is the normal path
187
+ assert.ok(
188
+ afterContent.includes('# Decisions Register'),
189
+ 'table-format file still has header after save',
190
+ );
191
+ assert.ok(
192
+ afterContent.includes('Use Vitest'),
193
+ 'new decision present in regenerated table',
194
+ );
195
+ } finally {
196
+ closeDatabase();
197
+ cleanupDir(tmpDir);
198
+ }
199
+ });
200
+
201
+ test('saveDecisionToDb with no existing DECISIONS.md creates table', async () => {
202
+ const tmpDir = makeTmpDir();
203
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
204
+ const mdPath = path.join(tmpDir, '.gsd', 'DECISIONS.md');
205
+ openDatabase(dbPath);
206
+
207
+ // No DECISIONS.md exists at all
208
+ assert.ok(!fs.existsSync(mdPath), 'DECISIONS.md does not exist initially');
209
+
210
+ try {
211
+ const result = await saveDecisionToDb({
212
+ scope: 'arch',
213
+ decision: 'Brand new decision',
214
+ choice: 'Option A',
215
+ rationale: 'Best fit',
216
+ }, tmpDir);
217
+
218
+ assert.deepStrictEqual(result.id, 'D001', 'first decision gets D001');
219
+ assert.ok(fs.existsSync(mdPath), 'DECISIONS.md created');
220
+
221
+ const content = fs.readFileSync(mdPath, 'utf-8');
222
+ assert.ok(content.includes('# Decisions Register'), 'new file has header');
223
+ assert.ok(content.includes('Brand new decision'), 'new file has decision');
224
+ } finally {
225
+ closeDatabase();
226
+ cleanupDir(tmpDir);
227
+ }
228
+ });
229
+
230
+ // ═══════════════════════════════════════════════════════════════════════════
231
+
232
+ });