@ktpartners/dgs-platform 2.9.0 → 3.3.0

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 (166) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/README.md +34 -2
  3. package/agents/dgs-executor.md +124 -3
  4. package/agents/dgs-idea-researcher.md +447 -0
  5. package/agents/dgs-plan-checker.md +61 -3
  6. package/agents/dgs-planner.md +51 -8
  7. package/bin/install.js +44 -0
  8. package/commands/dgs/abandon-quick.md +28 -0
  9. package/commands/dgs/add-tests.md +2 -2
  10. package/commands/dgs/audit-milestone.md +4 -3
  11. package/commands/dgs/capture-principle.md +11 -11
  12. package/commands/dgs/cleanup.md +2 -2
  13. package/commands/dgs/complete-milestone.md +11 -11
  14. package/commands/dgs/complete-quick.md +28 -0
  15. package/commands/dgs/create-milestone-job.md +2 -2
  16. package/commands/dgs/debug.md +3 -3
  17. package/commands/dgs/develop-idea.md +1 -1
  18. package/commands/dgs/diff-report.md +124 -0
  19. package/commands/dgs/fast.md +3 -1
  20. package/commands/dgs/health.md +1 -1
  21. package/commands/dgs/map-codebase.md +6 -6
  22. package/commands/dgs/new-milestone.md +5 -5
  23. package/commands/dgs/new-project.md +8 -21
  24. package/commands/dgs/package-scan.md +43 -0
  25. package/commands/dgs/plan-milestone-gaps.md +1 -1
  26. package/commands/dgs/progress.md +3 -3
  27. package/commands/dgs/quick-abandon.md +8 -0
  28. package/commands/dgs/quick-complete.md +8 -0
  29. package/commands/dgs/quick.md +10 -3
  30. package/commands/dgs/research-idea.md +3 -2
  31. package/commands/dgs/research-phase.md +3 -3
  32. package/commands/dgs/switch-project.md +14 -1
  33. package/commands/dgs/write-spec.md +3 -3
  34. package/deliver-great-systems/bin/dgs-tools.cjs +401 -32
  35. package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
  36. package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
  37. package/deliver-great-systems/bin/lib/commands.cjs +626 -46
  38. package/deliver-great-systems/bin/lib/commands.test.cjs +451 -0
  39. package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
  40. package/deliver-great-systems/bin/lib/config.cjs +80 -6
  41. package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
  42. package/deliver-great-systems/bin/lib/context.cjs +120 -0
  43. package/deliver-great-systems/bin/lib/core.cjs +35 -14
  44. package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
  45. package/deliver-great-systems/bin/lib/execution.cjs +49 -17
  46. package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
  47. package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
  48. package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
  49. package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
  50. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
  51. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
  52. package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
  53. package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
  54. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
  55. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
  56. package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
  57. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
  58. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
  59. package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
  60. package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
  61. package/deliver-great-systems/bin/lib/governance.cjs +211 -0
  62. package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
  63. package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
  64. package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
  65. package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
  66. package/deliver-great-systems/bin/lib/init.cjs +357 -61
  67. package/deliver-great-systems/bin/lib/init.test.cjs +625 -8
  68. package/deliver-great-systems/bin/lib/jobs.cjs +131 -25
  69. package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
  70. package/deliver-great-systems/bin/lib/migration.cjs +409 -1
  71. package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
  72. package/deliver-great-systems/bin/lib/milestone.cjs +154 -31
  73. package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
  74. package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
  75. package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
  76. package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
  77. package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
  78. package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
  79. package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
  80. package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
  81. package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
  82. package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
  83. package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
  84. package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
  85. package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
  86. package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
  87. package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
  88. package/deliver-great-systems/bin/lib/phase.cjs +146 -3
  89. package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
  90. package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
  91. package/deliver-great-systems/bin/lib/projects.cjs +65 -10
  92. package/deliver-great-systems/bin/lib/projects.test.cjs +198 -2
  93. package/deliver-great-systems/bin/lib/quick.cjs +739 -0
  94. package/deliver-great-systems/bin/lib/quick.test.cjs +730 -0
  95. package/deliver-great-systems/bin/lib/repos.cjs +37 -13
  96. package/deliver-great-systems/bin/lib/review.cjs +1821 -0
  97. package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
  98. package/deliver-great-systems/bin/lib/specs.cjs +3 -81
  99. package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
  100. package/deliver-great-systems/bin/lib/state.cjs +147 -55
  101. package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
  102. package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
  103. package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
  104. package/deliver-great-systems/bin/lib/sync.cjs +75 -0
  105. package/deliver-great-systems/bin/lib/verify.cjs +198 -7
  106. package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
  107. package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
  108. package/deliver-great-systems/bin/lib/worktrees.cjs +790 -0
  109. package/deliver-great-systems/bin/lib/worktrees.test.cjs +963 -0
  110. package/deliver-great-systems/references/agent-step-reliability.md +60 -0
  111. package/deliver-great-systems/references/conflict-resolution.md +4 -0
  112. package/deliver-great-systems/references/context-tiers.md +4 -0
  113. package/deliver-great-systems/references/package-scan-config.md +151 -0
  114. package/deliver-great-systems/references/questioning.md +0 -30
  115. package/deliver-great-systems/references/spec-review-loop.md +1 -2
  116. package/deliver-great-systems/references/workflow-conventions.md +29 -0
  117. package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
  118. package/deliver-great-systems/templates/REVIEW.md +35 -0
  119. package/deliver-great-systems/templates/VALIDATION.md +1 -1
  120. package/deliver-great-systems/templates/claude-md.md +27 -0
  121. package/deliver-great-systems/templates/package-scan-report.md +108 -0
  122. package/deliver-great-systems/templates/project.md +6 -170
  123. package/deliver-great-systems/templates/summary.md +3 -1
  124. package/deliver-great-systems/workflows/abandon-quick.md +89 -0
  125. package/deliver-great-systems/workflows/add-idea.md +3 -3
  126. package/deliver-great-systems/workflows/add-phase.md +5 -0
  127. package/deliver-great-systems/workflows/add-tests.md +14 -0
  128. package/deliver-great-systems/workflows/add-todo.md +1 -0
  129. package/deliver-great-systems/workflows/approve-spec.md +25 -4
  130. package/deliver-great-systems/workflows/audit-milestone.md +66 -10
  131. package/deliver-great-systems/workflows/audit-phase.md +15 -5
  132. package/deliver-great-systems/workflows/cancel-job.md +2 -2
  133. package/deliver-great-systems/workflows/check-todos.md +2 -3
  134. package/deliver-great-systems/workflows/codereview.md +103 -9
  135. package/deliver-great-systems/workflows/complete-milestone.md +218 -24
  136. package/deliver-great-systems/workflows/complete-quick.md +106 -0
  137. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  138. package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
  139. package/deliver-great-systems/workflows/develop-idea.md +11 -11
  140. package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
  141. package/deliver-great-systems/workflows/discuss-idea.md +1 -1
  142. package/deliver-great-systems/workflows/discuss-phase.md +3 -2
  143. package/deliver-great-systems/workflows/execute-phase.md +209 -33
  144. package/deliver-great-systems/workflows/execute-plan.md +22 -22
  145. package/deliver-great-systems/workflows/help.md +53 -20
  146. package/deliver-great-systems/workflows/import-spec.md +65 -7
  147. package/deliver-great-systems/workflows/init-product.md +45 -167
  148. package/deliver-great-systems/workflows/new-milestone.md +140 -33
  149. package/deliver-great-systems/workflows/new-project.md +60 -331
  150. package/deliver-great-systems/workflows/package-scan.md +59 -0
  151. package/deliver-great-systems/workflows/plan-phase.md +79 -1
  152. package/deliver-great-systems/workflows/progress-all.md +133 -0
  153. package/deliver-great-systems/workflows/quick-abandon.md +89 -0
  154. package/deliver-great-systems/workflows/quick-complete.md +106 -0
  155. package/deliver-great-systems/workflows/quick.md +328 -26
  156. package/deliver-great-systems/workflows/refine-spec.md +1 -1
  157. package/deliver-great-systems/workflows/research-idea.md +77 -139
  158. package/deliver-great-systems/workflows/resume-project.md +2 -2
  159. package/deliver-great-systems/workflows/run-job.md +29 -43
  160. package/deliver-great-systems/workflows/settings.md +13 -77
  161. package/deliver-great-systems/workflows/validate-phase.md +39 -1
  162. package/deliver-great-systems/workflows/verify-work.md +14 -0
  163. package/deliver-great-systems/workflows/write-spec.md +11 -13
  164. package/hooks/dist/dgs-enforce-discipline.js +196 -0
  165. package/package.json +1 -1
  166. package/scripts/build-hooks.js +1 -0
@@ -4,32 +4,31 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { escapeRegex, getMilestonePhaseFilter, output, error } = require('./core.cjs');
7
+ const { escapeRegex, getMilestonePhaseFilter, getProjectRoot, output, error, loadConfig } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
9
  const { getPlanningRoot } = require('./paths.cjs');
10
10
  const { writeStateMd } = require('./state.cjs');
11
+ const { getContributors, checkFourEyes } = require('./governance.cjs');
12
+ const { requireGitIdentity, formatAuthorString } = require('./identity.cjs');
11
13
 
12
- function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
13
- if (!reqIdsRaw || reqIdsRaw.length === 0) {
14
- error('requirement IDs required. Usage: requirements mark-complete REQ-01,REQ-02 or REQ-01 REQ-02');
15
- }
16
-
17
- // Accept comma-separated, space-separated, or bracket-wrapped: [REQ-01, REQ-02]
18
- const reqIds = reqIdsRaw
19
- .join(' ')
20
- .replace(/[\[\]]/g, '')
21
- .split(/[,\s]+/)
22
- .map(r => r.trim())
23
- .filter(Boolean);
24
-
25
- if (reqIds.length === 0) {
14
+ /**
15
+ * Internal helper: marks requirement IDs complete in REQUIREMENTS.md.
16
+ * Accepts an already-parsed array of IDs. Returns { result, reqPath, rawValue }
17
+ * WITHOUT calling output()/exit.
18
+ */
19
+ function requirementsMarkCompleteInternal(cwd, reqIds) {
20
+ if (!Array.isArray(reqIds) || reqIds.length === 0) {
26
21
  error('no valid requirement IDs found');
27
22
  }
28
23
 
29
- const reqPath = path.join(getPlanningRoot(cwd), 'REQUIREMENTS.md');
24
+ const projectRootRel = getProjectRoot(cwd);
25
+ const reqPath = path.join(getPlanningRoot(cwd), projectRootRel, 'REQUIREMENTS.md');
30
26
  if (!fs.existsSync(reqPath)) {
31
- output({ updated: false, reason: 'REQUIREMENTS.md not found', ids: reqIds }, raw, 'no requirements file');
32
- return;
27
+ return {
28
+ result: { updated: false, reason: 'REQUIREMENTS.md not found', ids: reqIds },
29
+ reqPath,
30
+ rawValue: 'no requirements file',
31
+ };
33
32
  }
34
33
 
35
34
  let reqContent = fs.readFileSync(reqPath, 'utf-8');
@@ -69,12 +68,37 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
69
68
  fs.writeFileSync(reqPath, reqContent, 'utf-8');
70
69
  }
71
70
 
72
- output({
73
- updated: updated.length > 0,
74
- marked_complete: updated,
75
- not_found: notFound,
76
- total: reqIds.length,
77
- }, raw, `${updated.length}/${reqIds.length} requirements marked complete`);
71
+ return {
72
+ result: {
73
+ updated: updated.length > 0,
74
+ marked_complete: updated,
75
+ not_found: notFound,
76
+ total: reqIds.length,
77
+ },
78
+ reqPath,
79
+ rawValue: `${updated.length}/${reqIds.length} requirements marked complete`,
80
+ };
81
+ }
82
+
83
+ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
84
+ if (!reqIdsRaw || reqIdsRaw.length === 0) {
85
+ error('requirement IDs required. Usage: requirements mark-complete REQ-01,REQ-02 or REQ-01 REQ-02');
86
+ }
87
+
88
+ // Accept comma-separated, space-separated, or bracket-wrapped: [REQ-01, REQ-02]
89
+ const reqIds = reqIdsRaw
90
+ .join(' ')
91
+ .replace(/[\[\]]/g, '')
92
+ .split(/[,\s]+/)
93
+ .map(r => r.trim())
94
+ .filter(Boolean);
95
+
96
+ if (reqIds.length === 0) {
97
+ error('no valid requirement IDs found');
98
+ }
99
+
100
+ const { result, rawValue } = requirementsMarkCompleteInternal(cwd, reqIds);
101
+ output(result, raw, rawValue);
78
102
  }
79
103
 
80
104
  function cmdMilestoneComplete(cwd, version, options, raw) {
@@ -83,12 +107,14 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
83
107
  }
84
108
 
85
109
  const planRoot = getPlanningRoot(cwd);
86
- const roadmapPath = path.join(planRoot, 'ROADMAP.md');
87
- const reqPath = path.join(planRoot, 'REQUIREMENTS.md');
88
- const statePath = path.join(planRoot, 'STATE.md');
110
+ const projectRootRel = getProjectRoot(cwd);
111
+ const projectRoot = path.join(planRoot, projectRootRel);
112
+ const roadmapPath = path.join(projectRoot, 'ROADMAP.md');
113
+ const reqPath = path.join(projectRoot, 'REQUIREMENTS.md');
114
+ const statePath = path.join(projectRoot, 'STATE.md');
89
115
  const milestonesPath = path.join(planRoot, 'MILESTONES.md');
90
116
  const archiveDir = path.join(planRoot, 'milestones');
91
- const phasesDir = path.join(planRoot, 'phases');
117
+ const phasesDir = path.join(projectRoot, 'phases');
92
118
  const today = new Date().toISOString().split('T')[0];
93
119
  const milestoneName = options.name || version;
94
120
 
@@ -135,6 +161,72 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
135
161
  }
136
162
  } catch {}
137
163
 
164
+ // ── Four-Eyes Gate (GATE-01) ───────────────────────────────────────────────
165
+ const config = loadConfig(cwd);
166
+ const rawConfig = (() => {
167
+ try {
168
+ return JSON.parse(fs.readFileSync(path.join(planRoot, 'config.json'), 'utf-8'));
169
+ } catch { return {}; }
170
+ })();
171
+ const fourEyesMode = (rawConfig.workflow && rawConfig.workflow.four_eyes) || 'off';
172
+ let governanceNote = null;
173
+
174
+ if (fourEyesMode !== 'off') {
175
+ // Resolve current user identity
176
+ let currentUserStr = '';
177
+ try {
178
+ const identity = requireGitIdentity(cwd);
179
+ currentUserStr = formatAuthorString(identity);
180
+ } catch {
181
+ // Identity gate already ran — this is a safety fallback
182
+ currentUserStr = '';
183
+ }
184
+
185
+ // Get contributors for this milestone
186
+ const contribResult = getContributors(cwd);
187
+ const contributors = contribResult.contributors || [];
188
+
189
+ // Run four-eyes check
190
+ const feResult = checkFourEyes(contributors, currentUserStr, fourEyesMode);
191
+
192
+ // Display contributor list (SHR-02 — always when not off)
193
+ const contribNames = contributors.length > 0
194
+ ? contributors.join(', ')
195
+ : '(none detected)';
196
+
197
+ if (feResult.passed) {
198
+ // Four-eyes satisfied — display and continue
199
+ process.stderr.write('Contributors: ' + contribNames + ' \u2014 \u2714 Four-eyes satisfied\n');
200
+ } else {
201
+ // Four-eyes failed — display contributor list first
202
+ process.stderr.write('Contributors: ' + contribNames + '\n');
203
+
204
+ // Extract current user name for display
205
+ let displayName = currentUserStr;
206
+ try {
207
+ const identity = requireGitIdentity(cwd);
208
+ displayName = identity.name;
209
+ } catch { /* use full string */ }
210
+
211
+ if (fourEyesMode === 'warn') {
212
+ // GATE-04: warn mode — display warning, proceed, log
213
+ process.stderr.write('\u26A0 You (' + displayName + ') contributed to this milestone. Completing anyway (warn mode).\n');
214
+ governanceNote = 'Four-eyes warning: ' + displayName + ' completed while also a contributor. Contributors: ' + contribNames;
215
+ } else if (fourEyesMode === 'enforce') {
216
+ if (options.force) {
217
+ // GATE-06: --force bypasses enforce
218
+ process.stderr.write('\u26A0 Forced: you (' + displayName + ') are the only contributor. Override logged.\n');
219
+ governanceNote = 'Four-eyes override: ' + displayName + ' force-completed. Contributors: ' + contribNames;
220
+ } else {
221
+ // GATE-05: enforce mode — block completion
222
+ error('\u2718 Blocked: you (' + displayName + ') are the only contributor. Use --force to override.');
223
+ return;
224
+ }
225
+ }
226
+ }
227
+ }
228
+ // Off mode: no check, no output, no performance impact (GATE-03)
229
+
138
230
  // Archive ROADMAP.md
139
231
  if (fs.existsSync(roadmapPath)) {
140
232
  const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
@@ -149,14 +241,15 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
149
241
  }
150
242
 
151
243
  // Archive audit file if exists
152
- const auditFile = path.join(planRoot, `${version}-MILESTONE-AUDIT.md`);
244
+ const auditFile = path.join(projectRoot, `${version}-MILESTONE-AUDIT.md`);
153
245
  if (fs.existsSync(auditFile)) {
154
246
  fs.renameSync(auditFile, path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`));
155
247
  }
156
248
 
157
249
  // Create/append MILESTONES.md entry
158
250
  const accomplishmentsList = accomplishments.map(a => `- ${a}`).join('\n');
159
- const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n---\n\n`;
251
+ const governanceLine = governanceNote ? `\n**Governance:** ${governanceNote}\n` : '';
252
+ const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}${governanceLine}\n\n---\n\n`;
160
253
 
161
254
  if (fs.existsSync(milestonesPath)) {
162
255
  const existing = fs.readFileSync(milestonesPath, 'utf-8');
@@ -190,9 +283,12 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
190
283
  /(\*\*Last Activity:\*\*\s*).*/,
191
284
  `$1${today}`
192
285
  );
286
+ const activitySuffix = governanceNote
287
+ ? ` (four-eyes: ${fourEyesMode === 'warn' ? 'warn' : 'force'} override by ${(() => { try { return requireGitIdentity(cwd).name; } catch { return 'unknown'; } })()})`
288
+ : '';
193
289
  stateContent = stateContent.replace(
194
290
  /(\*\*Last Activity Description:\*\*\s*).*/,
195
- `$1${version} milestone completed and archived`
291
+ `$1${version} milestone completed and archived${activitySuffix}`
196
292
  );
197
293
  writeStateMd(statePath, stateContent, cwd);
198
294
  }
@@ -216,6 +312,31 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
216
312
  } catch {}
217
313
  }
218
314
 
315
+ // Archive completed quick task directories
316
+ let quickArchived = false;
317
+ const quickDir = path.join(projectRoot, 'quick');
318
+ if (fs.existsSync(quickDir)) {
319
+ try {
320
+ const quickArchiveDir = path.join(archiveDir, `${version}-quick`);
321
+ const quickEntries = fs.readdirSync(quickDir, { withFileTypes: true });
322
+ const quickDirNames = quickEntries.filter(e => e.isDirectory()).map(e => e.name);
323
+ let archivedCount = 0;
324
+ for (const dir of quickDirNames) {
325
+ // Check if this quick dir has a SUMMARY file (completed)
326
+ const dirPath = path.join(quickDir, dir);
327
+ const dirFiles = fs.readdirSync(dirPath);
328
+ const hasSummary = dirFiles.some(f => f.endsWith('-SUMMARY.md'));
329
+ if (!hasSummary) continue;
330
+ if (!fs.existsSync(quickArchiveDir)) {
331
+ fs.mkdirSync(quickArchiveDir, { recursive: true });
332
+ }
333
+ fs.renameSync(dirPath, path.join(quickArchiveDir, dir));
334
+ archivedCount++;
335
+ }
336
+ quickArchived = archivedCount > 0;
337
+ } catch {}
338
+ }
339
+
219
340
  const result = {
220
341
  version,
221
342
  name: milestoneName,
@@ -229,6 +350,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
229
350
  requirements: fs.existsSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`)),
230
351
  audit: fs.existsSync(path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)),
231
352
  phases: phasesArchived,
353
+ quick: quickArchived,
232
354
  },
233
355
  milestones_updated: true,
234
356
  state_updated: fs.existsSync(statePath),
@@ -239,5 +361,6 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
239
361
 
240
362
  module.exports = {
241
363
  cmdRequirementsMarkComplete,
364
+ requirementsMarkCompleteInternal,
242
365
  cmdMilestoneComplete,
243
366
  };
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Tests for milestone.cjs — quick dir archival in cmdMilestoneComplete
3
+ *
4
+ * Uses Node.js built-in test runner (node:test) and assert (node:assert).
5
+ * Each test creates an isolated temp directory fixture and cleans up after.
6
+ */
7
+
8
+ const { describe, it, beforeEach, afterEach } = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const { createFixture, writeFile } = require('./test-helpers.cjs');
14
+ const { cmdMilestoneComplete } = require('./milestone.cjs');
15
+
16
+ // Helper: capture stdout from cmdMilestoneComplete (which calls output())
17
+ function captureResult(fn) {
18
+ const chunks = [];
19
+ const origWrite = process.stdout.write;
20
+ process.stdout.write = function (chunk) {
21
+ chunks.push(String(chunk));
22
+ };
23
+ // Also suppress stderr (governance / state messages)
24
+ const origStderrWrite = process.stderr.write;
25
+ process.stderr.write = function () {};
26
+ // Prevent process.exit from actually exiting during tests
27
+ const origExit = process.exit;
28
+ process.exit = function () {};
29
+ try {
30
+ fn();
31
+ } finally {
32
+ process.stdout.write = origWrite;
33
+ process.stderr.write = origStderrWrite;
34
+ process.exit = origExit;
35
+ }
36
+ const raw = chunks.join('');
37
+ try {
38
+ return JSON.parse(raw);
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Creates a root-layout fixture suitable for cmdMilestoneComplete.
46
+ * Root layout: no PROJECTS.md/REPOS.md (avoids v2 detection).
47
+ * Includes config.local.json with planningRoot, STATE.md, ROADMAP.md, phases/.
48
+ */
49
+ function createMilestoneFixture(extras) {
50
+ const base = {
51
+ 'config.json': JSON.stringify({}),
52
+ 'config.local.json': JSON.stringify({ planningRoot: '.' }),
53
+ 'STATE.md': '# Project State\n\nPhase: 1\nStatus: Ready\nProgress: [----------] 0%\n',
54
+ 'ROADMAP.md': '# Roadmap\n\n## Phase 1: Test\n',
55
+ 'phases/': null,
56
+ };
57
+ if (extras) {
58
+ Object.assign(base, extras);
59
+ }
60
+ return createFixture(base);
61
+ }
62
+
63
+ // ─── Quick Dir Archival Tests ────────────────────────────────────────────────
64
+
65
+ describe('cmdMilestoneComplete quick dir archival', () => {
66
+ let fixture;
67
+
68
+ afterEach(() => {
69
+ if (fixture) fixture.cleanup();
70
+ });
71
+
72
+ it('archives completed quick dirs (with SUMMARY.md) to milestones/v1.0-quick/', () => {
73
+ fixture = createMilestoneFixture({
74
+ 'quick/260101-abc-some-task/260101-abc-PLAN.md': '# Plan',
75
+ 'quick/260101-abc-some-task/260101-abc-SUMMARY.md': '# Summary',
76
+ });
77
+ const cwd = fixture.cwd;
78
+
79
+ const result = captureResult(() => {
80
+ cmdMilestoneComplete(cwd, 'v1.0', {}, false);
81
+ });
82
+
83
+ // Quick dir should be moved to milestones/v1.0-quick/
84
+ const archivedDir = path.join(cwd, 'milestones', 'v1.0-quick', '260101-abc-some-task');
85
+ assert.ok(fs.existsSync(archivedDir), 'Quick dir should be archived to milestones/v1.0-quick/');
86
+ assert.ok(!fs.existsSync(path.join(cwd, 'quick', '260101-abc-some-task')), 'Quick dir should be removed from quick/');
87
+ assert.ok(result, 'Should return result JSON');
88
+ assert.equal(result.archived.quick, true, 'archived.quick should be true');
89
+ });
90
+
91
+ it('does not move HISTORY.md from quick/', () => {
92
+ fixture = createMilestoneFixture({
93
+ 'quick/HISTORY.md': '# History\n',
94
+ 'quick/260102-def-other/260102-def-SUMMARY.md': '# Summary',
95
+ });
96
+ const cwd = fixture.cwd;
97
+
98
+ const result = captureResult(() => {
99
+ cmdMilestoneComplete(cwd, 'v1.0', {}, false);
100
+ });
101
+
102
+ // HISTORY.md should remain in quick/
103
+ assert.ok(fs.existsSync(path.join(cwd, 'quick', 'HISTORY.md')), 'HISTORY.md should stay in quick/');
104
+ // The completed dir should be archived
105
+ assert.ok(fs.existsSync(path.join(cwd, 'milestones', 'v1.0-quick', '260102-def-other')), 'Completed dir should be archived');
106
+ });
107
+
108
+ it('skips plain files (non-directories) in quick/', () => {
109
+ fixture = createMilestoneFixture({
110
+ 'quick/some-notes.txt': 'random notes',
111
+ 'quick/260103-ghi-task/260103-ghi-SUMMARY.md': '# Summary',
112
+ });
113
+ const cwd = fixture.cwd;
114
+
115
+ const result = captureResult(() => {
116
+ cmdMilestoneComplete(cwd, 'v1.0', {}, false);
117
+ });
118
+
119
+ // File should remain
120
+ assert.ok(fs.existsSync(path.join(cwd, 'quick', 'some-notes.txt')), 'Plain files should remain in quick/');
121
+ // Dir should be archived
122
+ assert.ok(fs.existsSync(path.join(cwd, 'milestones', 'v1.0-quick', '260103-ghi-task')), 'Completed dir should be archived');
123
+ });
124
+
125
+ it('does NOT move quick dirs without SUMMARY.md (incomplete)', () => {
126
+ fixture = createMilestoneFixture({
127
+ 'quick/260104-jkl-wip/260104-jkl-PLAN.md': '# Plan',
128
+ });
129
+ const cwd = fixture.cwd;
130
+
131
+ const result = captureResult(() => {
132
+ cmdMilestoneComplete(cwd, 'v1.0', {}, false);
133
+ });
134
+
135
+ // Should NOT be moved
136
+ assert.ok(fs.existsSync(path.join(cwd, 'quick', '260104-jkl-wip')), 'Incomplete quick dir should stay in quick/');
137
+ assert.ok(!fs.existsSync(path.join(cwd, 'milestones', 'v1.0-quick')), 'No quick archive dir should be created');
138
+ assert.ok(result, 'Should return result JSON');
139
+ assert.equal(result.archived.quick, false, 'archived.quick should be false when nothing archived');
140
+ });
141
+
142
+ it('gracefully handles when quick/ does not exist', () => {
143
+ fixture = createMilestoneFixture();
144
+ const cwd = fixture.cwd;
145
+
146
+ // No quick/ directory at all
147
+ const result = captureResult(() => {
148
+ cmdMilestoneComplete(cwd, 'v1.0', {}, false);
149
+ });
150
+
151
+ assert.ok(result, 'Should return result JSON without error');
152
+ assert.equal(result.archived.quick, false, 'archived.quick should be false when no quick/ dir');
153
+ });
154
+
155
+ it('returns quickArchived=false when quick/ has no completed dirs', () => {
156
+ fixture = createMilestoneFixture({
157
+ 'quick/HISTORY.md': '# History',
158
+ 'quick/260105-mno-wip/260105-mno-PLAN.md': '# Plan',
159
+ });
160
+ const cwd = fixture.cwd;
161
+
162
+ const result = captureResult(() => {
163
+ cmdMilestoneComplete(cwd, 'v1.0', {}, false);
164
+ });
165
+
166
+ assert.equal(result.archived.quick, false, 'archived.quick should be false');
167
+ assert.ok(!fs.existsSync(path.join(cwd, 'milestones', 'v1.0-quick')), 'No quick archive dir should be created');
168
+ });
169
+
170
+ it('creates milestones/v{X.Y}-quick/ only when there are dirs to archive', () => {
171
+ fixture = createMilestoneFixture({
172
+ 'quick/260106-pqr-first/260106-pqr-SUMMARY.md': '# Summary',
173
+ 'quick/260107-stu-second/260107-stu-SUMMARY.md': '# Summary',
174
+ 'quick/260108-vwx-wip/260108-vwx-PLAN.md': '# Plan',
175
+ });
176
+ const cwd = fixture.cwd;
177
+
178
+ const result = captureResult(() => {
179
+ cmdMilestoneComplete(cwd, 'v2.0', {}, false);
180
+ });
181
+
182
+ // Both completed dirs should be archived
183
+ assert.ok(fs.existsSync(path.join(cwd, 'milestones', 'v2.0-quick', '260106-pqr-first')), 'First completed dir archived');
184
+ assert.ok(fs.existsSync(path.join(cwd, 'milestones', 'v2.0-quick', '260107-stu-second')), 'Second completed dir archived');
185
+ // Incomplete dir should remain
186
+ assert.ok(fs.existsSync(path.join(cwd, 'quick', '260108-vwx-wip')), 'Incomplete dir should stay');
187
+ assert.equal(result.archived.quick, true, 'archived.quick should be true');
188
+ });
189
+
190
+ it('result JSON includes archived.quick field', () => {
191
+ fixture = createMilestoneFixture();
192
+ const cwd = fixture.cwd;
193
+
194
+ const result = captureResult(() => {
195
+ cmdMilestoneComplete(cwd, 'v1.0', {}, false);
196
+ });
197
+
198
+ assert.ok(result, 'Should return result');
199
+ assert.ok('archived' in result, 'Result should have archived object');
200
+ assert.ok('quick' in result.archived, 'archived should have quick field');
201
+ assert.equal(typeof result.archived.quick, 'boolean', 'archived.quick should be boolean');
202
+ });
203
+ });