@ktpartners/dgs-platform 2.6.2

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 (256) hide show
  1. package/LICENSE +38 -0
  2. package/README.md +851 -0
  3. package/agents/dgs-codebase-cross-analyzer.md +183 -0
  4. package/agents/dgs-codebase-mapper.md +782 -0
  5. package/agents/dgs-codebase-synthesizer.md +156 -0
  6. package/agents/dgs-debugger.md +1256 -0
  7. package/agents/dgs-executor.md +550 -0
  8. package/agents/dgs-integration-checker.md +481 -0
  9. package/agents/dgs-nyquist-auditor.md +178 -0
  10. package/agents/dgs-phase-researcher.md +563 -0
  11. package/agents/dgs-phase-verifier.md +450 -0
  12. package/agents/dgs-plan-checker.md +708 -0
  13. package/agents/dgs-planner.md +1324 -0
  14. package/agents/dgs-project-researcher.md +631 -0
  15. package/agents/dgs-research-synthesizer.md +249 -0
  16. package/agents/dgs-roadmapper.md +652 -0
  17. package/agents/dgs-verifier.md +607 -0
  18. package/bin/install.js +2073 -0
  19. package/commands/dgs/add-doc.md +45 -0
  20. package/commands/dgs/add-idea.md +38 -0
  21. package/commands/dgs/add-phase.md +43 -0
  22. package/commands/dgs/add-repo.md +54 -0
  23. package/commands/dgs/add-tests.md +41 -0
  24. package/commands/dgs/add-todo.md +47 -0
  25. package/commands/dgs/approve-spec.md +38 -0
  26. package/commands/dgs/audit-milestone.md +36 -0
  27. package/commands/dgs/audit-phase.md +37 -0
  28. package/commands/dgs/cancel-job.md +23 -0
  29. package/commands/dgs/capture-principle.md +143 -0
  30. package/commands/dgs/check-todos.md +45 -0
  31. package/commands/dgs/cleanup.md +18 -0
  32. package/commands/dgs/complete-milestone.md +136 -0
  33. package/commands/dgs/complete-project.md +70 -0
  34. package/commands/dgs/consolidate-ideas.md +50 -0
  35. package/commands/dgs/create-milestone-job.md +37 -0
  36. package/commands/dgs/debug.md +164 -0
  37. package/commands/dgs/develop-idea.md +53 -0
  38. package/commands/dgs/discuss-idea.md +41 -0
  39. package/commands/dgs/discuss-phase.md +83 -0
  40. package/commands/dgs/execute-phase.md +41 -0
  41. package/commands/dgs/fast.md +38 -0
  42. package/commands/dgs/find-related-ideas.md +43 -0
  43. package/commands/dgs/health.md +28 -0
  44. package/commands/dgs/help.md +22 -0
  45. package/commands/dgs/import-spec.md +36 -0
  46. package/commands/dgs/init-product.md +28 -0
  47. package/commands/dgs/insert-phase.md +32 -0
  48. package/commands/dgs/join-discord.md +18 -0
  49. package/commands/dgs/list-docs.md +40 -0
  50. package/commands/dgs/list-ideas.md +42 -0
  51. package/commands/dgs/list-jobs.md +22 -0
  52. package/commands/dgs/list-phase-assumptions.md +46 -0
  53. package/commands/dgs/list-projects.md +57 -0
  54. package/commands/dgs/list-specs.md +40 -0
  55. package/commands/dgs/map-codebase.md +92 -0
  56. package/commands/dgs/new-milestone.md +44 -0
  57. package/commands/dgs/new-project.md +42 -0
  58. package/commands/dgs/node-repair.md +26 -0
  59. package/commands/dgs/overlap-check.md +20 -0
  60. package/commands/dgs/pause-work.md +38 -0
  61. package/commands/dgs/plan-milestone-gaps.md +34 -0
  62. package/commands/dgs/plan-phase.md +44 -0
  63. package/commands/dgs/progress.md +24 -0
  64. package/commands/dgs/quick.md +41 -0
  65. package/commands/dgs/reactivate-project.md +70 -0
  66. package/commands/dgs/reapply-patches.md +110 -0
  67. package/commands/dgs/refine-spec.md +38 -0
  68. package/commands/dgs/reject-idea.md +43 -0
  69. package/commands/dgs/remove-doc.md +44 -0
  70. package/commands/dgs/remove-phase.md +31 -0
  71. package/commands/dgs/remove-repo.md +69 -0
  72. package/commands/dgs/research-idea.md +43 -0
  73. package/commands/dgs/research-phase.md +189 -0
  74. package/commands/dgs/restore-idea.md +45 -0
  75. package/commands/dgs/resume-work.md +40 -0
  76. package/commands/dgs/rollback-job.md +24 -0
  77. package/commands/dgs/run-job.md +35 -0
  78. package/commands/dgs/search.md +40 -0
  79. package/commands/dgs/set-profile.md +34 -0
  80. package/commands/dgs/settings.md +38 -0
  81. package/commands/dgs/switch-project.md +58 -0
  82. package/commands/dgs/undo-consolidation.md +42 -0
  83. package/commands/dgs/update-idea.md +44 -0
  84. package/commands/dgs/update.md +37 -0
  85. package/commands/dgs/validate-phase.md +35 -0
  86. package/commands/dgs/verify-work.md +39 -0
  87. package/commands/dgs/write-spec.md +49 -0
  88. package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-01-SUMMARY.md +84 -0
  89. package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-02-SUMMARY.md +86 -0
  90. package/deliver-great-systems/.planning/phases/10-v1-to-v2-migration-flow/10-01-SUMMARY.md +85 -0
  91. package/deliver-great-systems/bin/dgs-tools.cjs +1444 -0
  92. package/deliver-great-systems/bin/lib/auto-test.cjs +1365 -0
  93. package/deliver-great-systems/bin/lib/commands.cjs +570 -0
  94. package/deliver-great-systems/bin/lib/config.cjs +417 -0
  95. package/deliver-great-systems/bin/lib/conflict-agent.cjs +1063 -0
  96. package/deliver-great-systems/bin/lib/conflict-agent.test.cjs +554 -0
  97. package/deliver-great-systems/bin/lib/context.cjs +929 -0
  98. package/deliver-great-systems/bin/lib/context.test.cjs +693 -0
  99. package/deliver-great-systems/bin/lib/core.cjs +744 -0
  100. package/deliver-great-systems/bin/lib/core.test.cjs +822 -0
  101. package/deliver-great-systems/bin/lib/docs.cjs +919 -0
  102. package/deliver-great-systems/bin/lib/docs.test.cjs +211 -0
  103. package/deliver-great-systems/bin/lib/execution.cjs +705 -0
  104. package/deliver-great-systems/bin/lib/execution.test.cjs +1472 -0
  105. package/deliver-great-systems/bin/lib/frontmatter.cjs +324 -0
  106. package/deliver-great-systems/bin/lib/ideas.cjs +1406 -0
  107. package/deliver-great-systems/bin/lib/ideas.test.cjs +1417 -0
  108. package/deliver-great-systems/bin/lib/identity.cjs +125 -0
  109. package/deliver-great-systems/bin/lib/init.cjs +1114 -0
  110. package/deliver-great-systems/bin/lib/init.test.cjs +1271 -0
  111. package/deliver-great-systems/bin/lib/jobs.cjs +2015 -0
  112. package/deliver-great-systems/bin/lib/jobs.test.cjs +2619 -0
  113. package/deliver-great-systems/bin/lib/merge-conflicts.cjs +654 -0
  114. package/deliver-great-systems/bin/lib/merge-conflicts.test.cjs +370 -0
  115. package/deliver-great-systems/bin/lib/migration.cjs +352 -0
  116. package/deliver-great-systems/bin/lib/migration.test.cjs +582 -0
  117. package/deliver-great-systems/bin/lib/milestone.cjs +243 -0
  118. package/deliver-great-systems/bin/lib/overlap.cjs +437 -0
  119. package/deliver-great-systems/bin/lib/overlap.test.cjs +747 -0
  120. package/deliver-great-systems/bin/lib/path-audit.test.cjs +384 -0
  121. package/deliver-great-systems/bin/lib/paths.cjs +144 -0
  122. package/deliver-great-systems/bin/lib/paths.test.cjs +486 -0
  123. package/deliver-great-systems/bin/lib/phase.cjs +910 -0
  124. package/deliver-great-systems/bin/lib/projects.cjs +691 -0
  125. package/deliver-great-systems/bin/lib/projects.test.cjs +871 -0
  126. package/deliver-great-systems/bin/lib/repos.cjs +1432 -0
  127. package/deliver-great-systems/bin/lib/repos.test.cjs +1882 -0
  128. package/deliver-great-systems/bin/lib/roadmap.cjs +305 -0
  129. package/deliver-great-systems/bin/lib/search.cjs +570 -0
  130. package/deliver-great-systems/bin/lib/specs.cjs +1303 -0
  131. package/deliver-great-systems/bin/lib/state.cjs +893 -0
  132. package/deliver-great-systems/bin/lib/template.cjs +228 -0
  133. package/deliver-great-systems/bin/lib/test-helpers.cjs +291 -0
  134. package/deliver-great-systems/bin/lib/verify.cjs +796 -0
  135. package/deliver-great-systems/references/checkpoints.md +776 -0
  136. package/deliver-great-systems/references/conflict-resolution.md +66 -0
  137. package/deliver-great-systems/references/context-tiers.md +166 -0
  138. package/deliver-great-systems/references/continuation-format.md +249 -0
  139. package/deliver-great-systems/references/decimal-phase-calculation.md +67 -0
  140. package/deliver-great-systems/references/git-integration.md +250 -0
  141. package/deliver-great-systems/references/git-planning-commit.md +40 -0
  142. package/deliver-great-systems/references/model-profile-resolution.md +36 -0
  143. package/deliver-great-systems/references/model-profiles.md +95 -0
  144. package/deliver-great-systems/references/phase-argument-parsing.md +61 -0
  145. package/deliver-great-systems/references/planning-config.md +224 -0
  146. package/deliver-great-systems/references/questioning.md +162 -0
  147. package/deliver-great-systems/references/spec-review-loop.md +177 -0
  148. package/deliver-great-systems/references/tdd.md +265 -0
  149. package/deliver-great-systems/references/ui-brand.md +160 -0
  150. package/deliver-great-systems/references/verification-patterns.md +612 -0
  151. package/deliver-great-systems/templates/DEBUG.md +166 -0
  152. package/deliver-great-systems/templates/UAT.md +251 -0
  153. package/deliver-great-systems/templates/VALIDATION.md +95 -0
  154. package/deliver-great-systems/templates/claude-md.md +74 -0
  155. package/deliver-great-systems/templates/codebase/architecture.md +257 -0
  156. package/deliver-great-systems/templates/codebase/concerns.md +312 -0
  157. package/deliver-great-systems/templates/codebase/conventions.md +309 -0
  158. package/deliver-great-systems/templates/codebase/integrations.md +282 -0
  159. package/deliver-great-systems/templates/codebase/stack.md +188 -0
  160. package/deliver-great-systems/templates/codebase/structure.md +287 -0
  161. package/deliver-great-systems/templates/codebase/testing.md +482 -0
  162. package/deliver-great-systems/templates/config.json +38 -0
  163. package/deliver-great-systems/templates/context.md +354 -0
  164. package/deliver-great-systems/templates/continue-here.md +80 -0
  165. package/deliver-great-systems/templates/debug-subagent-prompt.md +93 -0
  166. package/deliver-great-systems/templates/discovery.md +148 -0
  167. package/deliver-great-systems/templates/milestone-archive.md +125 -0
  168. package/deliver-great-systems/templates/milestone.md +117 -0
  169. package/deliver-great-systems/templates/phase-prompt.md +615 -0
  170. package/deliver-great-systems/templates/planner-subagent-prompt.md +119 -0
  171. package/deliver-great-systems/templates/project.md +186 -0
  172. package/deliver-great-systems/templates/requirements.md +233 -0
  173. package/deliver-great-systems/templates/research-project/ARCHITECTURE.md +206 -0
  174. package/deliver-great-systems/templates/research-project/FEATURES.md +149 -0
  175. package/deliver-great-systems/templates/research-project/PITFALLS.md +202 -0
  176. package/deliver-great-systems/templates/research-project/STACK.md +122 -0
  177. package/deliver-great-systems/templates/research-project/SUMMARY.md +172 -0
  178. package/deliver-great-systems/templates/research.md +554 -0
  179. package/deliver-great-systems/templates/retrospective.md +54 -0
  180. package/deliver-great-systems/templates/roadmap.md +204 -0
  181. package/deliver-great-systems/templates/state.md +178 -0
  182. package/deliver-great-systems/templates/summary-complex.md +59 -0
  183. package/deliver-great-systems/templates/summary-minimal.md +41 -0
  184. package/deliver-great-systems/templates/summary-standard.md +48 -0
  185. package/deliver-great-systems/templates/summary.md +253 -0
  186. package/deliver-great-systems/templates/user-setup.md +313 -0
  187. package/deliver-great-systems/templates/verification-report.md +324 -0
  188. package/deliver-great-systems/workflows/add-doc.md +151 -0
  189. package/deliver-great-systems/workflows/add-idea.md +96 -0
  190. package/deliver-great-systems/workflows/add-phase.md +120 -0
  191. package/deliver-great-systems/workflows/add-tests.md +359 -0
  192. package/deliver-great-systems/workflows/add-todo.md +162 -0
  193. package/deliver-great-systems/workflows/approve-spec.md +194 -0
  194. package/deliver-great-systems/workflows/audit-milestone.md +364 -0
  195. package/deliver-great-systems/workflows/audit-phase.md +462 -0
  196. package/deliver-great-systems/workflows/cancel-job.md +108 -0
  197. package/deliver-great-systems/workflows/check-todos.md +181 -0
  198. package/deliver-great-systems/workflows/cleanup.md +247 -0
  199. package/deliver-great-systems/workflows/codereview.md +526 -0
  200. package/deliver-great-systems/workflows/complete-milestone.md +1298 -0
  201. package/deliver-great-systems/workflows/consolidate-ideas.md +365 -0
  202. package/deliver-great-systems/workflows/create-milestone-job.md +177 -0
  203. package/deliver-great-systems/workflows/develop-idea.md +544 -0
  204. package/deliver-great-systems/workflows/diagnose-issues.md +231 -0
  205. package/deliver-great-systems/workflows/discovery-phase.md +301 -0
  206. package/deliver-great-systems/workflows/discuss-idea.md +263 -0
  207. package/deliver-great-systems/workflows/discuss-phase.md +733 -0
  208. package/deliver-great-systems/workflows/execute-phase.md +571 -0
  209. package/deliver-great-systems/workflows/execute-plan.md +592 -0
  210. package/deliver-great-systems/workflows/find-related-ideas.md +271 -0
  211. package/deliver-great-systems/workflows/health.md +173 -0
  212. package/deliver-great-systems/workflows/help.md +997 -0
  213. package/deliver-great-systems/workflows/import-spec.md +381 -0
  214. package/deliver-great-systems/workflows/init-product.md +767 -0
  215. package/deliver-great-systems/workflows/insert-phase.md +138 -0
  216. package/deliver-great-systems/workflows/list-docs.md +119 -0
  217. package/deliver-great-systems/workflows/list-ideas.md +154 -0
  218. package/deliver-great-systems/workflows/list-jobs.md +89 -0
  219. package/deliver-great-systems/workflows/list-phase-assumptions.md +192 -0
  220. package/deliver-great-systems/workflows/list-specs.md +101 -0
  221. package/deliver-great-systems/workflows/map-codebase.md +621 -0
  222. package/deliver-great-systems/workflows/new-milestone.md +591 -0
  223. package/deliver-great-systems/workflows/new-project.md +1113 -0
  224. package/deliver-great-systems/workflows/node-repair.md +94 -0
  225. package/deliver-great-systems/workflows/overlap-check.md +86 -0
  226. package/deliver-great-systems/workflows/pause-work.md +134 -0
  227. package/deliver-great-systems/workflows/plan-milestone-gaps.md +306 -0
  228. package/deliver-great-systems/workflows/plan-phase.md +698 -0
  229. package/deliver-great-systems/workflows/progress.md +386 -0
  230. package/deliver-great-systems/workflows/quick.md +845 -0
  231. package/deliver-great-systems/workflows/refine-spec.md +275 -0
  232. package/deliver-great-systems/workflows/reject-idea.md +109 -0
  233. package/deliver-great-systems/workflows/remove-doc.md +117 -0
  234. package/deliver-great-systems/workflows/remove-phase.md +163 -0
  235. package/deliver-great-systems/workflows/research-idea.md +325 -0
  236. package/deliver-great-systems/workflows/research-phase.md +81 -0
  237. package/deliver-great-systems/workflows/restore-idea.md +101 -0
  238. package/deliver-great-systems/workflows/resume-project.md +311 -0
  239. package/deliver-great-systems/workflows/rollback-job.md +130 -0
  240. package/deliver-great-systems/workflows/run-job.md +498 -0
  241. package/deliver-great-systems/workflows/search.md +130 -0
  242. package/deliver-great-systems/workflows/set-profile.md +83 -0
  243. package/deliver-great-systems/workflows/settings.md +470 -0
  244. package/deliver-great-systems/workflows/transition.md +563 -0
  245. package/deliver-great-systems/workflows/undo-consolidation.md +155 -0
  246. package/deliver-great-systems/workflows/update-idea.md +157 -0
  247. package/deliver-great-systems/workflows/update.md +242 -0
  248. package/deliver-great-systems/workflows/validate-phase.md +177 -0
  249. package/deliver-great-systems/workflows/verify-phase.md +253 -0
  250. package/deliver-great-systems/workflows/verify-work.md +671 -0
  251. package/deliver-great-systems/workflows/write-spec.md +450 -0
  252. package/hooks/dist/dgs-check-update.js +62 -0
  253. package/hooks/dist/dgs-context-monitor.js +141 -0
  254. package/hooks/dist/dgs-statusline.js +115 -0
  255. package/package.json +60 -0
  256. package/scripts/build-hooks.js +43 -0
@@ -0,0 +1,2619 @@
1
+ /**
2
+ * Tests for jobs.cjs — Job file parse, update-step, move, find, header update,
3
+ * step insertion, and gap-fix operations
4
+ *
5
+ * Covers parseJobFile, updateJobStep, moveJobFile, findJobFile, updateJobHeader,
6
+ * insertJobSteps, buildGapFixSteps, insertGapFixSection, and cmd* CLI wrappers.
7
+ * Uses Node.js built-in test runner (node:test) and assert (node:assert).
8
+ */
9
+
10
+ const { describe, it, beforeEach, afterEach } = require('node:test');
11
+ const assert = require('node:assert/strict');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { createFixture, createTempProject } = require('./test-helpers.cjs');
15
+ const { resetPaths } = require('./paths.cjs');
16
+
17
+ const {
18
+ parseJobFile, updateJobStep, moveJobFile,
19
+ generateMilestoneSteps, buildJobFileContent,
20
+ cmdJobsCreateMilestone, cmdJobsMilestonePreview,
21
+ findJobFile, updateJobHeader, insertJobSteps,
22
+ buildGapFixSteps, insertGapFixSection,
23
+ listJobs, cancelJob, recordStartShas, rollbackJob,
24
+ healthCheck, dryRunPreview, generateJobSummary,
25
+ } = require('./jobs.cjs');
26
+
27
+ // ─── Sample Job File Content ────────────────────────────────────────────────
28
+
29
+ const WELL_FORMED_JOB = `# Milestone Job: v6.0
30
+
31
+ **Version:** v6.0
32
+ **Created:** 2026-03-02T10:00:00Z
33
+ **Status:** in-progress
34
+ **Check:** true
35
+
36
+ ## Steps
37
+
38
+ - [x] \`/dgs:plan-phase 41\` \u2014 completed 2026-03-02T14:30:00Z
39
+ - [>] \`/dgs:plan-phase 42\` \u2014 started 2026-03-02T14:00:00Z
40
+ - [ ] \`/dgs:execute-phase 41\`
41
+ - [!] \`/dgs:plan-phase 43\` \u2014 failed 2026-03-02T15:00:00Z: Planning inconclusive after retry
42
+ `;
43
+
44
+ const ALL_COMPLETED_JOB = `# Milestone Job: v5.0
45
+
46
+ **Version:** v5.0
47
+ **Created:** 2026-03-01T08:00:00Z
48
+ **Status:** completed
49
+ **Check:** false
50
+
51
+ ## Steps
52
+
53
+ - [x] \`/dgs:plan-phase 41\` \u2014 completed 2026-03-01T09:00:00Z
54
+ - [x] \`/dgs:execute-phase 41\` \u2014 completed 2026-03-01T10:00:00Z
55
+ `;
56
+
57
+ const EMPTY_STEPS_JOB = `# Milestone Job: v7.0
58
+
59
+ **Version:** v7.0
60
+ **Created:** 2026-04-01T12:00:00Z
61
+ **Status:** pending
62
+ **Check:** true
63
+
64
+ ## Steps
65
+
66
+ `;
67
+
68
+ const MISSING_VERSION_JOB = `# Milestone Job: v8.0
69
+
70
+ **Created:** 2026-04-01T12:00:00Z
71
+ **Status:** pending
72
+ **Check:** true
73
+
74
+ ## Steps
75
+
76
+ - [ ] \`/dgs:plan-phase 50\`
77
+ `;
78
+
79
+ const MISSING_CREATED_JOB = `# Milestone Job: v8.0
80
+
81
+ **Version:** v8.0
82
+ **Status:** pending
83
+ **Check:** true
84
+
85
+ ## Steps
86
+
87
+ - [ ] \`/dgs:plan-phase 50\`
88
+ `;
89
+
90
+ const MISSING_STATUS_JOB = `# Milestone Job: v8.0
91
+
92
+ **Version:** v8.0
93
+ **Created:** 2026-04-01T12:00:00Z
94
+ **Check:** true
95
+
96
+ ## Steps
97
+
98
+ - [ ] \`/dgs:plan-phase 50\`
99
+ `;
100
+
101
+ const MISSING_CHECK_JOB = `# Milestone Job: v8.0
102
+
103
+ **Version:** v8.0
104
+ **Created:** 2026-04-01T12:00:00Z
105
+ **Status:** pending
106
+
107
+ ## Steps
108
+
109
+ - [ ] \`/dgs:plan-phase 50\`
110
+ `;
111
+
112
+ const UNRECOGNIZED_COMMAND_JOB = `# Milestone Job: v6.0
113
+
114
+ **Version:** v6.0
115
+ **Created:** 2026-03-02T10:00:00Z
116
+ **Status:** pending
117
+ **Check:** true
118
+
119
+ ## Steps
120
+
121
+ - [ ] \`/dgs:plan-phase 41\`
122
+ - [ ] \`/dgs:unknown-cmd 42\`
123
+ - [ ] \`/dgs:execute-phase 41\`
124
+ `;
125
+
126
+ const AUDIT_AND_COMPLETE_JOB = `# Milestone Job: v6.0
127
+
128
+ **Version:** v6.0
129
+ **Created:** 2026-03-02T10:00:00Z
130
+ **Status:** pending
131
+ **Check:** true
132
+
133
+ ## Steps
134
+
135
+ - [ ] \`/dgs:audit-milestone v6\`
136
+ - [ ] \`/dgs:complete-milestone v6\`
137
+ - [ ] \`/dgs:plan-milestone-gaps\`
138
+ - [ ] \`/dgs:discuss-phase 41\`
139
+ - [ ] \`/dgs:research-phase 42\`
140
+ - [ ] \`/dgs:verify-phase 43\`
141
+ `;
142
+
143
+ const UAT_WITH_HUMAN_NEEDED = `---
144
+ mode: auto-test
145
+ ai_verified: true
146
+ ---
147
+
148
+ # UAT: Phase 50
149
+
150
+ ## Tests
151
+
152
+ ### 1. Build compiles successfully
153
+ expected: Build exits with code 0
154
+ result: pass
155
+ command: npm run build
156
+
157
+ ### 2. Dashboard renders correctly
158
+ expected: Dashboard shows 3 cards in grid layout
159
+ result: human_needed
160
+ source: 50-01-PLAN.md
161
+
162
+ ### 3. Login form accessible
163
+ expected: Screen reader announces form fields
164
+ result: human_needed
165
+ source: VALIDATION.md
166
+
167
+ ## Summary
168
+ total: 3
169
+ passed: 1
170
+ issues: 0
171
+ human_needed: 2
172
+ pending: 0
173
+ skipped: 0
174
+ `;
175
+
176
+ const UAT_ALL_PASSED = `---
177
+ mode: auto-test
178
+ ai_verified: true
179
+ ---
180
+
181
+ # UAT: Phase 50
182
+
183
+ ## Tests
184
+
185
+ ### 1. Build compiles successfully
186
+ expected: Build exits with code 0
187
+ result: pass
188
+ command: npm run build
189
+
190
+ ### 2. Dashboard renders correctly
191
+ expected: Dashboard shows 3 cards in grid layout
192
+ result: pass
193
+ command: node -e "console.log('ok')"
194
+
195
+ ## Summary
196
+ total: 2
197
+ passed: 2
198
+ issues: 0
199
+ human_needed: 0
200
+ pending: 0
201
+ skipped: 0
202
+ `;
203
+
204
+ const UAT_MANUAL_WITH_HUMAN_NEEDED = `---
205
+ ai_verified: false
206
+ ---
207
+
208
+ # UAT: Phase 50
209
+
210
+ ## Tests
211
+
212
+ ### 1. Visual check
213
+ expected: Looks correct
214
+ result: human_needed
215
+ source: manual-check
216
+
217
+ ## Summary
218
+ total: 1
219
+ passed: 0
220
+ issues: 0
221
+ human_needed: 1
222
+ pending: 0
223
+ skipped: 0
224
+ `;
225
+
226
+ const ROADMAP_WITH_PHASE_50 = `# Roadmap
227
+
228
+ ## v6.0 Auto-Verify (In Progress)
229
+
230
+ Phases 50-50
231
+
232
+ ### Phase 50: Test Phase
233
+
234
+ - [ ] **Phase 50: Test Phase** - Testing
235
+ `;
236
+
237
+ const COMPLETED_JOB_V6 = `# Milestone Job: v6.0
238
+
239
+ **Version:** v6.0
240
+ **Created:** 2026-03-02T10:00:00Z
241
+ **Status:** completed
242
+ **Check:** true
243
+
244
+ ## Steps
245
+
246
+ - [x] \`/dgs:plan-phase 50\` \u2014 completed 2026-03-02T14:30:00Z
247
+ - [x] \`/dgs:execute-phase 50\` \u2014 completed 2026-03-02T15:00:00Z
248
+ `;
249
+
250
+ const WELL_FORMED_JOB_WITH_CREATED_BY = `# Milestone Job: v6.0
251
+
252
+ **Version:** v6.0
253
+ **Created:** 2026-03-02T10:00:00Z
254
+ **Created_by:** Adrian <adrian@example.com>
255
+ **Status:** in-progress
256
+ **Check:** true
257
+
258
+ ## Steps
259
+
260
+ - [x] \`/dgs:plan-phase 41\` \u2014 completed 2026-03-02T14:30:00Z
261
+ - [ ] \`/dgs:execute-phase 41\`
262
+ `;
263
+
264
+ // ─── parseJobFile Tests ─────────────────────────────────────────────────────
265
+
266
+ describe('jobs', () => {
267
+
268
+ describe('parseJobFile', () => {
269
+ let fixture;
270
+
271
+ afterEach(() => {
272
+ if (fixture) fixture.cleanup();
273
+ });
274
+
275
+ it('parses well-formed file with mixed statuses', () => {
276
+ fixture = createFixture({
277
+ 'job-v6.md': WELL_FORMED_JOB,
278
+ });
279
+ const result = parseJobFile(path.join(fixture.cwd, 'job-v6.md'));
280
+
281
+ assert.equal(result.version, 'v6.0');
282
+ assert.equal(result.created, '2026-03-02T10:00:00Z');
283
+ assert.equal(result.status, 'in-progress');
284
+ assert.equal(result.check, true);
285
+ assert.equal(result.stepCount, 4);
286
+ assert.equal(result.completedCount, 1);
287
+ assert.equal(result.failedCount, 1);
288
+ assert.equal(result.inProgressCount, 1);
289
+ assert.equal(result.nextStepIndex, 2);
290
+ assert.equal(result.progress, 25);
291
+
292
+ // Step 0: completed
293
+ assert.equal(result.steps[0].index, 0);
294
+ assert.equal(result.steps[0].status, 'completed');
295
+ assert.equal(result.steps[0].command, 'plan-phase');
296
+ assert.equal(result.steps[0].args, '41');
297
+ assert.equal(result.steps[0].raw, '/dgs:plan-phase 41');
298
+ assert.equal(result.steps[0].timestamp, '2026-03-02T14:30:00Z');
299
+ assert.equal(result.steps[0].error, null);
300
+
301
+ // Step 1: in-progress
302
+ assert.equal(result.steps[1].index, 1);
303
+ assert.equal(result.steps[1].status, 'in-progress');
304
+ assert.equal(result.steps[1].command, 'plan-phase');
305
+ assert.equal(result.steps[1].args, '42');
306
+ assert.equal(result.steps[1].timestamp, '2026-03-02T14:00:00Z');
307
+
308
+ // Step 2: pending
309
+ assert.equal(result.steps[2].index, 2);
310
+ assert.equal(result.steps[2].status, 'pending');
311
+ assert.equal(result.steps[2].command, 'execute-phase');
312
+ assert.equal(result.steps[2].args, '41');
313
+ assert.equal(result.steps[2].timestamp, null);
314
+
315
+ // Step 3: failed with error
316
+ assert.equal(result.steps[3].index, 3);
317
+ assert.equal(result.steps[3].status, 'failed');
318
+ assert.equal(result.steps[3].command, 'plan-phase');
319
+ assert.equal(result.steps[3].args, '43');
320
+ assert.equal(result.steps[3].timestamp, '2026-03-02T15:00:00Z');
321
+ assert.equal(result.steps[3].error, 'Planning inconclusive after retry');
322
+ });
323
+
324
+ it('returns nextStepIndex null and progress 100 when all steps completed', () => {
325
+ fixture = createFixture({
326
+ 'job-v5.md': ALL_COMPLETED_JOB,
327
+ });
328
+ const result = parseJobFile(path.join(fixture.cwd, 'job-v5.md'));
329
+
330
+ assert.equal(result.version, 'v5.0');
331
+ assert.equal(result.status, 'completed');
332
+ assert.equal(result.check, false);
333
+ assert.equal(result.stepCount, 2);
334
+ assert.equal(result.completedCount, 2);
335
+ assert.equal(result.failedCount, 0);
336
+ assert.equal(result.inProgressCount, 0);
337
+ assert.equal(result.nextStepIndex, null);
338
+ assert.equal(result.progress, 100);
339
+ });
340
+
341
+ it('handles empty steps section', () => {
342
+ fixture = createFixture({
343
+ 'job-v7.md': EMPTY_STEPS_JOB,
344
+ });
345
+ const result = parseJobFile(path.join(fixture.cwd, 'job-v7.md'));
346
+
347
+ assert.equal(result.stepCount, 0);
348
+ assert.equal(result.nextStepIndex, null);
349
+ assert.equal(result.progress, 100);
350
+ assert.deepEqual(result.steps, []);
351
+ });
352
+
353
+ it('hard-fails on missing Version field', () => {
354
+ fixture = createFixture({
355
+ 'job-bad.md': MISSING_VERSION_JOB,
356
+ });
357
+ assert.throws(
358
+ () => parseJobFile(path.join(fixture.cwd, 'job-bad.md')),
359
+ (err) => err.message.includes('Version')
360
+ );
361
+ });
362
+
363
+ it('hard-fails on missing Created field', () => {
364
+ fixture = createFixture({
365
+ 'job-bad.md': MISSING_CREATED_JOB,
366
+ });
367
+ assert.throws(
368
+ () => parseJobFile(path.join(fixture.cwd, 'job-bad.md')),
369
+ (err) => err.message.includes('Created')
370
+ );
371
+ });
372
+
373
+ it('hard-fails on missing Status field', () => {
374
+ fixture = createFixture({
375
+ 'job-bad.md': MISSING_STATUS_JOB,
376
+ });
377
+ assert.throws(
378
+ () => parseJobFile(path.join(fixture.cwd, 'job-bad.md')),
379
+ (err) => err.message.includes('Status')
380
+ );
381
+ });
382
+
383
+ it('hard-fails on missing Check field', () => {
384
+ fixture = createFixture({
385
+ 'job-bad.md': MISSING_CHECK_JOB,
386
+ });
387
+ assert.throws(
388
+ () => parseJobFile(path.join(fixture.cwd, 'job-bad.md')),
389
+ (err) => err.message.includes('Check')
390
+ );
391
+ });
392
+
393
+ it('warns on unrecognized step commands without hard error', () => {
394
+ fixture = createFixture({
395
+ 'job-warn.md': UNRECOGNIZED_COMMAND_JOB,
396
+ });
397
+ const result = parseJobFile(path.join(fixture.cwd, 'job-warn.md'));
398
+
399
+ assert.equal(result.stepCount, 3);
400
+ // Step 0: recognized command, no warning
401
+ assert.equal(result.steps[0].command, 'plan-phase');
402
+ assert.equal(result.steps[0].warning, undefined);
403
+ // Step 1: unrecognized command, should have warning
404
+ assert.equal(result.steps[1].command, 'unknown-cmd');
405
+ assert.ok(result.steps[1].warning, 'Unrecognized command should have a warning');
406
+ // Step 2: recognized command, no warning
407
+ assert.equal(result.steps[2].command, 'execute-phase');
408
+ assert.equal(result.steps[2].warning, undefined);
409
+ });
410
+
411
+ it('parses version from header field, not filename', () => {
412
+ fixture = createFixture({
413
+ 'job-wrong-name.md': WELL_FORMED_JOB,
414
+ });
415
+ const result = parseJobFile(path.join(fixture.cwd, 'job-wrong-name.md'));
416
+ assert.equal(result.version, 'v6.0');
417
+ });
418
+
419
+ it('parses all known DGS commands correctly', () => {
420
+ fixture = createFixture({
421
+ 'job-cmds.md': AUDIT_AND_COMPLETE_JOB,
422
+ });
423
+ const result = parseJobFile(path.join(fixture.cwd, 'job-cmds.md'));
424
+
425
+ assert.equal(result.steps[0].command, 'audit-milestone');
426
+ assert.equal(result.steps[0].args, 'v6');
427
+ assert.equal(result.steps[1].command, 'complete-milestone');
428
+ assert.equal(result.steps[1].args, 'v6');
429
+ assert.equal(result.steps[2].command, 'plan-milestone-gaps');
430
+ assert.equal(result.steps[2].args, '');
431
+ assert.equal(result.steps[3].command, 'discuss-phase');
432
+ assert.equal(result.steps[3].args, '41');
433
+ assert.equal(result.steps[4].command, 'research-phase');
434
+ assert.equal(result.steps[4].args, '42');
435
+ assert.equal(result.steps[5].command, 'verify-phase');
436
+ assert.equal(result.steps[5].args, '43');
437
+
438
+ // All known commands should NOT have warnings
439
+ for (const step of result.steps) {
440
+ assert.equal(step.warning, undefined, `${step.command} should be recognized`);
441
+ }
442
+ });
443
+
444
+ it('hard-fails on non-existent file', () => {
445
+ assert.throws(
446
+ () => parseJobFile('/tmp/nonexistent-job-file-xyz.md'),
447
+ (err) => err.message.includes('not found') || err.message.includes('ENOENT')
448
+ );
449
+ });
450
+ });
451
+
452
+ // ─── updateJobStep Tests ──────────────────────────────────────────────────
453
+
454
+ describe('updateJobStep', () => {
455
+ let fixture;
456
+
457
+ afterEach(() => {
458
+ if (fixture) fixture.cleanup();
459
+ });
460
+
461
+ it('marks step as completed with timestamp', () => {
462
+ fixture = createFixture({
463
+ 'job.md': WELL_FORMED_JOB,
464
+ });
465
+ const filePath = path.join(fixture.cwd, 'job.md');
466
+ updateJobStep(filePath, 2, 'completed', { timestamp: '2026-03-02T16:00:00Z' });
467
+
468
+ const content = fs.readFileSync(filePath, 'utf-8');
469
+ assert.ok(content.includes('- [x] `/dgs:execute-phase 41` \u2014 completed 2026-03-02T16:00:00Z'));
470
+ });
471
+
472
+ it('marks step as failed with error message', () => {
473
+ fixture = createFixture({
474
+ 'job.md': WELL_FORMED_JOB,
475
+ });
476
+ const filePath = path.join(fixture.cwd, 'job.md');
477
+ updateJobStep(filePath, 2, 'failed', {
478
+ timestamp: '2026-03-02T16:00:00Z',
479
+ error: 'Some error occurred',
480
+ });
481
+
482
+ const content = fs.readFileSync(filePath, 'utf-8');
483
+ assert.ok(content.includes('- [!] `/dgs:execute-phase 41` \u2014 failed 2026-03-02T16:00:00Z: Some error occurred'));
484
+ });
485
+
486
+ it('marks step as in-progress with timestamp', () => {
487
+ fixture = createFixture({
488
+ 'job.md': WELL_FORMED_JOB,
489
+ });
490
+ const filePath = path.join(fixture.cwd, 'job.md');
491
+ updateJobStep(filePath, 2, 'in-progress', { timestamp: '2026-03-02T16:00:00Z' });
492
+
493
+ const content = fs.readFileSync(filePath, 'utf-8');
494
+ assert.ok(content.includes('- [>] `/dgs:execute-phase 41` \u2014 started 2026-03-02T16:00:00Z'));
495
+ });
496
+
497
+ it('truncates error messages longer than 120 characters', () => {
498
+ fixture = createFixture({
499
+ 'job.md': WELL_FORMED_JOB,
500
+ });
501
+ const filePath = path.join(fixture.cwd, 'job.md');
502
+ const longError = 'A'.repeat(200);
503
+ updateJobStep(filePath, 2, 'failed', {
504
+ timestamp: '2026-03-02T16:00:00Z',
505
+ error: longError,
506
+ });
507
+
508
+ const content = fs.readFileSync(filePath, 'utf-8');
509
+ // Error should be truncated to ~120 chars + "..."
510
+ const stepLine = content.split('\n').find(l => l.includes('execute-phase 41'));
511
+ assert.ok(stepLine, 'Step line should exist');
512
+ const errorPart = stepLine.split(': ').slice(1).join(': ');
513
+ // The error on the line should be truncated
514
+ assert.ok(errorPart.length <= 130, `Error should be truncated, got length ${errorPart.length}`);
515
+ assert.ok(errorPart.endsWith('...'), 'Truncated error should end with ...');
516
+ });
517
+
518
+ it('hard-errors on negative step index', () => {
519
+ fixture = createFixture({
520
+ 'job.md': WELL_FORMED_JOB,
521
+ });
522
+ const filePath = path.join(fixture.cwd, 'job.md');
523
+ assert.throws(
524
+ () => updateJobStep(filePath, -1, 'completed', { timestamp: '2026-03-02T16:00:00Z' }),
525
+ (err) => err.message.includes('out of range')
526
+ );
527
+ });
528
+
529
+ it('hard-errors on step index >= stepCount', () => {
530
+ fixture = createFixture({
531
+ 'job.md': WELL_FORMED_JOB,
532
+ });
533
+ const filePath = path.join(fixture.cwd, 'job.md');
534
+ assert.throws(
535
+ () => updateJobStep(filePath, 10, 'completed', { timestamp: '2026-03-02T16:00:00Z' }),
536
+ (err) => err.message.includes('out of range')
537
+ );
538
+ });
539
+
540
+ it('preserves all other content (header, other steps) exactly', () => {
541
+ fixture = createFixture({
542
+ 'job.md': WELL_FORMED_JOB,
543
+ });
544
+ const filePath = path.join(fixture.cwd, 'job.md');
545
+ const before = fs.readFileSync(filePath, 'utf-8');
546
+
547
+ updateJobStep(filePath, 2, 'completed', { timestamp: '2026-03-02T16:00:00Z' });
548
+
549
+ const after = fs.readFileSync(filePath, 'utf-8');
550
+
551
+ // Header should be preserved
552
+ assert.ok(after.includes('# Milestone Job: v6.0'));
553
+ assert.ok(after.includes('**Version:** v6.0'));
554
+ assert.ok(after.includes('**Created:** 2026-03-02T10:00:00Z'));
555
+ assert.ok(after.includes('**Status:** in-progress'));
556
+ assert.ok(after.includes('**Check:** true'));
557
+
558
+ // Other steps should be preserved exactly
559
+ assert.ok(after.includes('- [x] `/dgs:plan-phase 41` \u2014 completed 2026-03-02T14:30:00Z'));
560
+ assert.ok(after.includes('- [>] `/dgs:plan-phase 42` \u2014 started 2026-03-02T14:00:00Z'));
561
+ assert.ok(after.includes('- [!] `/dgs:plan-phase 43` \u2014 failed 2026-03-02T15:00:00Z: Planning inconclusive after retry'));
562
+ });
563
+
564
+ it('resets a previously completed step to pending', () => {
565
+ fixture = createFixture({
566
+ 'job.md': WELL_FORMED_JOB,
567
+ });
568
+ const filePath = path.join(fixture.cwd, 'job.md');
569
+ updateJobStep(filePath, 0, 'pending');
570
+
571
+ const content = fs.readFileSync(filePath, 'utf-8');
572
+ assert.ok(content.includes('- [ ] `/dgs:plan-phase 41`'));
573
+ // Should not have any annotation after backtick close
574
+ const stepLine = content.split('\n').find(l => l.includes('plan-phase 41') && l.startsWith('- [ ]'));
575
+ assert.ok(stepLine, 'Pending step line should exist');
576
+ assert.ok(!stepLine.includes('\u2014'), 'Pending step should have no annotation');
577
+ });
578
+ });
579
+
580
+ // ─── moveJobFile Tests ────────────────────────────────────────────────────
581
+
582
+ describe('moveJobFile', () => {
583
+ let fixture;
584
+
585
+ afterEach(() => {
586
+ if (fixture) fixture.cleanup();
587
+ });
588
+
589
+ it('moves file from source to target directory', () => {
590
+ fixture = createFixture({
591
+ 'pending/job-v6.md': WELL_FORMED_JOB,
592
+ 'in-progress/': null,
593
+ });
594
+ const src = path.join(fixture.cwd, 'pending', 'job-v6.md');
595
+ const targetDir = path.join(fixture.cwd, 'in-progress');
596
+
597
+ const result = moveJobFile(src, targetDir);
598
+
599
+ assert.equal(result.moved, true);
600
+ assert.ok(!fs.existsSync(src), 'Source file should be removed');
601
+ assert.ok(fs.existsSync(path.join(targetDir, 'job-v6.md')), 'File should exist in target');
602
+ });
603
+
604
+ it('auto-creates target directory if it does not exist', () => {
605
+ fixture = createFixture({
606
+ 'pending/job-v6.md': WELL_FORMED_JOB,
607
+ });
608
+ const src = path.join(fixture.cwd, 'pending', 'job-v6.md');
609
+ const targetDir = path.join(fixture.cwd, 'completed');
610
+
611
+ const result = moveJobFile(src, targetDir);
612
+
613
+ assert.equal(result.moved, true);
614
+ assert.ok(fs.existsSync(targetDir), 'Target directory should have been created');
615
+ assert.ok(fs.existsSync(path.join(targetDir, 'job-v6.md')), 'File should exist in target');
616
+ assert.ok(!fs.existsSync(src), 'Source file should be removed');
617
+ });
618
+
619
+ it('returns no-op with warning when file already in target directory', () => {
620
+ fixture = createFixture({
621
+ 'in-progress/job-v6.md': WELL_FORMED_JOB,
622
+ });
623
+ const src = path.join(fixture.cwd, 'in-progress', 'job-v6.md');
624
+ const targetDir = path.join(fixture.cwd, 'in-progress');
625
+
626
+ const result = moveJobFile(src, targetDir);
627
+
628
+ assert.equal(result.moved, false);
629
+ assert.ok(result.warning, 'Should return a warning for no-op');
630
+ assert.ok(fs.existsSync(src), 'File should still exist');
631
+ });
632
+
633
+ it('returns from and to paths in result', () => {
634
+ fixture = createFixture({
635
+ 'pending/job-v6.md': WELL_FORMED_JOB,
636
+ 'completed/': null,
637
+ });
638
+ const src = path.join(fixture.cwd, 'pending', 'job-v6.md');
639
+ const targetDir = path.join(fixture.cwd, 'completed');
640
+
641
+ const result = moveJobFile(src, targetDir);
642
+
643
+ assert.equal(result.moved, true);
644
+ assert.equal(result.from, src);
645
+ assert.equal(result.to, path.join(targetDir, 'job-v6.md'));
646
+ });
647
+
648
+ it('handles deeply nested target directory creation', () => {
649
+ fixture = createFixture({
650
+ 'job-v6.md': WELL_FORMED_JOB,
651
+ });
652
+ const src = path.join(fixture.cwd, 'job-v6.md');
653
+ const targetDir = path.join(fixture.cwd, 'a', 'b', 'c', 'completed');
654
+
655
+ const result = moveJobFile(src, targetDir);
656
+
657
+ assert.equal(result.moved, true);
658
+ assert.ok(fs.existsSync(path.join(targetDir, 'job-v6.md')));
659
+ });
660
+ });
661
+
662
+ // ─── generateMilestoneSteps Tests ──────────────────────────────────────────
663
+
664
+ describe('generateMilestoneSteps', () => {
665
+
666
+ it('produces 4 steps for unplanned phase (no_directory)', () => {
667
+ const phases = [
668
+ { number: '50', name: 'Job Creation', disk_status: 'no_directory', roadmap_complete: false },
669
+ ];
670
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
671
+
672
+ assert.equal(steps.length, 4);
673
+ assert.equal(steps[0].command, 'map-codebase');
674
+ assert.equal(steps[0].args, '50 --auto');
675
+ assert.equal(steps[1].command, 'plan-phase');
676
+ assert.equal(steps[1].args, '50 --non-interactive');
677
+ assert.equal(steps[2].command, 'execute-phase');
678
+ assert.equal(steps[2].args, '50 --non-interactive');
679
+ assert.equal(steps[3].command, 'verify-work');
680
+ assert.equal(steps[3].args, '50 --auto-test');
681
+ });
682
+
683
+ it('produces 4 steps for unplanned phase (empty)', () => {
684
+ const phases = [
685
+ { number: '51', name: 'Job Execution', disk_status: 'empty', roadmap_complete: false },
686
+ ];
687
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
688
+
689
+ assert.equal(steps.length, 4);
690
+ assert.equal(steps[0].command, 'map-codebase');
691
+ assert.equal(steps[1].command, 'plan-phase');
692
+ assert.equal(steps[2].command, 'execute-phase');
693
+ assert.equal(steps[3].command, 'verify-work');
694
+ });
695
+
696
+ it('produces 4 steps for discussed phase (context but no plan)', () => {
697
+ const phases = [
698
+ { number: '52', name: 'Silent Mode', disk_status: 'discussed', roadmap_complete: false },
699
+ ];
700
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
701
+
702
+ assert.equal(steps.length, 4);
703
+ assert.equal(steps[0].command, 'map-codebase');
704
+ assert.equal(steps[1].command, 'plan-phase');
705
+ assert.equal(steps[2].command, 'execute-phase');
706
+ assert.equal(steps[3].command, 'verify-work');
707
+ });
708
+
709
+ it('produces 4 steps for researched phase', () => {
710
+ const phases = [
711
+ { number: '53', name: 'Audit Integration', disk_status: 'researched', roadmap_complete: false },
712
+ ];
713
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
714
+
715
+ assert.equal(steps.length, 4);
716
+ assert.equal(steps[0].command, 'map-codebase');
717
+ assert.equal(steps[1].command, 'plan-phase');
718
+ assert.equal(steps[2].command, 'execute-phase');
719
+ assert.equal(steps[3].command, 'verify-work');
720
+ });
721
+
722
+ it('produces 2 steps for planned phase', () => {
723
+ const phases = [
724
+ { number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
725
+ ];
726
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
727
+
728
+ assert.equal(steps.length, 2);
729
+ assert.equal(steps[0].command, 'execute-phase');
730
+ assert.equal(steps[0].args, '50 --non-interactive');
731
+ assert.equal(steps[1].command, 'verify-work');
732
+ assert.equal(steps[1].args, '50 --auto-test');
733
+ });
734
+
735
+ it('produces 2 steps for partially executed phase', () => {
736
+ const phases = [
737
+ { number: '50', name: 'Job Creation', disk_status: 'partial', roadmap_complete: false },
738
+ ];
739
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
740
+
741
+ assert.equal(steps.length, 2);
742
+ assert.equal(steps[0].command, 'execute-phase');
743
+ assert.equal(steps[1].command, 'verify-work');
744
+ assert.equal(steps[1].args, '50 --auto-test');
745
+ });
746
+
747
+ it('produces 0 steps for completed phase (disk_status complete)', () => {
748
+ const phases = [
749
+ { number: '49', name: 'Job File Format', disk_status: 'complete', roadmap_complete: true },
750
+ ];
751
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
752
+
753
+ assert.equal(steps.length, 0);
754
+ });
755
+
756
+ it('produces 0 steps for roadmap_complete phase even if disk_status is not complete', () => {
757
+ const phases = [
758
+ { number: '49', name: 'Job File Format', disk_status: 'planned', roadmap_complete: true },
759
+ ];
760
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
761
+
762
+ assert.equal(steps.length, 0);
763
+ });
764
+
765
+ it('handles mixed phases correctly (complete + unplanned + planned)', () => {
766
+ const phases = [
767
+ { number: '49', name: 'Job File Format', disk_status: 'complete', roadmap_complete: true },
768
+ { number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
769
+ { number: '51', name: 'Job Execution', disk_status: 'no_directory', roadmap_complete: false },
770
+ ];
771
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
772
+
773
+ // Phase 49: 0 steps (complete)
774
+ // Phase 50: 2 steps (planned -> execute + verify)
775
+ // Phase 51: 4 steps (unplanned -> map + plan + execute + verify)
776
+ assert.equal(steps.length, 6);
777
+ assert.equal(steps[0].command, 'execute-phase');
778
+ assert.equal(steps[0].args, '50 --non-interactive');
779
+ assert.equal(steps[1].command, 'verify-work');
780
+ assert.equal(steps[1].args, '50 --auto-test');
781
+ assert.equal(steps[2].command, 'map-codebase');
782
+ assert.equal(steps[2].args, '51 --auto');
783
+ assert.equal(steps[3].command, 'plan-phase');
784
+ assert.equal(steps[3].args, '51 --non-interactive');
785
+ assert.equal(steps[4].command, 'execute-phase');
786
+ assert.equal(steps[4].args, '51 --non-interactive');
787
+ assert.equal(steps[5].command, 'verify-work');
788
+ assert.equal(steps[5].args, '51 --auto-test');
789
+ });
790
+
791
+ it('appends audit and complete steps when check=true', () => {
792
+ const phases = [
793
+ { number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
794
+ ];
795
+ const steps = generateMilestoneSteps(phases, { check: true, version: 'v6.0' });
796
+
797
+ // 2 phase steps + 2 check steps = 4
798
+ assert.equal(steps.length, 4);
799
+ assert.equal(steps[2].command, 'audit-milestone');
800
+ assert.equal(steps[2].args, 'v6.0');
801
+ assert.equal(steps[3].command, 'complete-milestone');
802
+ assert.equal(steps[3].args, 'v6.0');
803
+ });
804
+
805
+ it('does NOT append audit/complete steps when check=false', () => {
806
+ const phases = [
807
+ { number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
808
+ ];
809
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
810
+
811
+ assert.equal(steps.length, 2);
812
+ // No audit/complete steps
813
+ assert.ok(!steps.some(s => s.command === 'audit-milestone'));
814
+ assert.ok(!steps.some(s => s.command === 'complete-milestone'));
815
+ });
816
+
817
+ it('returns only audit+complete when all phases complete and check=true', () => {
818
+ const phases = [
819
+ { number: '49', name: 'Job File Format', disk_status: 'complete', roadmap_complete: true },
820
+ { number: '50', name: 'Job Creation', disk_status: 'complete', roadmap_complete: true },
821
+ ];
822
+ const steps = generateMilestoneSteps(phases, { check: true, version: 'v6.0' });
823
+
824
+ assert.equal(steps.length, 2);
825
+ assert.equal(steps[0].command, 'audit-milestone');
826
+ assert.equal(steps[1].command, 'complete-milestone');
827
+ });
828
+
829
+ it('returns empty array when all phases complete and check=false', () => {
830
+ const phases = [
831
+ { number: '49', name: 'Job File Format', disk_status: 'complete', roadmap_complete: true },
832
+ { number: '50', name: 'Job Creation', disk_status: 'complete', roadmap_complete: true },
833
+ ];
834
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
835
+
836
+ assert.equal(steps.length, 0);
837
+ });
838
+
839
+ it('handles decimal phase numbers in correct sequence order', () => {
840
+ const phases = [
841
+ { number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
842
+ { number: '50.1', name: 'Hotfix', disk_status: 'no_directory', roadmap_complete: false },
843
+ { number: '51', name: 'Job Execution', disk_status: 'planned', roadmap_complete: false },
844
+ ];
845
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
846
+
847
+ // Phase 50: 2 steps (execute + verify), Phase 50.1: 4 steps (map + plan + execute + verify), Phase 51: 2 steps (execute + verify) = 8 total
848
+ assert.equal(steps.length, 8);
849
+ // First phase should be 50 (execute-phase)
850
+ assert.equal(steps[0].args, '50 --non-interactive');
851
+ // Then 50.1 (map-codebase at index 2)
852
+ assert.equal(steps[2].command, 'map-codebase');
853
+ assert.equal(steps[2].args, '50.1 --auto');
854
+ // Then 51 (execute-phase at index 6)
855
+ assert.equal(steps[6].args, '51 --non-interactive');
856
+ });
857
+
858
+ it('includes correct flags on plan-phase (--non-interactive), execute-phase (--non-interactive), and map-codebase (--auto) steps', () => {
859
+ const phases = [
860
+ { number: '50', name: 'Job Creation', disk_status: 'no_directory', roadmap_complete: false },
861
+ ];
862
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
863
+
864
+ const mapStep = steps.find(s => s.command === 'map-codebase');
865
+ const planStep = steps.find(s => s.command === 'plan-phase');
866
+ const executeStep = steps.find(s => s.command === 'execute-phase');
867
+ const verifyStep = steps.find(s => s.command === 'verify-work');
868
+
869
+ assert.ok(mapStep.args.includes('--auto'), 'map-codebase should have --auto');
870
+ assert.ok(planStep.args.includes('--non-interactive'), 'plan-phase should have --non-interactive');
871
+ assert.ok(executeStep.args.includes('--non-interactive'), 'execute-phase should have --non-interactive');
872
+ assert.ok(verifyStep.args.includes('--auto-test'), 'verify-work should have --auto-test');
873
+ });
874
+
875
+ it('sorts phases by number numerically, not lexicographically', () => {
876
+ const phases = [
877
+ { number: '51', name: 'Job Execution', disk_status: 'planned', roadmap_complete: false },
878
+ { number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
879
+ { number: '49', name: 'Job File Format', disk_status: 'complete', roadmap_complete: true },
880
+ ];
881
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
882
+
883
+ // Phase 49 complete (0 steps), Phase 50 planned (2 steps), Phase 51 planned (2 steps) = 4
884
+ assert.equal(steps.length, 4);
885
+ assert.equal(steps[0].args, '50 --non-interactive');
886
+ assert.equal(steps[1].args, '50 --auto-test');
887
+ assert.equal(steps[2].args, '51 --non-interactive');
888
+ assert.equal(steps[3].args, '51 --auto-test');
889
+ });
890
+
891
+ it('returns steps as objects with command and args properties', () => {
892
+ const phases = [
893
+ { number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
894
+ ];
895
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
896
+
897
+ for (const step of steps) {
898
+ assert.ok(typeof step.command === 'string', 'step should have command string');
899
+ assert.ok(typeof step.args === 'string', 'step should have args string');
900
+ }
901
+ });
902
+
903
+ it('map-codebase step has --auto flag', () => {
904
+ const phases = [
905
+ { number: '50', name: 'Job Creation', disk_status: 'no_directory', roadmap_complete: false },
906
+ { number: '51', name: 'Job Execution', disk_status: 'empty', roadmap_complete: false },
907
+ ];
908
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
909
+
910
+ const mapSteps = steps.filter(s => s.command === 'map-codebase');
911
+ assert.ok(mapSteps.length > 0, 'Should have map-codebase steps');
912
+ for (const step of mapSteps) {
913
+ assert.ok(step.args.includes('--auto'), `map-codebase step for ${step.args} should have --auto`);
914
+ }
915
+ });
916
+
917
+ it('verify-work steps include --auto-test flag', () => {
918
+ const phases = [
919
+ { number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
920
+ { number: '51', name: 'Job Execution', disk_status: 'no_directory', roadmap_complete: false },
921
+ ];
922
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
923
+
924
+ const verifySteps = steps.filter(s => s.command === 'verify-work');
925
+ assert.ok(verifySteps.length > 0, 'Should have verify-work steps');
926
+ for (const step of verifySteps) {
927
+ assert.ok(step.args.includes('--auto-test'), `verify-work step for ${step.args} should have --auto-test`);
928
+ }
929
+ });
930
+
931
+ it('no map-codebase for NEEDS_EXECUTION phases', () => {
932
+ const phases = [
933
+ { number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
934
+ { number: '51', name: 'Job Execution', disk_status: 'partial', roadmap_complete: false },
935
+ ];
936
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
937
+
938
+ const mapSteps = steps.filter(s => s.command === 'map-codebase');
939
+ assert.equal(mapSteps.length, 0, 'Planned and partial phases should NOT have map-codebase steps');
940
+ });
941
+ });
942
+
943
+ // ─── buildJobFileContent Tests ─────────────────────────────────────────────
944
+
945
+ describe('buildJobFileContent', () => {
946
+
947
+ it('produces valid markdown with header fields', () => {
948
+ const steps = [
949
+ { command: 'plan-phase', args: '50 --non-interactive' },
950
+ { command: 'execute-phase', args: '50 --non-interactive' },
951
+ { command: 'verify-work', args: '50' },
952
+ ];
953
+ const content = buildJobFileContent('v6.0', true, steps);
954
+
955
+ assert.ok(content.includes('# Milestone Job: v6.0'), 'Should have title');
956
+ assert.ok(content.includes('**Version:** v6.0'), 'Should have version');
957
+ assert.ok(content.includes('**Status:** pending'), 'Should have status');
958
+ assert.ok(content.includes('**Check:** true'), 'Should have check=true');
959
+ });
960
+
961
+ it('has Created field with ISO timestamp', () => {
962
+ const content = buildJobFileContent('v6.0', true, []);
963
+ const createdMatch = content.match(/\*\*Created:\*\*\s*(\S+)/);
964
+ assert.ok(createdMatch, 'Should have Created field');
965
+ // Verify ISO format: YYYY-MM-DDTHH:MM:SSZ
966
+ assert.ok(createdMatch[1].match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/), 'Created should be ISO timestamp');
967
+ });
968
+
969
+ it('produces checkbox lines for each step', () => {
970
+ const steps = [
971
+ { command: 'plan-phase', args: '50 --non-interactive' },
972
+ { command: 'execute-phase', args: '50 --non-interactive' },
973
+ { command: 'verify-work', args: '50' },
974
+ ];
975
+ const content = buildJobFileContent('v6.0', true, steps);
976
+
977
+ assert.ok(content.includes('- [ ] `/dgs:plan-phase 50 --non-interactive`'), 'plan-phase step');
978
+ assert.ok(content.includes('- [ ] `/dgs:execute-phase 50 --non-interactive`'), 'execute-phase step');
979
+ assert.ok(content.includes('- [ ] `/dgs:verify-work 50`'), 'verify-work step');
980
+ });
981
+
982
+ it('includes ## Steps section heading', () => {
983
+ const content = buildJobFileContent('v6.0', true, []);
984
+ assert.ok(content.includes('## Steps'), 'Should have Steps heading');
985
+ });
986
+
987
+ it('sets Check to false when check param is false', () => {
988
+ const content = buildJobFileContent('v6.0', false, []);
989
+ assert.ok(content.includes('**Check:** false'), 'Should have check=false');
990
+ });
991
+
992
+ it('round-trips through parseJobFile without errors', () => {
993
+ const steps = [
994
+ { command: 'plan-phase', args: '50 --non-interactive' },
995
+ { command: 'execute-phase', args: '50 --non-interactive' },
996
+ { command: 'verify-work', args: '50' },
997
+ { command: 'audit-milestone', args: 'v6.0' },
998
+ { command: 'complete-milestone', args: 'v6.0' },
999
+ ];
1000
+ const content = buildJobFileContent('v6.0', true, steps);
1001
+
1002
+ // Write to temp file, parse it back
1003
+ const fixture = createFixture({
1004
+ 'job-roundtrip.md': content,
1005
+ });
1006
+
1007
+ try {
1008
+ const parsed = parseJobFile(path.join(fixture.cwd, 'job-roundtrip.md'));
1009
+ assert.equal(parsed.version, 'v6.0');
1010
+ assert.equal(parsed.check, true);
1011
+ assert.equal(parsed.status, 'pending');
1012
+ assert.equal(parsed.stepCount, 5);
1013
+ assert.equal(parsed.steps[0].command, 'plan-phase');
1014
+ assert.equal(parsed.steps[0].args, '50 --non-interactive');
1015
+ assert.equal(parsed.steps[4].command, 'complete-milestone');
1016
+ assert.equal(parsed.steps[4].args, 'v6.0');
1017
+ } finally {
1018
+ fixture.cleanup();
1019
+ }
1020
+ });
1021
+
1022
+ it('ends with trailing newline', () => {
1023
+ const content = buildJobFileContent('v6.0', true, []);
1024
+ assert.ok(content.endsWith('\n'), 'Should end with newline');
1025
+ });
1026
+ });
1027
+
1028
+ // ─── cmdJobsCreateMilestone Tests ──────────────────────────────────────────
1029
+
1030
+ describe('cmdJobsCreateMilestone', () => {
1031
+ let fixture;
1032
+
1033
+ afterEach(() => {
1034
+ if (fixture) fixture.cleanup();
1035
+ });
1036
+
1037
+ const FIXTURE_ROADMAP = `# Roadmap
1038
+
1039
+ ## Milestones
1040
+
1041
+ - v6.0 Job Orchestration -- Phases 49-55 (in progress)
1042
+
1043
+ ## Phases
1044
+
1045
+ ### v6.0 Job Orchestration (In Progress)
1046
+
1047
+ - [x] **Phase 49: Job File Format & Infrastructure** (completed 2026-03-03)
1048
+ - [ ] **Phase 50: Job Creation**
1049
+ - [ ] **Phase 51: Job Execution Core**
1050
+
1051
+ ### Phase 49: Job File Format & Infrastructure
1052
+ **Goal**: Job files can be created, parsed, updated, and moved
1053
+ **Depends on**: Nothing
1054
+ **Plans:** 2/2 plans complete
1055
+ Plans:
1056
+ - [x] 49-01-PLAN.md
1057
+ - [x] 49-02-PLAN.md
1058
+
1059
+ ### Phase 50: Job Creation
1060
+ **Goal**: Users can generate a milestone build job
1061
+ **Depends on**: Phase 49
1062
+ Plans:
1063
+ - [ ] 50-01-PLAN.md
1064
+ - [ ] 50-02-PLAN.md
1065
+
1066
+ ### Phase 51: Job Execution Core
1067
+ **Goal**: Users can execute a milestone job
1068
+ **Depends on**: Phase 50
1069
+ `;
1070
+
1071
+ it('auto-detects active milestone version from ROADMAP.md', () => {
1072
+ fixture = createFixture({
1073
+ '.planning/ROADMAP.md': FIXTURE_ROADMAP,
1074
+ '.planning/phases/49-job-file-format/49-01-PLAN.md': '',
1075
+ '.planning/phases/49-job-file-format/49-02-PLAN.md': '',
1076
+ '.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
1077
+ '.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
1078
+ '.planning/phases/50-job-creation/50-01-PLAN.md': '',
1079
+ '.planning/phases/50-job-creation/50-02-PLAN.md': '',
1080
+ });
1081
+
1082
+ const result = cmdJobsCreateMilestone(fixture.cwd, null, true, false);
1083
+ assert.equal(result.version, 'v6.0');
1084
+ assert.equal(result.created, true);
1085
+ });
1086
+
1087
+ it('writes job file to .planning/jobs/pending/', () => {
1088
+ fixture = createFixture({
1089
+ '.planning/ROADMAP.md': FIXTURE_ROADMAP,
1090
+ '.planning/phases/49-job-file-format/49-01-PLAN.md': '',
1091
+ '.planning/phases/49-job-file-format/49-02-PLAN.md': '',
1092
+ '.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
1093
+ '.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
1094
+ '.planning/phases/50-job-creation/50-01-PLAN.md': '',
1095
+ '.planning/phases/50-job-creation/50-02-PLAN.md': '',
1096
+ });
1097
+
1098
+ const result = cmdJobsCreateMilestone(fixture.cwd, 'v6.0', true, false);
1099
+ assert.ok(result.file.includes('pending'));
1100
+ assert.ok(result.file.includes('milestone-v6.0.md'));
1101
+
1102
+ const jobFilePath = path.join(fixture.cwd, result.file);
1103
+ assert.ok(fs.existsSync(jobFilePath), 'Job file should exist on disk');
1104
+ });
1105
+
1106
+ it('auto-creates .planning/jobs/pending/ directory', () => {
1107
+ fixture = createFixture({
1108
+ '.planning/ROADMAP.md': FIXTURE_ROADMAP,
1109
+ '.planning/phases/49-job-file-format/49-01-PLAN.md': '',
1110
+ '.planning/phases/49-job-file-format/49-02-PLAN.md': '',
1111
+ '.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
1112
+ '.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
1113
+ '.planning/phases/50-job-creation/50-01-PLAN.md': '',
1114
+ '.planning/phases/50-job-creation/50-02-PLAN.md': '',
1115
+ });
1116
+
1117
+ // Ensure pending dir does not exist yet
1118
+ const pendingDir = path.join(fixture.cwd, '.planning', 'jobs', 'pending');
1119
+ assert.ok(!fs.existsSync(pendingDir), 'pending dir should not exist before call');
1120
+
1121
+ cmdJobsCreateMilestone(fixture.cwd, 'v6.0', true, false);
1122
+ assert.ok(fs.existsSync(pendingDir), 'pending dir should be auto-created');
1123
+ });
1124
+
1125
+ it('returns JSON with expected fields', () => {
1126
+ fixture = createFixture({
1127
+ '.planning/ROADMAP.md': FIXTURE_ROADMAP,
1128
+ '.planning/phases/49-job-file-format/49-01-PLAN.md': '',
1129
+ '.planning/phases/49-job-file-format/49-02-PLAN.md': '',
1130
+ '.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
1131
+ '.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
1132
+ '.planning/phases/50-job-creation/50-01-PLAN.md': '',
1133
+ '.planning/phases/50-job-creation/50-02-PLAN.md': '',
1134
+ });
1135
+
1136
+ const result = cmdJobsCreateMilestone(fixture.cwd, 'v6.0', true, false);
1137
+ assert.equal(result.created, true);
1138
+ assert.equal(result.version, 'v6.0');
1139
+ assert.ok(typeof result.file === 'string');
1140
+ assert.ok(typeof result.step_count === 'number');
1141
+ assert.ok(typeof result.phase_count === 'number');
1142
+ assert.ok(Array.isArray(result.steps_preview));
1143
+ assert.ok(result.step_count > 0);
1144
+ });
1145
+
1146
+ it('throws error when no ROADMAP.md exists', () => {
1147
+ fixture = createFixture({
1148
+ '.planning/phases/': null,
1149
+ });
1150
+
1151
+ assert.throws(
1152
+ () => cmdJobsCreateMilestone(fixture.cwd, null, true, false),
1153
+ (err) => err.message.includes('ROADMAP.md')
1154
+ );
1155
+ });
1156
+
1157
+ it('throws error when specified version not found in ROADMAP', () => {
1158
+ fixture = createFixture({
1159
+ '.planning/ROADMAP.md': FIXTURE_ROADMAP,
1160
+ '.planning/phases/': null,
1161
+ });
1162
+
1163
+ assert.throws(
1164
+ () => cmdJobsCreateMilestone(fixture.cwd, 'v99.0', true, false),
1165
+ (err) => err.message.includes('v99.0') || err.message.includes('not found')
1166
+ );
1167
+ });
1168
+ });
1169
+
1170
+ // ─── cmdJobsMilestonePreview Tests ─────────────────────────────────────────
1171
+
1172
+ describe('cmdJobsMilestonePreview', () => {
1173
+ let fixture;
1174
+
1175
+ afterEach(() => {
1176
+ if (fixture) fixture.cleanup();
1177
+ });
1178
+
1179
+ const FIXTURE_ROADMAP = `# Roadmap
1180
+
1181
+ ## Milestones
1182
+
1183
+ - v6.0 Job Orchestration -- Phases 49-55 (in progress)
1184
+
1185
+ ## Phases
1186
+
1187
+ ### v6.0 Job Orchestration (In Progress)
1188
+
1189
+ - [x] **Phase 49: Job File Format & Infrastructure** (completed 2026-03-03)
1190
+ - [ ] **Phase 50: Job Creation**
1191
+ - [ ] **Phase 51: Job Execution Core**
1192
+
1193
+ ### Phase 49: Job File Format & Infrastructure
1194
+ **Goal**: Job files can be created, parsed, updated, and moved
1195
+ **Depends on**: Nothing
1196
+ **Plans:** 2/2 plans complete
1197
+ Plans:
1198
+ - [x] 49-01-PLAN.md
1199
+ - [x] 49-02-PLAN.md
1200
+
1201
+ ### Phase 50: Job Creation
1202
+ **Goal**: Users can generate a milestone build job
1203
+ **Depends on**: Phase 49
1204
+ Plans:
1205
+ - [ ] 50-01-PLAN.md
1206
+ - [ ] 50-02-PLAN.md
1207
+
1208
+ ### Phase 51: Job Execution Core
1209
+ **Goal**: Users can execute a milestone job
1210
+ **Depends on**: Phase 50
1211
+ `;
1212
+
1213
+ it('returns preview JSON with expected fields', () => {
1214
+ fixture = createFixture({
1215
+ '.planning/ROADMAP.md': FIXTURE_ROADMAP,
1216
+ '.planning/phases/49-job-file-format/49-01-PLAN.md': '',
1217
+ '.planning/phases/49-job-file-format/49-02-PLAN.md': '',
1218
+ '.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
1219
+ '.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
1220
+ '.planning/phases/50-job-creation/50-01-PLAN.md': '',
1221
+ '.planning/phases/50-job-creation/50-02-PLAN.md': '',
1222
+ });
1223
+
1224
+ const result = cmdJobsMilestonePreview(fixture.cwd, 'v6.0', true, false);
1225
+ assert.equal(result.preview, true);
1226
+ assert.equal(result.version, 'v6.0');
1227
+ assert.equal(result.check, true);
1228
+ assert.ok(typeof result.step_count === 'number');
1229
+ assert.ok(typeof result.phase_count === 'number');
1230
+ assert.ok(Array.isArray(result.steps_preview));
1231
+ assert.ok(typeof result.content === 'string');
1232
+ });
1233
+
1234
+ it('does NOT write any file to disk', () => {
1235
+ fixture = createFixture({
1236
+ '.planning/ROADMAP.md': FIXTURE_ROADMAP,
1237
+ '.planning/phases/49-job-file-format/49-01-PLAN.md': '',
1238
+ '.planning/phases/49-job-file-format/49-02-PLAN.md': '',
1239
+ '.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
1240
+ '.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
1241
+ '.planning/phases/50-job-creation/50-01-PLAN.md': '',
1242
+ '.planning/phases/50-job-creation/50-02-PLAN.md': '',
1243
+ });
1244
+
1245
+ cmdJobsMilestonePreview(fixture.cwd, 'v6.0', true, false);
1246
+
1247
+ const pendingDir = path.join(fixture.cwd, '.planning', 'jobs', 'pending');
1248
+ assert.ok(!fs.existsSync(pendingDir), 'No jobs directory should be created for preview');
1249
+ });
1250
+
1251
+ it('steps_preview is array of step command strings', () => {
1252
+ fixture = createFixture({
1253
+ '.planning/ROADMAP.md': FIXTURE_ROADMAP,
1254
+ '.planning/phases/49-job-file-format/49-01-PLAN.md': '',
1255
+ '.planning/phases/49-job-file-format/49-02-PLAN.md': '',
1256
+ '.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
1257
+ '.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
1258
+ '.planning/phases/50-job-creation/50-01-PLAN.md': '',
1259
+ '.planning/phases/50-job-creation/50-02-PLAN.md': '',
1260
+ });
1261
+
1262
+ const result = cmdJobsMilestonePreview(fixture.cwd, 'v6.0', true, false);
1263
+ for (const preview of result.steps_preview) {
1264
+ assert.ok(typeof preview === 'string', 'Each preview should be a string');
1265
+ assert.ok(preview.startsWith('/dgs:'), 'Each preview should start with /dgs:');
1266
+ }
1267
+ });
1268
+
1269
+ it('content is the full markdown that would be written', () => {
1270
+ fixture = createFixture({
1271
+ '.planning/ROADMAP.md': FIXTURE_ROADMAP,
1272
+ '.planning/phases/49-job-file-format/49-01-PLAN.md': '',
1273
+ '.planning/phases/49-job-file-format/49-02-PLAN.md': '',
1274
+ '.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
1275
+ '.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
1276
+ '.planning/phases/50-job-creation/50-01-PLAN.md': '',
1277
+ '.planning/phases/50-job-creation/50-02-PLAN.md': '',
1278
+ });
1279
+
1280
+ const result = cmdJobsMilestonePreview(fixture.cwd, 'v6.0', true, false);
1281
+ assert.ok(result.content.includes('# Milestone Job: v6.0'));
1282
+ assert.ok(result.content.includes('## Steps'));
1283
+ });
1284
+
1285
+ it('phase_count counts distinct phases with steps', () => {
1286
+ fixture = createFixture({
1287
+ '.planning/ROADMAP.md': FIXTURE_ROADMAP,
1288
+ '.planning/phases/49-job-file-format/49-01-PLAN.md': '',
1289
+ '.planning/phases/49-job-file-format/49-02-PLAN.md': '',
1290
+ '.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
1291
+ '.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
1292
+ '.planning/phases/50-job-creation/50-01-PLAN.md': '',
1293
+ '.planning/phases/50-job-creation/50-02-PLAN.md': '',
1294
+ });
1295
+
1296
+ const result = cmdJobsMilestonePreview(fixture.cwd, 'v6.0', true, false);
1297
+ // Phase 49 complete (0 steps), Phase 50 planned (2 steps), Phase 51 no_directory (3 steps) = 2 phases
1298
+ assert.equal(result.phase_count, 2);
1299
+ });
1300
+
1301
+ it('combines correctly with --no-check (omits audit/complete from preview)', () => {
1302
+ fixture = createFixture({
1303
+ '.planning/ROADMAP.md': FIXTURE_ROADMAP,
1304
+ '.planning/phases/49-job-file-format/49-01-PLAN.md': '',
1305
+ '.planning/phases/49-job-file-format/49-02-PLAN.md': '',
1306
+ '.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
1307
+ '.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
1308
+ '.planning/phases/50-job-creation/50-01-PLAN.md': '',
1309
+ '.planning/phases/50-job-creation/50-02-PLAN.md': '',
1310
+ });
1311
+
1312
+ const withCheck = cmdJobsMilestonePreview(fixture.cwd, 'v6.0', true, false);
1313
+ const withoutCheck = cmdJobsMilestonePreview(fixture.cwd, 'v6.0', false, false);
1314
+
1315
+ assert.equal(withCheck.check, true);
1316
+ assert.equal(withoutCheck.check, false);
1317
+ assert.ok(withCheck.step_count > withoutCheck.step_count, 'check=true should have more steps');
1318
+ assert.ok(withCheck.steps_preview.some(s => s.includes('audit-milestone')), 'check=true should have audit');
1319
+ assert.ok(!withoutCheck.steps_preview.some(s => s.includes('audit-milestone')), 'check=false should NOT have audit');
1320
+ });
1321
+
1322
+ it('auto-detects milestone version same as create', () => {
1323
+ fixture = createFixture({
1324
+ '.planning/ROADMAP.md': FIXTURE_ROADMAP,
1325
+ '.planning/phases/49-job-file-format/49-01-PLAN.md': '',
1326
+ '.planning/phases/49-job-file-format/49-02-PLAN.md': '',
1327
+ '.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
1328
+ '.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
1329
+ '.planning/phases/50-job-creation/50-01-PLAN.md': '',
1330
+ '.planning/phases/50-job-creation/50-02-PLAN.md': '',
1331
+ });
1332
+
1333
+ const result = cmdJobsMilestonePreview(fixture.cwd, null, true, false);
1334
+ assert.equal(result.version, 'v6.0');
1335
+ });
1336
+
1337
+ it('throws error when no ROADMAP.md exists', () => {
1338
+ fixture = createFixture({
1339
+ '.planning/phases/': null,
1340
+ });
1341
+
1342
+ assert.throws(
1343
+ () => cmdJobsMilestonePreview(fixture.cwd, null, true, false),
1344
+ (err) => err.message.includes('ROADMAP.md')
1345
+ );
1346
+ });
1347
+ });
1348
+
1349
+ // ─── findJobFile Tests ──────────────────────────────────────────────────
1350
+
1351
+ describe('findJobFile', () => {
1352
+ let fixture;
1353
+
1354
+ afterEach(() => {
1355
+ if (fixture) fixture.cleanup();
1356
+ });
1357
+
1358
+ it('finds job in in-progress/ when file exists there', () => {
1359
+ fixture = createFixture({
1360
+ '.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
1361
+ });
1362
+ const result = findJobFile(fixture.cwd, 'v6.0');
1363
+ assert.equal(result.found, true);
1364
+ assert.equal(result.directory, 'in-progress');
1365
+ assert.ok(result.path.includes('in-progress'));
1366
+ assert.ok(result.path.includes('milestone-v6.0.md'));
1367
+ });
1368
+
1369
+ it('finds job in pending/ when not in in-progress/', () => {
1370
+ fixture = createFixture({
1371
+ '.planning/jobs/pending/milestone-v6.0.md': WELL_FORMED_JOB,
1372
+ });
1373
+ const result = findJobFile(fixture.cwd, 'v6.0');
1374
+ assert.equal(result.found, true);
1375
+ assert.equal(result.directory, 'pending');
1376
+ assert.ok(result.path.includes('pending'));
1377
+ });
1378
+
1379
+ it('prefers in-progress/ over pending/ when file exists in both', () => {
1380
+ fixture = createFixture({
1381
+ '.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
1382
+ '.planning/jobs/pending/milestone-v6.0.md': WELL_FORMED_JOB,
1383
+ });
1384
+ const result = findJobFile(fixture.cwd, 'v6.0');
1385
+ assert.equal(result.found, true);
1386
+ assert.equal(result.directory, 'in-progress');
1387
+ });
1388
+
1389
+ it('returns found:false when file exists in neither directory', () => {
1390
+ fixture = createFixture({
1391
+ '.planning/jobs/pending/': null,
1392
+ '.planning/jobs/in-progress/': null,
1393
+ });
1394
+ const result = findJobFile(fixture.cwd, 'v6.0');
1395
+ assert.equal(result.found, false);
1396
+ });
1397
+
1398
+ it('checks completed/ as fallback for inspection', () => {
1399
+ fixture = createFixture({
1400
+ '.planning/jobs/completed/milestone-v6.0.md': ALL_COMPLETED_JOB,
1401
+ });
1402
+ const result = findJobFile(fixture.cwd, 'v6.0');
1403
+ assert.equal(result.found, true);
1404
+ assert.equal(result.directory, 'completed');
1405
+ assert.ok(result.path.includes('completed'));
1406
+ });
1407
+
1408
+ it('handles version without v prefix (e.g., "6.0" -> milestone-v6.0.md)', () => {
1409
+ fixture = createFixture({
1410
+ '.planning/jobs/pending/milestone-v6.0.md': WELL_FORMED_JOB,
1411
+ });
1412
+ const result = findJobFile(fixture.cwd, '6.0');
1413
+ assert.equal(result.found, true);
1414
+ assert.ok(result.path.includes('milestone-v6.0.md'));
1415
+ });
1416
+
1417
+ it('handles version with v prefix correctly', () => {
1418
+ fixture = createFixture({
1419
+ '.planning/jobs/pending/milestone-v6.0.md': WELL_FORMED_JOB,
1420
+ });
1421
+ const result = findJobFile(fixture.cwd, 'v6.0');
1422
+ assert.equal(result.found, true);
1423
+ assert.ok(result.path.includes('milestone-v6.0.md'));
1424
+ });
1425
+
1426
+ it('finds job in project subdirectory completed/', () => {
1427
+ fixture = createFixture({
1428
+ '.planning/projects/test-project/jobs/completed/milestone-v6.0.md': ALL_COMPLETED_JOB,
1429
+ });
1430
+ const result = findJobFile(fixture.cwd, 'v6.0');
1431
+ assert.equal(result.found, true);
1432
+ assert.equal(result.directory, 'completed');
1433
+ assert.ok(result.path.includes('projects'));
1434
+ assert.ok(result.path.includes('test-project'));
1435
+ assert.ok(result.path.includes('milestone-v6.0.md'));
1436
+ });
1437
+
1438
+ it('prefers top-level jobs/ over project subdirectory', () => {
1439
+ fixture = createFixture({
1440
+ '.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
1441
+ '.planning/projects/test-project/jobs/completed/milestone-v6.0.md': ALL_COMPLETED_JOB,
1442
+ });
1443
+ const result = findJobFile(fixture.cwd, 'v6.0');
1444
+ assert.equal(result.found, true);
1445
+ assert.equal(result.directory, 'in-progress');
1446
+ assert.ok(!result.path.includes('projects'), 'Should find top-level job first');
1447
+ });
1448
+
1449
+ it('finds job in project subdirectory in-progress/', () => {
1450
+ fixture = createFixture({
1451
+ '.planning/projects/my-app/jobs/in-progress/milestone-v7.0.md': WELL_FORMED_JOB,
1452
+ });
1453
+ const result = findJobFile(fixture.cwd, 'v7.0');
1454
+ assert.equal(result.found, true);
1455
+ assert.equal(result.directory, 'in-progress');
1456
+ assert.ok(result.path.includes('my-app'));
1457
+ });
1458
+
1459
+ it('returns found:false when not in top-level or project subdirectories', () => {
1460
+ fixture = createFixture({
1461
+ '.planning/jobs/pending/': null,
1462
+ '.planning/projects/test-project/jobs/pending/': null,
1463
+ });
1464
+ const result = findJobFile(fixture.cwd, 'v99.0');
1465
+ assert.equal(result.found, false);
1466
+ });
1467
+ });
1468
+
1469
+ // ─── updateJobHeader Tests ──────────────────────────────────────────────
1470
+
1471
+ describe('updateJobHeader', () => {
1472
+ let fixture;
1473
+
1474
+ afterEach(() => {
1475
+ if (fixture) fixture.cleanup();
1476
+ });
1477
+
1478
+ it('updates Status field from pending to in-progress', () => {
1479
+ const pendingJob = `# Milestone Job: v6.0
1480
+
1481
+ **Version:** v6.0
1482
+ **Created:** 2026-03-02T10:00:00Z
1483
+ **Status:** pending
1484
+ **Check:** true
1485
+
1486
+ ## Steps
1487
+
1488
+ - [ ] \`/dgs:plan-phase 41\`
1489
+ - [ ] \`/dgs:execute-phase 41\`
1490
+ `;
1491
+ fixture = createFixture({ 'job.md': pendingJob });
1492
+ const filePath = path.join(fixture.cwd, 'job.md');
1493
+
1494
+ updateJobHeader(filePath, 'Status', 'in-progress');
1495
+
1496
+ const content = fs.readFileSync(filePath, 'utf-8');
1497
+ assert.ok(content.includes('**Status:** in-progress'), 'Status should be updated');
1498
+ // Other header fields preserved
1499
+ assert.ok(content.includes('**Version:** v6.0'));
1500
+ assert.ok(content.includes('**Created:** 2026-03-02T10:00:00Z'));
1501
+ assert.ok(content.includes('**Check:** true'));
1502
+ });
1503
+
1504
+ it('updates Status field from in-progress to completed preserving all other headers', () => {
1505
+ fixture = createFixture({ 'job.md': WELL_FORMED_JOB });
1506
+ const filePath = path.join(fixture.cwd, 'job.md');
1507
+
1508
+ updateJobHeader(filePath, 'Status', 'completed');
1509
+
1510
+ const content = fs.readFileSync(filePath, 'utf-8');
1511
+ assert.ok(content.includes('**Status:** completed'), 'Status should be updated to completed');
1512
+ assert.ok(content.includes('**Version:** v6.0'), 'Version preserved');
1513
+ assert.ok(content.includes('**Created:** 2026-03-02T10:00:00Z'), 'Created preserved');
1514
+ assert.ok(content.includes('**Check:** true'), 'Check preserved');
1515
+ });
1516
+
1517
+ it('updates Status field from in-progress to failed', () => {
1518
+ fixture = createFixture({ 'job.md': WELL_FORMED_JOB });
1519
+ const filePath = path.join(fixture.cwd, 'job.md');
1520
+
1521
+ updateJobHeader(filePath, 'Status', 'failed');
1522
+
1523
+ const content = fs.readFileSync(filePath, 'utf-8');
1524
+ assert.ok(content.includes('**Status:** failed'), 'Status should be updated to failed');
1525
+ });
1526
+
1527
+ it('throws Error if the field is not found in the file', () => {
1528
+ fixture = createFixture({ 'job.md': WELL_FORMED_JOB });
1529
+ const filePath = path.join(fixture.cwd, 'job.md');
1530
+
1531
+ assert.throws(
1532
+ () => updateJobHeader(filePath, 'NonExistent', 'value'),
1533
+ (err) => err.message.includes('NonExistent') || err.message.includes('not found')
1534
+ );
1535
+ });
1536
+
1537
+ it('preserves exact step line content (round-trip identical steps)', () => {
1538
+ fixture = createFixture({ 'job.md': WELL_FORMED_JOB });
1539
+ const filePath = path.join(fixture.cwd, 'job.md');
1540
+
1541
+ // Parse steps before
1542
+ const beforeParsed = parseJobFile(filePath);
1543
+
1544
+ // Update a header field
1545
+ updateJobHeader(filePath, 'Status', 'completed');
1546
+
1547
+ // Parse steps after
1548
+ const afterParsed = parseJobFile(filePath);
1549
+
1550
+ // Steps should be identical
1551
+ assert.equal(afterParsed.steps.length, beforeParsed.steps.length);
1552
+ for (let i = 0; i < beforeParsed.steps.length; i++) {
1553
+ assert.equal(afterParsed.steps[i].command, beforeParsed.steps[i].command);
1554
+ assert.equal(afterParsed.steps[i].args, beforeParsed.steps[i].args);
1555
+ assert.equal(afterParsed.steps[i].status, beforeParsed.steps[i].status);
1556
+ assert.equal(afterParsed.steps[i].timestamp, beforeParsed.steps[i].timestamp);
1557
+ assert.equal(afterParsed.steps[i].error, beforeParsed.steps[i].error);
1558
+ }
1559
+ });
1560
+ });
1561
+
1562
+ // ─── insertJobSteps Tests ──────────────────────────────────────────────
1563
+
1564
+ describe('insertJobSteps', () => {
1565
+ let fixture;
1566
+
1567
+ afterEach(() => {
1568
+ if (fixture) fixture.cleanup();
1569
+ });
1570
+
1571
+ const SIX_STEP_JOB = `# Milestone Job: v6.0
1572
+
1573
+ **Version:** v6.0
1574
+ **Created:** 2026-03-02T10:00:00Z
1575
+ **Status:** in-progress
1576
+ **Check:** true
1577
+
1578
+ ## Steps
1579
+
1580
+ - [x] \`/dgs:plan-phase 50\` \u2014 completed 2026-03-02T11:00:00Z
1581
+ - [x] \`/dgs:execute-phase 50 --auto\` \u2014 completed 2026-03-02T12:00:00Z
1582
+ - [x] \`/dgs:verify-work 50\` \u2014 completed 2026-03-02T13:00:00Z
1583
+ - [x] \`/dgs:plan-phase 51\` \u2014 completed 2026-03-02T14:00:00Z
1584
+ - [x] \`/dgs:execute-phase 51 --auto\` \u2014 completed 2026-03-02T15:00:00Z
1585
+ - [ ] \`/dgs:verify-work 51\`
1586
+ `;
1587
+
1588
+ it('inserts steps after a specific completed step', () => {
1589
+ fixture = createFixture({ 'job.md': SIX_STEP_JOB });
1590
+ const filePath = path.join(fixture.cwd, 'job.md');
1591
+
1592
+ const newSteps = [
1593
+ { command: 'plan-phase', args: '51.1 --non-interactive' },
1594
+ { command: 'execute-phase', args: '51.1 --non-interactive' },
1595
+ { command: 'verify-work', args: '51.1' },
1596
+ { command: 'plan-phase', args: '51.2 --non-interactive' },
1597
+ ];
1598
+
1599
+ const result = insertJobSteps(filePath, 4, newSteps);
1600
+
1601
+ assert.equal(result.inserted, true);
1602
+ assert.equal(result.count, 4);
1603
+ assert.equal(result.startIndex, 5);
1604
+
1605
+ // Verify the file content
1606
+ const content = fs.readFileSync(filePath, 'utf-8');
1607
+ const stepLines = content.split('\n').filter(l => l.trim().startsWith('- ['));
1608
+ assert.equal(stepLines.length, 10, 'Should have 6 original + 4 new = 10 steps');
1609
+
1610
+ // The new steps should be pending with correct commands
1611
+ assert.ok(stepLines[5].includes('plan-phase 51.1 --non-interactive'), 'First inserted step');
1612
+ assert.ok(stepLines[6].includes('execute-phase 51.1 --non-interactive'), 'Second inserted step');
1613
+ assert.ok(stepLines[7].includes('verify-work 51.1'), 'Third inserted step');
1614
+ assert.ok(stepLines[8].includes('plan-phase 51.2 --non-interactive'), 'Fourth inserted step');
1615
+ });
1616
+
1617
+ it('each new step is formatted as pending checkbox with /dgs: prefix', () => {
1618
+ fixture = createFixture({ 'job.md': SIX_STEP_JOB });
1619
+ const filePath = path.join(fixture.cwd, 'job.md');
1620
+
1621
+ const newSteps = [
1622
+ { command: 'plan-phase', args: '52 --non-interactive' },
1623
+ ];
1624
+
1625
+ insertJobSteps(filePath, 0, newSteps);
1626
+
1627
+ const content = fs.readFileSync(filePath, 'utf-8');
1628
+ const stepLines = content.split('\n').filter(l => l.trim().startsWith('- ['));
1629
+ // Inserted step at index 1 (after step 0)
1630
+ assert.ok(stepLines[1].includes('- [ ] `/dgs:plan-phase 52 --non-interactive`'), 'Step should be formatted correctly');
1631
+ });
1632
+
1633
+ it('inserting after the last step appends at the end of steps section', () => {
1634
+ fixture = createFixture({ 'job.md': SIX_STEP_JOB });
1635
+ const filePath = path.join(fixture.cwd, 'job.md');
1636
+
1637
+ const newSteps = [
1638
+ { command: 'audit-milestone', args: 'v6.0' },
1639
+ ];
1640
+
1641
+ const result = insertJobSteps(filePath, 5, newSteps);
1642
+
1643
+ assert.equal(result.inserted, true);
1644
+ assert.equal(result.count, 1);
1645
+ assert.equal(result.startIndex, 6);
1646
+
1647
+ const content = fs.readFileSync(filePath, 'utf-8');
1648
+ const stepLines = content.split('\n').filter(l => l.trim().startsWith('- ['));
1649
+ assert.equal(stepLines.length, 7, 'Should have 6 + 1 = 7 steps');
1650
+ assert.ok(stepLines[6].includes('audit-milestone v6.0'), 'Last step should be the new one');
1651
+ });
1652
+
1653
+ it('inserting with afterStepIndex -1 inserts before all existing steps', () => {
1654
+ fixture = createFixture({ 'job.md': SIX_STEP_JOB });
1655
+ const filePath = path.join(fixture.cwd, 'job.md');
1656
+
1657
+ const newSteps = [
1658
+ { command: 'plan-phase', args: '49 --non-interactive' },
1659
+ ];
1660
+
1661
+ const result = insertJobSteps(filePath, -1, newSteps);
1662
+
1663
+ assert.equal(result.inserted, true);
1664
+ assert.equal(result.count, 1);
1665
+ assert.equal(result.startIndex, 0);
1666
+
1667
+ const content = fs.readFileSync(filePath, 'utf-8');
1668
+ const stepLines = content.split('\n').filter(l => l.trim().startsWith('- ['));
1669
+ assert.equal(stepLines.length, 7, 'Should have 6 + 1 = 7 steps');
1670
+ assert.ok(stepLines[0].includes('plan-phase 49 --non-interactive'), 'First step should be the new one');
1671
+ // Original first step should now be second
1672
+ assert.ok(stepLines[1].includes('plan-phase 50'), 'Original first step should be second');
1673
+ });
1674
+
1675
+ it('preserves all existing steps and header content exactly', () => {
1676
+ fixture = createFixture({ 'job.md': SIX_STEP_JOB });
1677
+ const filePath = path.join(fixture.cwd, 'job.md');
1678
+
1679
+ // Parse before
1680
+ const beforeParsed = parseJobFile(filePath);
1681
+
1682
+ const newSteps = [
1683
+ { command: 'plan-phase', args: '52 --non-interactive' },
1684
+ ];
1685
+
1686
+ insertJobSteps(filePath, 2, newSteps);
1687
+
1688
+ // Parse after
1689
+ const afterParsed = parseJobFile(filePath);
1690
+
1691
+ // Header should be preserved
1692
+ assert.equal(afterParsed.version, beforeParsed.version);
1693
+ assert.equal(afterParsed.status, beforeParsed.status);
1694
+ assert.equal(afterParsed.check, beforeParsed.check);
1695
+
1696
+ // Original steps should still be there (just shifted)
1697
+ assert.equal(afterParsed.stepCount, beforeParsed.stepCount + 1);
1698
+
1699
+ // Steps 0-2 should be identical to before
1700
+ for (let i = 0; i <= 2; i++) {
1701
+ assert.equal(afterParsed.steps[i].command, beforeParsed.steps[i].command);
1702
+ assert.equal(afterParsed.steps[i].args, beforeParsed.steps[i].args);
1703
+ assert.equal(afterParsed.steps[i].status, beforeParsed.steps[i].status);
1704
+ }
1705
+
1706
+ // Steps 4-6 after insert should correspond to original 3-5
1707
+ for (let i = 3; i < beforeParsed.steps.length; i++) {
1708
+ assert.equal(afterParsed.steps[i + 1].command, beforeParsed.steps[i].command);
1709
+ assert.equal(afterParsed.steps[i + 1].args, beforeParsed.steps[i].args);
1710
+ assert.equal(afterParsed.steps[i + 1].status, beforeParsed.steps[i].status);
1711
+ }
1712
+ });
1713
+
1714
+ it('returns { inserted: true, count: N, startIndex: M }', () => {
1715
+ fixture = createFixture({ 'job.md': SIX_STEP_JOB });
1716
+ const filePath = path.join(fixture.cwd, 'job.md');
1717
+
1718
+ const newSteps = [
1719
+ { command: 'plan-phase', args: '52 --non-interactive' },
1720
+ { command: 'execute-phase', args: '52 --non-interactive' },
1721
+ ];
1722
+
1723
+ const result = insertJobSteps(filePath, 3, newSteps);
1724
+
1725
+ assert.equal(result.inserted, true);
1726
+ assert.equal(result.count, 2);
1727
+ assert.equal(result.startIndex, 4);
1728
+ });
1729
+ });
1730
+
1731
+ // ─── parseJobFile GapFixCycle Tests ───────────────────────────────────
1732
+
1733
+ describe('parseJobFile GapFixCycle header', () => {
1734
+ let fixture;
1735
+
1736
+ afterEach(() => {
1737
+ if (fixture) fixture.cleanup();
1738
+ });
1739
+
1740
+ it('returns gapFixCycle: 0 when no GapFixCycle header field exists', () => {
1741
+ fixture = createFixture({ 'job.md': WELL_FORMED_JOB });
1742
+ const filePath = path.join(fixture.cwd, 'job.md');
1743
+
1744
+ const result = parseJobFile(filePath);
1745
+ assert.equal(result.gapFixCycle, 0, 'Should default to 0 when GapFixCycle header absent');
1746
+ });
1747
+
1748
+ it('returns gapFixCycle: 2 when **GapFixCycle:** 2 header line exists', () => {
1749
+ const jobWithGapFix = `# Milestone Job: v6.0
1750
+
1751
+ **Version:** v6.0
1752
+ **Created:** 2026-03-02T10:00:00Z
1753
+ **Status:** in-progress
1754
+ **Check:** true
1755
+ **GapFixCycle:** 2
1756
+
1757
+ ## Steps
1758
+
1759
+ - [x] \`/dgs:plan-phase 41\` \u2014 completed 2026-03-02T14:30:00Z
1760
+ - [ ] \`/dgs:execute-phase 41\`
1761
+ `;
1762
+ fixture = createFixture({ 'job.md': jobWithGapFix });
1763
+ const filePath = path.join(fixture.cwd, 'job.md');
1764
+
1765
+ const result = parseJobFile(filePath);
1766
+ assert.equal(result.gapFixCycle, 2, 'Should parse GapFixCycle as 2');
1767
+ });
1768
+ });
1769
+
1770
+ // ─── buildGapFixSteps Tests ───────────────────────────────────────────
1771
+
1772
+ describe('buildGapFixSteps', () => {
1773
+ it('generates plan-phase + execute-phase per phase, ending with audit-milestone', () => {
1774
+ const newPhases = [
1775
+ { number: '53.1', name: 'Fix auth gaps' },
1776
+ { number: '53.2', name: 'Fix data gaps' },
1777
+ ];
1778
+
1779
+ const steps = buildGapFixSteps(newPhases, 'v6.0');
1780
+
1781
+ assert.equal(steps.length, 5, 'Should have 5 steps (2 phases x 2 + 1 audit)');
1782
+
1783
+ assert.equal(steps[0].command, 'plan-phase');
1784
+ assert.equal(steps[0].args, '53.1 --non-interactive');
1785
+
1786
+ assert.equal(steps[1].command, 'execute-phase');
1787
+ assert.equal(steps[1].args, '53.1 --non-interactive');
1788
+
1789
+ assert.equal(steps[2].command, 'plan-phase');
1790
+ assert.equal(steps[2].args, '53.2 --non-interactive');
1791
+
1792
+ assert.equal(steps[3].command, 'execute-phase');
1793
+ assert.equal(steps[3].args, '53.2 --non-interactive');
1794
+
1795
+ assert.equal(steps[4].command, 'audit-milestone');
1796
+ assert.equal(steps[4].args, 'v6.0');
1797
+ });
1798
+
1799
+ it('does not include discuss-phase or verify-work steps', () => {
1800
+ const newPhases = [
1801
+ { number: '53.1', name: 'Fix auth gaps' },
1802
+ ];
1803
+
1804
+ const steps = buildGapFixSteps(newPhases, 'v6.0');
1805
+
1806
+ const commands = steps.map(s => s.command);
1807
+ assert.ok(!commands.includes('discuss-phase'), 'Should not include discuss-phase');
1808
+ assert.ok(!commands.includes('verify-work'), 'Should not include verify-work');
1809
+ });
1810
+ });
1811
+
1812
+ // ─── insertGapFixSection Tests ────────────────────────────────────────
1813
+
1814
+ describe('insertGapFixSection', () => {
1815
+ let fixture;
1816
+
1817
+ afterEach(() => {
1818
+ if (fixture) fixture.cleanup();
1819
+ });
1820
+
1821
+ const AUDIT_JOB = `# Milestone Job: v6.0
1822
+
1823
+ **Version:** v6.0
1824
+ **Created:** 2026-03-02T10:00:00Z
1825
+ **Status:** in-progress
1826
+ **Check:** true
1827
+
1828
+ ## Steps
1829
+
1830
+ - [x] \`/dgs:plan-phase 50\` \u2014 completed 2026-03-02T11:00:00Z
1831
+ - [x] \`/dgs:execute-phase 50 --auto\` \u2014 completed 2026-03-02T12:00:00Z
1832
+ - [x] \`/dgs:verify-work 50\` \u2014 completed 2026-03-02T13:00:00Z
1833
+ - [x] \`/dgs:audit-milestone v6.0\` \u2014 completed 2026-03-02T14:00:00Z
1834
+ - [ ] \`/dgs:complete-milestone v6.0\`
1835
+ `;
1836
+
1837
+ it('inserts section marker and gap-fix steps after the triggering audit step', () => {
1838
+ fixture = createFixture({ 'job.md': AUDIT_JOB });
1839
+ const filePath = path.join(fixture.cwd, 'job.md');
1840
+
1841
+ const newPhases = [
1842
+ { number: '50.1', name: 'Fix auth' },
1843
+ { number: '50.2', name: 'Fix data' },
1844
+ ];
1845
+
1846
+ const result = insertGapFixSection(filePath, 3, 1, newPhases, 'v6.0');
1847
+
1848
+ assert.equal(result.inserted, true);
1849
+ assert.equal(result.count, 5, 'Should have 5 steps (2x plan+execute + 1 audit)');
1850
+ assert.equal(result.cycleNumber, 1);
1851
+ assert.ok(result.sectionMarker.includes('Gap-Fix Cycle 1'), 'Marker should include cycle number');
1852
+ assert.ok(result.sectionMarker.includes('2 gaps'), 'Marker should include gap count');
1853
+
1854
+ // Check file content
1855
+ const content = fs.readFileSync(filePath, 'utf-8');
1856
+ assert.ok(content.includes('--- Gap-Fix Cycle 1'), 'File should contain section marker');
1857
+ assert.ok(content.includes('Fix auth'), 'Marker should include phase name');
1858
+ assert.ok(content.includes('Fix data'), 'Marker should include phase name');
1859
+
1860
+ // Section marker should appear between the audit step and the gap-fix steps
1861
+ const lines = content.split('\n');
1862
+ const auditLine = lines.findIndex(l => l.includes('audit-milestone v6.0') && l.includes('[x]'));
1863
+ const markerLine = lines.findIndex(l => l.includes('--- Gap-Fix Cycle 1'));
1864
+ assert.ok(markerLine > auditLine, 'Section marker should be after the audit step');
1865
+ });
1866
+
1867
+ it('writes the GapFixCycle header field', () => {
1868
+ fixture = createFixture({ 'job.md': AUDIT_JOB });
1869
+ const filePath = path.join(fixture.cwd, 'job.md');
1870
+
1871
+ const newPhases = [
1872
+ { number: '50.1', name: 'Fix auth' },
1873
+ ];
1874
+
1875
+ insertGapFixSection(filePath, 3, 1, newPhases, 'v6.0');
1876
+
1877
+ const content = fs.readFileSync(filePath, 'utf-8');
1878
+ assert.ok(content.includes('**GapFixCycle:** 1'), 'Should add GapFixCycle header');
1879
+ });
1880
+
1881
+ it('places section marker + steps between audit step and subsequent steps', () => {
1882
+ fixture = createFixture({ 'job.md': AUDIT_JOB });
1883
+ const filePath = path.join(fixture.cwd, 'job.md');
1884
+
1885
+ const newPhases = [
1886
+ { number: '50.1', name: 'Fix auth' },
1887
+ ];
1888
+
1889
+ insertGapFixSection(filePath, 3, 1, newPhases, 'v6.0');
1890
+
1891
+ const content = fs.readFileSync(filePath, 'utf-8');
1892
+ const lines = content.split('\n');
1893
+ const stepLines = lines.filter(l => l.trim().startsWith('- ['));
1894
+
1895
+ // Original: 5 steps (plan, exec, verify, audit, complete)
1896
+ // Inserted: 3 steps (plan, exec, audit-milestone re-audit)
1897
+ // Total: 8 step lines
1898
+ assert.equal(stepLines.length, 8, 'Should have 5 original + 3 inserted = 8 steps');
1899
+
1900
+ // The complete-milestone step should still be the last step
1901
+ assert.ok(stepLines[stepLines.length - 1].includes('complete-milestone'), 'complete-milestone should remain last');
1902
+ });
1903
+
1904
+ it('updates existing GapFixCycle header using updateJobHeader when field already present', () => {
1905
+ const jobWithExistingCycle = `# Milestone Job: v6.0
1906
+
1907
+ **Version:** v6.0
1908
+ **Created:** 2026-03-02T10:00:00Z
1909
+ **Status:** in-progress
1910
+ **Check:** true
1911
+ **GapFixCycle:** 1
1912
+
1913
+ ## Steps
1914
+
1915
+ - [x] \`/dgs:plan-phase 50\` \u2014 completed 2026-03-02T11:00:00Z
1916
+ - [x] \`/dgs:audit-milestone v6.0\` \u2014 completed 2026-03-02T14:00:00Z
1917
+ - [ ] \`/dgs:complete-milestone v6.0\`
1918
+ `;
1919
+ fixture = createFixture({ 'job.md': jobWithExistingCycle });
1920
+ const filePath = path.join(fixture.cwd, 'job.md');
1921
+
1922
+ const newPhases = [
1923
+ { number: '50.2', name: 'Fix more gaps' },
1924
+ ];
1925
+
1926
+ insertGapFixSection(filePath, 1, 2, newPhases, 'v6.0');
1927
+
1928
+ const content = fs.readFileSync(filePath, 'utf-8');
1929
+ assert.ok(content.includes('**GapFixCycle:** 2'), 'GapFixCycle should be updated to 2');
1930
+ assert.ok(!content.includes('**GapFixCycle:** 1'), 'Old GapFixCycle value should be replaced');
1931
+ });
1932
+ });
1933
+
1934
+ // ─── listJobs Tests ───────────────────────────────────────────────────
1935
+
1936
+ describe('listJobs', () => {
1937
+ let fixture;
1938
+
1939
+ afterEach(() => {
1940
+ if (fixture) fixture.cleanup();
1941
+ });
1942
+
1943
+ it('returns empty groups when no job files exist in any directory', () => {
1944
+ fixture = createFixture({
1945
+ '.planning/jobs/pending/': null,
1946
+ '.planning/jobs/in-progress/': null,
1947
+ '.planning/jobs/completed/': null,
1948
+ });
1949
+ const result = listJobs(fixture.cwd);
1950
+
1951
+ assert.deepEqual(result.pending, []);
1952
+ assert.deepEqual(result.in_progress, []);
1953
+ assert.deepEqual(result.completed, []);
1954
+ });
1955
+
1956
+ it('returns jobs grouped by status with correct fields', () => {
1957
+ fixture = createFixture({
1958
+ '.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
1959
+ '.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
1960
+ });
1961
+ const result = listJobs(fixture.cwd);
1962
+
1963
+ assert.equal(result.in_progress.length, 1);
1964
+ assert.equal(result.completed.length, 1);
1965
+ assert.equal(result.pending.length, 0);
1966
+
1967
+ const inProg = result.in_progress[0];
1968
+ assert.equal(inProg.version, 'v6.0');
1969
+ assert.equal(inProg.status, 'in-progress');
1970
+ assert.equal(inProg.check, true);
1971
+ assert.equal(inProg.progress, '1/4');
1972
+ assert.ok(inProg.file.includes('milestone-v6.0.md'));
1973
+
1974
+ const comp = result.completed[0];
1975
+ assert.equal(comp.version, 'v5.0');
1976
+ assert.equal(comp.status, 'completed');
1977
+ assert.equal(comp.check, false);
1978
+ assert.equal(comp.progress, '2/2');
1979
+ });
1980
+
1981
+ it('shows check flag false when --no-check was used', () => {
1982
+ fixture = createFixture({
1983
+ '.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
1984
+ });
1985
+ const result = listJobs(fixture.cwd);
1986
+
1987
+ const comp = result.completed[0];
1988
+ assert.equal(comp.check, false);
1989
+ });
1990
+
1991
+ it('handles missing directories gracefully with empty groups', () => {
1992
+ fixture = createFixture({
1993
+ '.planning/': null,
1994
+ });
1995
+ const result = listJobs(fixture.cwd);
1996
+
1997
+ assert.deepEqual(result.pending, []);
1998
+ assert.deepEqual(result.in_progress, []);
1999
+ assert.deepEqual(result.completed, []);
2000
+ });
2001
+
2002
+ it('each entry includes progress as fraction string like "4/12"', () => {
2003
+ fixture = createFixture({
2004
+ '.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
2005
+ });
2006
+ const result = listJobs(fixture.cwd);
2007
+ const entry = result.in_progress[0];
2008
+ assert.match(entry.progress, /^\d+\/\d+$/, 'Progress should be fraction string');
2009
+ });
2010
+ });
2011
+
2012
+ // ─── cancelJob Tests ──────────────────────────────────────────────────
2013
+
2014
+ describe('cancelJob', () => {
2015
+ let fixture;
2016
+
2017
+ afterEach(() => {
2018
+ if (fixture) fixture.cleanup();
2019
+ });
2020
+
2021
+ it('cancels in-progress job: resets [>] steps to [ ], updates Status, moves to pending/', () => {
2022
+ fixture = createFixture({
2023
+ '.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
2024
+ '.planning/jobs/pending/': null,
2025
+ });
2026
+ const result = cancelJob(fixture.cwd, 'v6.0');
2027
+
2028
+ assert.equal(result.cancelled, true);
2029
+ assert.equal(result.version, 'v6.0');
2030
+ assert.ok(result.path);
2031
+ assert.equal(result.steps_reset, 1); // one [>] step
2032
+
2033
+ // Verify file moved to pending
2034
+ assert.ok(fs.existsSync(path.join(fixture.cwd, '.planning', 'jobs', 'pending', 'milestone-v6.0.md')));
2035
+ assert.ok(!fs.existsSync(path.join(fixture.cwd, '.planning', 'jobs', 'in-progress', 'milestone-v6.0.md')));
2036
+
2037
+ // Verify content: [>] reset to [ ], [x] preserved
2038
+ const content = fs.readFileSync(path.join(fixture.cwd, '.planning', 'jobs', 'pending', 'milestone-v6.0.md'), 'utf-8');
2039
+ assert.ok(content.includes('**Status:** pending'), 'Status should be pending');
2040
+ assert.ok(!content.includes('[>]'), 'No in-progress markers should remain');
2041
+ // completed steps preserved
2042
+ assert.ok(content.includes('[x]'), 'Completed steps should be preserved');
2043
+ });
2044
+
2045
+ it('returns steps_reset count of reset steps', () => {
2046
+ const twoInProgress = `# Milestone Job: v6.0
2047
+
2048
+ **Version:** v6.0
2049
+ **Created:** 2026-03-02T10:00:00Z
2050
+ **Status:** in-progress
2051
+ **Check:** true
2052
+
2053
+ ## Steps
2054
+
2055
+ - [x] \`/dgs:plan-phase 41\` \u2014 completed 2026-03-02T14:30:00Z
2056
+ - [>] \`/dgs:execute-phase 41\` \u2014 started 2026-03-02T15:00:00Z
2057
+ - [>] \`/dgs:plan-phase 42\` \u2014 started 2026-03-02T15:30:00Z
2058
+ - [ ] \`/dgs:execute-phase 42\`
2059
+ `;
2060
+ fixture = createFixture({
2061
+ '.planning/jobs/in-progress/milestone-v6.0.md': twoInProgress,
2062
+ '.planning/jobs/pending/': null,
2063
+ });
2064
+ const result = cancelJob(fixture.cwd, 'v6.0');
2065
+
2066
+ assert.equal(result.cancelled, true);
2067
+ assert.equal(result.steps_reset, 2);
2068
+ });
2069
+
2070
+ it('keeps completed [x] steps marked done', () => {
2071
+ fixture = createFixture({
2072
+ '.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
2073
+ '.planning/jobs/pending/': null,
2074
+ });
2075
+ cancelJob(fixture.cwd, 'v6.0');
2076
+
2077
+ const content = fs.readFileSync(path.join(fixture.cwd, '.planning', 'jobs', 'pending', 'milestone-v6.0.md'), 'utf-8');
2078
+ const stepLines = content.split('\n').filter(l => l.trim().startsWith('- ['));
2079
+ const completedSteps = stepLines.filter(l => l.includes('[x]'));
2080
+ assert.ok(completedSteps.length > 0, 'Completed steps should be preserved');
2081
+ });
2082
+
2083
+ it('returns not_found when no job exists for version', () => {
2084
+ fixture = createFixture({
2085
+ '.planning/jobs/pending/': null,
2086
+ '.planning/jobs/in-progress/': null,
2087
+ '.planning/jobs/completed/': null,
2088
+ });
2089
+ const result = cancelJob(fixture.cwd, 'v99.0');
2090
+
2091
+ assert.equal(result.cancelled, false);
2092
+ assert.equal(result.reason, 'not_found');
2093
+ });
2094
+
2095
+ it('returns not_in_progress when job is in pending/', () => {
2096
+ fixture = createFixture({
2097
+ '.planning/jobs/pending/milestone-v6.0.md': WELL_FORMED_JOB,
2098
+ });
2099
+ const result = cancelJob(fixture.cwd, 'v6.0');
2100
+
2101
+ assert.equal(result.cancelled, false);
2102
+ assert.equal(result.reason, 'not_in_progress');
2103
+ });
2104
+
2105
+ it('returns not_in_progress when job is in completed/', () => {
2106
+ fixture = createFixture({
2107
+ '.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
2108
+ });
2109
+ const result = cancelJob(fixture.cwd, 'v5.0');
2110
+
2111
+ assert.equal(result.cancelled, false);
2112
+ assert.equal(result.reason, 'not_in_progress');
2113
+ });
2114
+ });
2115
+
2116
+ // ─── healthCheck Tests ────────────────────────────────────────────────
2117
+
2118
+ describe('healthCheck', () => {
2119
+ let fixture;
2120
+
2121
+ afterEach(() => {
2122
+ if (fixture) fixture.cleanup();
2123
+ });
2124
+
2125
+ it('returns healthy: true with valid directory structure', () => {
2126
+ fixture = createFixture({
2127
+ '.planning/jobs/pending/': null,
2128
+ '.planning/jobs/in-progress/': null,
2129
+ '.planning/jobs/completed/': null,
2130
+ });
2131
+ const result = healthCheck(fixture.cwd);
2132
+
2133
+ assert.equal(result.healthy, true);
2134
+ assert.ok(Array.isArray(result.directories));
2135
+ assert.equal(result.job_count, 0);
2136
+ });
2137
+
2138
+ it('returns healthy: false listing issues for missing directories, then auto-creates them', () => {
2139
+ fixture = createFixture({
2140
+ '.planning/': null,
2141
+ });
2142
+ const result = healthCheck(fixture.cwd);
2143
+
2144
+ // Should report missing directories but auto-create them
2145
+ assert.ok(Array.isArray(result.directories));
2146
+ // After auto-create, directories should exist
2147
+ assert.ok(fs.existsSync(path.join(fixture.cwd, '.planning', 'jobs', 'pending')));
2148
+ assert.ok(fs.existsSync(path.join(fixture.cwd, '.planning', 'jobs', 'in-progress')));
2149
+ assert.ok(fs.existsSync(path.join(fixture.cwd, '.planning', 'jobs', 'completed')));
2150
+ });
2151
+
2152
+ it('validates each job file parses successfully; reports parse failures as issues', () => {
2153
+ const malformedJob = `# Bad Job
2154
+
2155
+ No version or anything
2156
+
2157
+ - [ ] random line
2158
+ `;
2159
+ fixture = createFixture({
2160
+ '.planning/jobs/pending/milestone-v99.0.md': malformedJob,
2161
+ '.planning/jobs/in-progress/': null,
2162
+ '.planning/jobs/completed/': null,
2163
+ });
2164
+ const result = healthCheck(fixture.cwd);
2165
+
2166
+ assert.equal(result.healthy, false);
2167
+ assert.ok(result.issues.length > 0, 'Should report parse failure');
2168
+ assert.ok(result.issues.some(i => i.includes('milestone-v99.0.md') || i.includes('parse')), 'Issue should reference the file');
2169
+ });
2170
+
2171
+ it('counts total job files across all directories', () => {
2172
+ fixture = createFixture({
2173
+ '.planning/jobs/pending/milestone-v7.0.md': EMPTY_STEPS_JOB,
2174
+ '.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
2175
+ '.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
2176
+ });
2177
+ const result = healthCheck(fixture.cwd);
2178
+
2179
+ assert.equal(result.job_count, 3);
2180
+ });
2181
+ });
2182
+
2183
+ // ─── dryRunPreview Tests ──────────────────────────────────────────────
2184
+
2185
+ describe('dryRunPreview', () => {
2186
+ let fixture;
2187
+
2188
+ afterEach(() => {
2189
+ if (fixture) fixture.cleanup();
2190
+ });
2191
+
2192
+ it('returns step list with status annotations', () => {
2193
+ fixture = createFixture({
2194
+ '.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
2195
+ '.planning/ROADMAP.md': '# Roadmap',
2196
+ });
2197
+ const result = dryRunPreview(fixture.cwd, 'v6.0');
2198
+
2199
+ assert.equal(result.found, true);
2200
+ assert.equal(result.version, 'v6.0');
2201
+ assert.ok(Array.isArray(result.steps));
2202
+ assert.equal(result.steps.length, 4);
2203
+ });
2204
+
2205
+ it('each step includes index, command, args, status, display', () => {
2206
+ fixture = createFixture({
2207
+ '.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
2208
+ '.planning/ROADMAP.md': '# Roadmap',
2209
+ });
2210
+ const result = dryRunPreview(fixture.cwd, 'v6.0');
2211
+
2212
+ for (const step of result.steps) {
2213
+ assert.ok(typeof step.index === 'number', 'step should have index');
2214
+ assert.ok(typeof step.command === 'string', 'step should have command');
2215
+ assert.ok(typeof step.args === 'string', 'step should have args');
2216
+ assert.ok(typeof step.status === 'string', 'step should have status');
2217
+ assert.ok(typeof step.display === 'string', 'step should have display');
2218
+ }
2219
+ });
2220
+
2221
+ it('shows resume_from as first non-completed step', () => {
2222
+ fixture = createFixture({
2223
+ '.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
2224
+ '.planning/ROADMAP.md': '# Roadmap',
2225
+ });
2226
+ const result = dryRunPreview(fixture.cwd, 'v6.0');
2227
+
2228
+ // Step 0 completed, step 1 in-progress, step 2 pending
2229
+ // Resume from first non-completed = step 1
2230
+ assert.equal(result.resume_from, 1);
2231
+ });
2232
+
2233
+ it('returns found: false when job does not exist', () => {
2234
+ fixture = createFixture({
2235
+ '.planning/jobs/pending/': null,
2236
+ '.planning/jobs/in-progress/': null,
2237
+ '.planning/jobs/completed/': null,
2238
+ });
2239
+ const result = dryRunPreview(fixture.cwd, 'v99.0');
2240
+
2241
+ assert.equal(result.found, false);
2242
+ });
2243
+
2244
+ it('validates preconditions: warns if .planning/ missing or ROADMAP.md missing', () => {
2245
+ fixture = createFixture({
2246
+ '.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
2247
+ });
2248
+ // No ROADMAP.md
2249
+ const result = dryRunPreview(fixture.cwd, 'v6.0');
2250
+
2251
+ assert.ok(result.found, true);
2252
+ assert.ok(Array.isArray(result.warnings));
2253
+ assert.ok(result.warnings.some(w => w.includes('ROADMAP.md')), 'Should warn about missing ROADMAP.md');
2254
+ });
2255
+ });
2256
+
2257
+ // ─── generateJobSummary Tests ─────────────────────────────────────────
2258
+
2259
+ describe('generateJobSummary', () => {
2260
+ let fixture;
2261
+
2262
+ afterEach(() => {
2263
+ if (fixture) fixture.cleanup();
2264
+ });
2265
+
2266
+ it('produces markdown string with correct header', () => {
2267
+ fixture = createFixture({
2268
+ '.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
2269
+ });
2270
+ const result = generateJobSummary(fixture.cwd, 'v5.0');
2271
+
2272
+ assert.equal(result.found, true);
2273
+ assert.equal(result.version, 'v5.0');
2274
+ assert.ok(typeof result.content === 'string');
2275
+ assert.ok(result.content.includes('# Job Summary: milestone-v5.0'));
2276
+ });
2277
+
2278
+ it('includes per-step timing table', () => {
2279
+ fixture = createFixture({
2280
+ '.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
2281
+ });
2282
+ const result = generateJobSummary(fixture.cwd, 'v5.0');
2283
+
2284
+ assert.ok(result.content.includes('| #'));
2285
+ assert.ok(result.content.includes('Command'));
2286
+ assert.ok(result.content.includes('Status'));
2287
+ });
2288
+
2289
+ it('includes error section for failed steps', () => {
2290
+ const failedJob = `# Milestone Job: v6.0
2291
+
2292
+ **Version:** v6.0
2293
+ **Created:** 2026-03-02T10:00:00Z
2294
+ **Status:** failed
2295
+ **Check:** true
2296
+
2297
+ ## Steps
2298
+
2299
+ - [x] \`/dgs:plan-phase 41\` \u2014 completed 2026-03-02T14:30:00Z
2300
+ - [!] \`/dgs:execute-phase 41\` \u2014 failed 2026-03-02T15:00:00Z: Tests failed in auth module
2301
+ - [ ] \`/dgs:verify-work 41\`
2302
+ `;
2303
+ fixture = createFixture({
2304
+ '.planning/jobs/in-progress/milestone-v6.0.md': failedJob,
2305
+ });
2306
+ const result = generateJobSummary(fixture.cwd, 'v6.0');
2307
+
2308
+ assert.ok(result.content.includes('Error'), 'Should include error section');
2309
+ assert.ok(result.content.includes('Tests failed in auth module'), 'Should include specific error message');
2310
+ });
2311
+
2312
+ it('includes auto-resolve audit section listing auto-resolved steps', () => {
2313
+ const autoJob = `# Milestone Job: v6.0
2314
+
2315
+ **Version:** v6.0
2316
+ **Created:** 2026-03-02T10:00:00Z
2317
+ **Status:** completed
2318
+ **Check:** true
2319
+
2320
+ ## Steps
2321
+
2322
+ - [x] \`/dgs:plan-phase 41 --auto\` \u2014 completed 2026-03-02T14:30:00Z
2323
+ - [x] \`/dgs:execute-phase 41 --auto\` \u2014 completed 2026-03-02T15:00:00Z
2324
+ - [x] \`/dgs:verify-work 41\` \u2014 completed 2026-03-02T15:30:00Z
2325
+ `;
2326
+ fixture = createFixture({
2327
+ '.planning/jobs/completed/milestone-v6.0.md': autoJob,
2328
+ });
2329
+ const result = generateJobSummary(fixture.cwd, 'v6.0');
2330
+
2331
+ assert.ok(result.content.includes('Auto'), 'Should include auto-resolve audit section');
2332
+ });
2333
+
2334
+ it('handles completed, failed, and cancelled jobs', () => {
2335
+ fixture = createFixture({
2336
+ '.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
2337
+ });
2338
+ const result = generateJobSummary(fixture.cwd, 'v5.0');
2339
+ assert.equal(result.found, true);
2340
+ assert.ok(result.content.includes('completed'));
2341
+ });
2342
+
2343
+ it('returns found: false when job does not exist', () => {
2344
+ fixture = createFixture({
2345
+ '.planning/jobs/pending/': null,
2346
+ '.planning/jobs/in-progress/': null,
2347
+ '.planning/jobs/completed/': null,
2348
+ });
2349
+ const result = generateJobSummary(fixture.cwd, 'v99.0');
2350
+
2351
+ assert.equal(result.found, false);
2352
+ });
2353
+
2354
+ it('includes human verifications section when UAT has human_needed entries', () => {
2355
+ fixture = createFixture({
2356
+ '.planning/jobs/completed/milestone-v6.0.md': COMPLETED_JOB_V6,
2357
+ '.planning/ROADMAP.md': ROADMAP_WITH_PHASE_50,
2358
+ '.planning/phases/50-test-phase/50-UAT.md': UAT_WITH_HUMAN_NEEDED,
2359
+ });
2360
+ const result = generateJobSummary(fixture.cwd, 'v6.0');
2361
+
2362
+ assert.equal(result.found, true);
2363
+ assert.ok(result.content.includes('Outstanding Human Verifications'), 'Should include human verifications section');
2364
+ assert.ok(result.content.includes('Dashboard renders correctly'), 'Should include dashboard test');
2365
+ assert.ok(result.content.includes('Login form accessible'), 'Should include login form test');
2366
+ assert.equal(result.human_verification_count, 2);
2367
+ });
2368
+
2369
+ it('omits human verifications section when no human_needed entries', () => {
2370
+ fixture = createFixture({
2371
+ '.planning/jobs/completed/milestone-v6.0.md': COMPLETED_JOB_V6,
2372
+ '.planning/ROADMAP.md': ROADMAP_WITH_PHASE_50,
2373
+ '.planning/phases/50-test-phase/50-UAT.md': UAT_ALL_PASSED,
2374
+ });
2375
+ const result = generateJobSummary(fixture.cwd, 'v6.0');
2376
+
2377
+ assert.equal(result.found, true);
2378
+ assert.ok(!result.content.includes('Outstanding Human Verifications'), 'Should not include human verifications section');
2379
+ assert.equal(result.human_verification_count, 0);
2380
+ });
2381
+
2382
+ it('omits human verifications when no UAT files exist', () => {
2383
+ fixture = createFixture({
2384
+ '.planning/jobs/completed/milestone-v6.0.md': COMPLETED_JOB_V6,
2385
+ '.planning/ROADMAP.md': ROADMAP_WITH_PHASE_50,
2386
+ '.planning/phases/50-test-phase/': null,
2387
+ });
2388
+ const result = generateJobSummary(fixture.cwd, 'v6.0');
2389
+
2390
+ assert.equal(result.found, true);
2391
+ assert.ok(!result.content.includes('Outstanding Human Verifications'), 'Should not include human verifications section');
2392
+ assert.equal(result.human_verification_count, 0);
2393
+ });
2394
+
2395
+ it('skips UAT files without mode: auto-test', () => {
2396
+ fixture = createFixture({
2397
+ '.planning/jobs/completed/milestone-v6.0.md': COMPLETED_JOB_V6,
2398
+ '.planning/ROADMAP.md': ROADMAP_WITH_PHASE_50,
2399
+ '.planning/phases/50-test-phase/50-UAT.md': UAT_MANUAL_WITH_HUMAN_NEEDED,
2400
+ });
2401
+ const result = generateJobSummary(fixture.cwd, 'v6.0');
2402
+
2403
+ assert.equal(result.found, true);
2404
+ assert.ok(!result.content.includes('Outstanding Human Verifications'), 'Should not include human verifications for manual UAT');
2405
+ assert.equal(result.human_verification_count, 0);
2406
+ });
2407
+ });
2408
+
2409
+ // ─── Created_by field Tests ─────────────────────────────────────────────
2410
+
2411
+ describe('Created_by field', () => {
2412
+ let fixture;
2413
+
2414
+ afterEach(() => {
2415
+ if (fixture) fixture.cleanup();
2416
+ });
2417
+
2418
+ it('parseJobFile returns created_by when present', () => {
2419
+ fixture = createFixture({
2420
+ 'job.md': WELL_FORMED_JOB_WITH_CREATED_BY,
2421
+ });
2422
+ const result = parseJobFile(path.join(fixture.cwd, 'job.md'));
2423
+ assert.equal(result.created_by, 'Adrian <adrian@example.com>');
2424
+ });
2425
+
2426
+ it('parseJobFile returns null created_by when absent', () => {
2427
+ fixture = createFixture({
2428
+ 'job.md': WELL_FORMED_JOB,
2429
+ });
2430
+ const result = parseJobFile(path.join(fixture.cwd, 'job.md'));
2431
+ assert.equal(result.created_by, null);
2432
+ });
2433
+
2434
+ it('buildJobFileContent includes Created_by when provided', () => {
2435
+ const content = buildJobFileContent('v99.0', true, [{command:'plan-phase', args:'99'}], 'Test <test@t.com>');
2436
+ assert.ok(content.includes('**Created_by:** Test <test@t.com>'));
2437
+ });
2438
+
2439
+ it('buildJobFileContent omits Created_by when not provided', () => {
2440
+ const content = buildJobFileContent('v99.0', true, [{command:'plan-phase', args:'99'}]);
2441
+ assert.ok(!content.includes('Created_by'));
2442
+ });
2443
+
2444
+ it('buildJobFileContent Created_by appears between Created and Status', () => {
2445
+ const content = buildJobFileContent('v99.0', true, [{command:'plan-phase', args:'99'}], 'Test <test@t.com>');
2446
+ const lines = content.split('\n');
2447
+ const createdIdx = lines.findIndex(l => l.startsWith('**Created:**'));
2448
+ const createdByIdx = lines.findIndex(l => l.startsWith('**Created_by:**'));
2449
+ const statusIdx = lines.findIndex(l => l.startsWith('**Status:**'));
2450
+ assert.ok(createdByIdx > createdIdx, 'Created_by should come after Created');
2451
+ assert.ok(createdByIdx < statusIdx, 'Created_by should come before Status');
2452
+ });
2453
+
2454
+ it('generateJobSummary includes Created_by in overview when present', () => {
2455
+ const jobWithCreatedBy = WELL_FORMED_JOB_WITH_CREATED_BY.replace('in-progress', 'completed');
2456
+ fixture = createFixture({
2457
+ '.planning/jobs/completed/milestone-v6.0.md': jobWithCreatedBy,
2458
+ });
2459
+ const result = generateJobSummary(fixture.cwd, 'v6.0');
2460
+ assert.equal(result.found, true);
2461
+ assert.ok(result.content.includes('**Created_by:** Adrian <adrian@example.com>'));
2462
+ });
2463
+
2464
+ it('generateJobSummary omits Created_by in overview when absent', () => {
2465
+ fixture = createFixture({
2466
+ '.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
2467
+ });
2468
+ const result = generateJobSummary(fixture.cwd, 'v5.0');
2469
+ assert.equal(result.found, true);
2470
+ assert.ok(!result.content.includes('Created_by'));
2471
+ });
2472
+ });
2473
+ });
2474
+
2475
+ // ─── Root Layout Tests ───────────────────────────────────────────────────────
2476
+
2477
+ describe('jobs root-layout', () => {
2478
+ let fixture;
2479
+
2480
+ afterEach(() => {
2481
+ if (fixture) fixture.cleanup();
2482
+ fixture = null;
2483
+ resetPaths();
2484
+ });
2485
+
2486
+ it('findJobFile resolves jobs dir at root layout', () => {
2487
+ fixture = createTempProject({ layout: 'root' });
2488
+ // Create a job file at root/jobs/pending/ (not .planning/jobs/pending/)
2489
+ const jobsDir = path.join(fixture.cwd, 'jobs', 'pending');
2490
+ fs.mkdirSync(jobsDir, { recursive: true });
2491
+ const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** pending\n**Check:** true\n\n## Steps\n\n- [ ] \`/dgs:plan-phase 1\`\n`;
2492
+ fs.writeFileSync(path.join(jobsDir, 'milestone-v1.0.md'), jobContent);
2493
+
2494
+ const result = findJobFile(fixture.cwd, 'v1.0');
2495
+ assert.equal(result.found, true);
2496
+ assert.equal(result.directory, 'pending');
2497
+ // Should NOT be at .planning/jobs/
2498
+ assert.ok(result.path.includes(path.join(fixture.cwd, 'jobs')));
2499
+ assert.ok(!result.path.includes('.planning'));
2500
+ });
2501
+
2502
+ it('healthCheck creates jobs dirs at root layout', () => {
2503
+ fixture = createTempProject({ layout: 'root' });
2504
+ const result = healthCheck(fixture.cwd);
2505
+ assert.ok(result.directories.length >= 3);
2506
+ // Verify dirs were created at root, not at .planning/
2507
+ assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')));
2508
+ assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')));
2509
+ assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')));
2510
+ });
2511
+
2512
+ it('listJobs works in root layout', () => {
2513
+ fixture = createTempProject({ layout: 'root' });
2514
+ // Create jobs directory structure at root
2515
+ const pendingDir = path.join(fixture.cwd, 'jobs', 'pending');
2516
+ fs.mkdirSync(pendingDir, { recursive: true });
2517
+ fs.mkdirSync(path.join(fixture.cwd, 'jobs', 'in-progress'), { recursive: true });
2518
+ fs.mkdirSync(path.join(fixture.cwd, 'jobs', 'completed'), { recursive: true });
2519
+
2520
+ const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** pending\n**Check:** true\n\n## Steps\n\n- [ ] \`/dgs:plan-phase 1\`\n`;
2521
+ fs.writeFileSync(path.join(pendingDir, 'milestone-v1.0.md'), jobContent);
2522
+
2523
+ const result = listJobs(fixture.cwd);
2524
+ assert.equal(result.pending.length, 1);
2525
+ assert.equal(result.pending[0].version, 'v1.0');
2526
+ });
2527
+ });
2528
+
2529
+ // ─── SHA Recording & Rollback Tests ─────────────────────────────────────────
2530
+
2531
+ describe('recordStartShas', () => {
2532
+ let fixture;
2533
+ afterEach(() => { if (fixture) fixture.cleanup(); });
2534
+
2535
+ it('records planning repo SHA when no REPOS.md exists', () => {
2536
+ fixture = createTempProject({ withGit: true });
2537
+ const pendingDir = path.join(fixture.planningDir, 'jobs', 'pending');
2538
+ fs.mkdirSync(pendingDir, { recursive: true });
2539
+ const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** in-progress\n**Check:** true\n\n## Steps\n\n- [ ] \`/dgs:plan-phase 1\`\n`;
2540
+ const jobPath = path.join(pendingDir, 'milestone-v1.0.md');
2541
+ fs.writeFileSync(jobPath, jobContent);
2542
+
2543
+ const result = recordStartShas(fixture.cwd, jobPath);
2544
+ assert.equal(result.recorded, true);
2545
+ assert.ok(result.shas._planning, 'Should have _planning SHA');
2546
+ assert.ok(result.shas._planning.length >= 7, 'SHA should be at least 7 chars');
2547
+
2548
+ // Verify it was written into the file
2549
+ const updatedContent = fs.readFileSync(jobPath, 'utf-8');
2550
+ assert.ok(updatedContent.includes('**StartShas:**'), 'Job file should contain StartShas header');
2551
+ });
2552
+
2553
+ it('skips recording if StartShas already exists', () => {
2554
+ fixture = createTempProject({ withGit: true });
2555
+ const pendingDir = path.join(fixture.planningDir, 'jobs', 'pending');
2556
+ fs.mkdirSync(pendingDir, { recursive: true });
2557
+ const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** in-progress\n**Check:** true\n**StartShas:** {"_planning":"abc123"}\n\n## Steps\n\n- [ ] \`/dgs:plan-phase 1\`\n`;
2558
+ const jobPath = path.join(pendingDir, 'milestone-v1.0.md');
2559
+ fs.writeFileSync(jobPath, jobContent);
2560
+
2561
+ const result = recordStartShas(fixture.cwd, jobPath);
2562
+ assert.equal(result.recorded, true);
2563
+ assert.equal(result.already_existed, true);
2564
+ });
2565
+ });
2566
+
2567
+ describe('parseJobFile with StartShas', () => {
2568
+ let fixture;
2569
+ afterEach(() => { if (fixture) fixture.cleanup(); });
2570
+
2571
+ it('returns startShas as parsed JSON when present', () => {
2572
+ const shaObj = { _planning: 'abc123', myrepo: 'def456' };
2573
+ const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** in-progress\n**Check:** true\n**StartShas:** ${JSON.stringify(shaObj)}\n\n## Steps\n\n- [ ] \`/dgs:plan-phase 1\`\n`;
2574
+
2575
+ fixture = createFixture({
2576
+ '.planning/config.json': '{}',
2577
+ '.planning/job.md': jobContent,
2578
+ });
2579
+
2580
+ const parsed = parseJobFile(path.join(fixture.cwd, '.planning', 'job.md'));
2581
+ assert.deepEqual(parsed.startShas, shaObj);
2582
+ });
2583
+
2584
+ it('returns startShas as null when absent', () => {
2585
+ const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** pending\n**Check:** true\n\n## Steps\n\n- [ ] \`/dgs:plan-phase 1\`\n`;
2586
+
2587
+ fixture = createFixture({
2588
+ '.planning/config.json': '{}',
2589
+ '.planning/job.md': jobContent,
2590
+ });
2591
+
2592
+ const parsed = parseJobFile(path.join(fixture.cwd, '.planning', 'job.md'));
2593
+ assert.equal(parsed.startShas, null);
2594
+ });
2595
+ });
2596
+
2597
+ describe('rollbackJob', () => {
2598
+ let fixture;
2599
+ afterEach(() => { if (fixture) fixture.cleanup(); });
2600
+
2601
+ it('returns not_found for missing job', () => {
2602
+ fixture = createTempProject({ withGit: true });
2603
+ const result = rollbackJob(fixture.cwd, 'v99.0');
2604
+ assert.equal(result.rolledBack, false);
2605
+ assert.equal(result.reason, 'not_found');
2606
+ });
2607
+
2608
+ it('returns no_start_shas for job without SHAs', () => {
2609
+ fixture = createTempProject({ withGit: true });
2610
+ const pendingDir = path.join(fixture.planningDir, 'jobs', 'pending');
2611
+ fs.mkdirSync(pendingDir, { recursive: true });
2612
+ const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** completed\n**Check:** true\n\n## Steps\n\n- [x] \`/dgs:plan-phase 1\` \u2014 completed 2026-01-01T01:00:00Z\n`;
2613
+ fs.writeFileSync(path.join(pendingDir, 'milestone-v1.0.md'), jobContent);
2614
+
2615
+ const result = rollbackJob(fixture.cwd, 'v1.0');
2616
+ assert.equal(result.rolledBack, false);
2617
+ assert.equal(result.reason, 'no_start_shas');
2618
+ });
2619
+ });