@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
@@ -224,7 +224,12 @@ function cmdRoadmapAnalyze(cwd, raw) {
224
224
  output(result, raw);
225
225
  }
226
226
 
227
- function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
227
+ /**
228
+ * Internal helper: performs the ROADMAP.md plan-progress update for a single
229
+ * phase and returns { result, roadmapPath, rawValue } without emitting/exiting.
230
+ * External callers (cmdRoadmapUpdatePlanProgress, cmdPlanFinalize) handle output.
231
+ */
232
+ function roadmapUpdatePlanProgressInternal(cwd, phaseNum) {
228
233
  if (!phaseNum) {
229
234
  error('phase number required for roadmap update-plan-progress');
230
235
  }
@@ -240,8 +245,11 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
240
245
  const summaryCount = phaseInfo.summaries.length;
241
246
 
242
247
  if (planCount === 0) {
243
- output({ updated: false, reason: 'No plans found', plan_count: 0, summary_count: 0 }, raw, 'no plans');
244
- return;
248
+ return {
249
+ result: { updated: false, reason: 'No plans found', plan_count: 0, summary_count: 0 },
250
+ roadmapPath,
251
+ rawValue: 'no plans',
252
+ };
245
253
  }
246
254
 
247
255
  const isComplete = summaryCount >= planCount;
@@ -249,8 +257,11 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
249
257
  const today = new Date().toISOString().split('T')[0];
250
258
 
251
259
  if (!fs.existsSync(roadmapPath)) {
252
- output({ updated: false, reason: 'ROADMAP.md not found', plan_count: planCount, summary_count: summaryCount }, raw, 'no roadmap');
253
- return;
260
+ return {
261
+ result: { updated: false, reason: 'ROADMAP.md not found', plan_count: planCount, summary_count: summaryCount },
262
+ roadmapPath,
263
+ rawValue: 'no roadmap',
264
+ };
254
265
  }
255
266
 
256
267
  let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
@@ -288,18 +299,28 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
288
299
 
289
300
  fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
290
301
 
291
- output({
292
- updated: true,
293
- phase: phaseNum,
294
- plan_count: planCount,
295
- summary_count: summaryCount,
296
- status,
297
- complete: isComplete,
298
- }, raw, `${summaryCount}/${planCount} ${status}`);
302
+ return {
303
+ result: {
304
+ updated: true,
305
+ phase: phaseNum,
306
+ plan_count: planCount,
307
+ summary_count: summaryCount,
308
+ status,
309
+ complete: isComplete,
310
+ },
311
+ roadmapPath,
312
+ rawValue: `${summaryCount}/${planCount} ${status}`,
313
+ };
314
+ }
315
+
316
+ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
317
+ const { result, rawValue } = roadmapUpdatePlanProgressInternal(cwd, phaseNum);
318
+ output(result, raw, rawValue);
299
319
  }
300
320
 
301
321
  module.exports = {
302
322
  cmdRoadmapGetPhase,
303
323
  cmdRoadmapAnalyze,
304
324
  cmdRoadmapUpdatePlanProgress,
325
+ roadmapUpdatePlanProgressInternal,
305
326
  };
@@ -774,13 +774,13 @@ function cmdSpecsFinalize(cwd, idOrFilename, raw, author) {
774
774
  const content = buildSpecContent(spec.frontmatter, spec.body);
775
775
  fs.writeFileSync(spec.path, content, 'utf-8');
776
776
 
777
- // Move source ideas to done
777
+ // Set source ideas to done via frontmatter edit (no file moves)
778
+ const { setIdeaStatus } = require('./ideas.cjs');
778
779
  const ideasMoved = [];
779
780
  const ideasFailed = [];
780
781
  const sourceIdeas = spec.frontmatter.source_ideas || [];
781
782
 
782
783
  for (const ideaFilename of sourceIdeas) {
783
- // Extract idea ID from filename (e.g., "001-slug.md" -> "001")
784
784
  const idMatch = ideaFilename.match(/^(\d+)/);
785
785
  if (!idMatch) {
786
786
  ideasFailed.push({ idea: ideaFilename, reason: 'could not parse ID' });
@@ -789,90 +789,13 @@ function cmdSpecsFinalize(cwd, idOrFilename, raw, author) {
789
789
  const ideaId = idMatch[1];
790
790
 
791
791
  try {
792
- // Move ideas to done manually (ideas.cjs cmd functions call process.exit,
793
- // so we use the internal helpers directly instead)
794
- // Instead, do the move manually following the same pattern
795
- const { findIdeaFile, ensureIdeasDirs } = require('./ideas.cjs');
796
- const idea = findIdeaFile(cwd, ideaId);
797
-
798
- if (!idea) {
799
- ideasFailed.push({ idea: ideaFilename, reason: 'idea not found' });
800
- continue;
801
- }
802
-
803
- if (idea.state === 'done') {
804
- // Already done, skip
805
- ideasMoved.push(ideaFilename);
806
- continue;
807
- }
808
-
809
- if (idea.state === 'consolidated') {
810
- // Consolidated ideas cannot be used as spec sources
811
- ideasFailed.push({ idea: ideaFilename, reason: 'idea is consolidated' });
812
- continue;
813
- }
814
-
815
- ensureIdeasDirs(cwd);
816
-
817
- const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
818
- const fromRel = path.join(planRootRel, 'ideas', idea.state, idea.filename);
819
- const toRel = path.join(planRootRel, 'ideas', 'done', idea.filename);
820
- const mvResult = execGit(cwd, ['mv', fromRel, toRel]);
821
-
822
- if (mvResult.exitCode !== 0) {
823
- ideasFailed.push({ idea: ideaFilename, reason: mvResult.stderr || 'git mv failed' });
824
- continue;
825
- }
826
-
792
+ setIdeaStatus(cwd, ideaId, 'done');
827
793
  ideasMoved.push(ideaFilename);
828
794
  } catch (err) {
829
795
  ideasFailed.push({ idea: ideaFilename, reason: err.message || 'unknown error' });
830
796
  }
831
797
  }
832
798
 
833
- // Move associated research documents and update links in idea files
834
- const researchDocsMoved = [];
835
-
836
- for (const ideaFilename of ideasMoved) {
837
- try {
838
- // Extract slug from idea filename: "001-my-idea.md" -> "my-idea"
839
- const slug = ideaFilename.replace(/^\d+-/, '').replace(/\.md$/, '');
840
- const researchDocName = `${slug}-research.md`;
841
- const specPlanRoot = getPlanningRoot(cwd);
842
- const specPlanRootRel = path.relative(cwd, specPlanRoot) || '.';
843
- const sourcePath = path.join(specPlanRoot, 'docs', 'ideas', 'pending', researchDocName);
844
-
845
- // Skip if no research doc exists (graceful handling)
846
- if (!fs.existsSync(sourcePath)) continue;
847
-
848
- // Ensure done directory exists
849
- fs.mkdirSync(path.join(specPlanRoot, 'docs', 'ideas', 'done'), { recursive: true });
850
-
851
- const sourceRel = path.join(specPlanRootRel, 'docs', 'ideas', 'pending', researchDocName);
852
- const targetRel = path.join(specPlanRootRel, 'docs', 'ideas', 'done', researchDocName);
853
- const mvResult = execGit(cwd, ['mv', sourceRel, targetRel]);
854
-
855
- if (mvResult.exitCode !== 0) continue;
856
-
857
- researchDocsMoved.push(researchDocName);
858
-
859
- // Update documentLink paths in the idea file (now in done/)
860
- const ideaDonePath = path.join(getPlanningRoot(cwd), 'ideas', 'done', ideaFilename);
861
- if (fs.existsSync(ideaDonePath)) {
862
- let ideaContent = fs.readFileSync(ideaDonePath, 'utf-8');
863
- // Replace pending/ paths with done/ for this research doc
864
- const pendingPattern = `docs/ideas/pending/${researchDocName}`;
865
- const doneReplacement = `docs/ideas/done/${researchDocName}`;
866
- if (ideaContent.includes(pendingPattern)) {
867
- ideaContent = ideaContent.split(pendingPattern).join(doneReplacement);
868
- fs.writeFileSync(ideaDonePath, ideaContent, 'utf-8');
869
- }
870
- }
871
- } catch {
872
- // Graceful handling: skip on any error
873
- }
874
- }
875
-
876
799
  // Auto-commit the spec status change and idea moves
877
800
  execGit(cwd, ['add', '-A']);
878
801
  const title = spec.frontmatter.title || spec.frontmatter.id;
@@ -884,7 +807,6 @@ function cmdSpecsFinalize(cwd, idOrFilename, raw, author) {
884
807
  status: 'final',
885
808
  ideas_moved: ideasMoved,
886
809
  ideas_failed: ideasFailed,
887
- research_docs_moved: researchDocsMoved,
888
810
  };
889
811
  output(result, raw);
890
812
  }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * GATE tests: State Transition Invariant
3
+ *
4
+ * Enforces that no code path in ideas, todos, jobs, or specs uses file moves
5
+ * (git mv, renameSync, unlinkSync) for state transitions. All state changes
6
+ * must use frontmatter edits (setIdeaStatus, setTodoStatus, setJobStatus).
7
+ *
8
+ * This GATE was introduced in Phase 133 (v20.0) after rewriting all 9 file-move
9
+ * state transitions to frontmatter edits.
10
+ */
11
+
12
+ const { describe, it } = require('node:test');
13
+ const assert = require('node:assert/strict');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const LIB_DIR = path.join(__dirname);
18
+
19
+ // State subdirectory patterns that indicate directory-based state management
20
+ const STATE_DIR_PATTERNS = [
21
+ /ideas\/pending\//,
22
+ /ideas\/done\//,
23
+ /ideas\/rejected\//,
24
+ /ideas\/consolidated\//,
25
+ /todos\/pending\//,
26
+ /todos\/completed\//,
27
+ /jobs\/pending\//,
28
+ /jobs\/in-progress\//,
29
+ /jobs\/completed\//,
30
+ ];
31
+
32
+ // Move/rename/delete operations that are prohibited in state transition contexts
33
+ const MOVE_PATTERNS = [
34
+ { pattern: /execGit\([^)]*\[\s*['"]mv['"]/, label: 'git mv' },
35
+ { pattern: /renameSync\(/, label: 'fs.renameSync' },
36
+ ];
37
+
38
+ // Files to scan (source files only, not tests)
39
+ const TARGET_FILES = ['ideas.cjs', 'commands.cjs', 'jobs.cjs', 'specs.cjs'];
40
+
41
+ /**
42
+ * Scan a file for lines containing both a state directory pattern AND a move operation.
43
+ * Returns violations as { line: number, text: string, stateDir: string, operation: string }.
44
+ */
45
+ function scanForViolations(filePath) {
46
+ const content = fs.readFileSync(filePath, 'utf-8');
47
+ const lines = content.split('\n');
48
+ const violations = [];
49
+
50
+ for (let i = 0; i < lines.length; i++) {
51
+ const line = lines[i];
52
+
53
+ // Skip comments
54
+ if (line.trim().startsWith('//') || line.trim().startsWith('*')) continue;
55
+
56
+ // Check each move pattern
57
+ for (const { pattern: movePattern, label: moveLabel } of MOVE_PATTERNS) {
58
+ if (!movePattern.test(line)) continue;
59
+
60
+ // Check if this line or nearby lines reference state directories
61
+ // Check a window of +/-5 lines for state directory context
62
+ const windowStart = Math.max(0, i - 5);
63
+ const windowEnd = Math.min(lines.length - 1, i + 5);
64
+ const window = lines.slice(windowStart, windowEnd + 1).join('\n');
65
+
66
+ for (const statePattern of STATE_DIR_PATTERNS) {
67
+ if (statePattern.test(window)) {
68
+ violations.push({
69
+ line: i + 1,
70
+ text: line.trim(),
71
+ stateDir: statePattern.source,
72
+ operation: moveLabel,
73
+ });
74
+ break; // One violation per line is enough
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ return violations;
81
+ }
82
+
83
+ describe('state transition gate: no file moves for state changes', () => {
84
+
85
+ for (const fileName of TARGET_FILES) {
86
+ it(`GATE: zero file-move state transitions in ${fileName}`, () => {
87
+ const filePath = path.join(LIB_DIR, fileName);
88
+
89
+ if (!fs.existsSync(filePath)) {
90
+ // File doesn't exist — not a violation, just skip
91
+ return;
92
+ }
93
+
94
+ const violations = scanForViolations(filePath);
95
+
96
+ if (violations.length > 0) {
97
+ const details = violations.map(v =>
98
+ ` Line ${v.line}: ${v.operation} near ${v.stateDir}\n ${v.text}`
99
+ ).join('\n');
100
+
101
+ assert.fail(
102
+ `Found ${violations.length} file-move state transition(s) in ${fileName}:\n${details}\n\n` +
103
+ 'All state transitions must use frontmatter edits (setIdeaStatus, setTodoStatus, setJobStatus).\n' +
104
+ 'Replace git mv / renameSync with the appropriate set*Status helper.'
105
+ );
106
+ }
107
+
108
+ assert.equal(violations.length, 0,
109
+ `${fileName} should have zero file-move state transitions`);
110
+ });
111
+ }
112
+
113
+ it('GATE: cmdTodoComplete has no unlinkSync (copy+delete pattern)', () => {
114
+ const filePath = path.join(LIB_DIR, 'commands.cjs');
115
+ if (!fs.existsSync(filePath)) return;
116
+
117
+ const content = fs.readFileSync(filePath, 'utf-8');
118
+
119
+ // Find cmdTodoComplete function body
120
+ const fnStart = content.indexOf('function cmdTodoComplete');
121
+ if (fnStart === -1) return; // Function not found
122
+
123
+ // Find next function definition or module.exports as boundary
124
+ const afterFn = content.slice(fnStart + 1);
125
+ const fnEnd = afterFn.search(/\nfunction\s|\nmodule\.exports/);
126
+ const fnBody = fnEnd > 0 ? afterFn.slice(0, fnEnd) : afterFn;
127
+
128
+ if (fnBody.includes('unlinkSync')) {
129
+ assert.fail(
130
+ 'cmdTodoComplete contains unlinkSync — this is a copy+delete state transition pattern.\n' +
131
+ 'Use setTodoStatus to edit frontmatter instead of copying and deleting files.'
132
+ );
133
+ }
134
+ });
135
+
136
+ it('GATE: cmdSpecsFinalize has no research doc moving', () => {
137
+ const filePath = path.join(LIB_DIR, 'specs.cjs');
138
+ if (!fs.existsSync(filePath)) return;
139
+
140
+ const content = fs.readFileSync(filePath, 'utf-8');
141
+
142
+ // Find cmdSpecsFinalize function body
143
+ const fnStart = content.indexOf('function cmdSpecsFinalize');
144
+ if (fnStart === -1) return;
145
+
146
+ const afterFn = content.slice(fnStart + 1);
147
+ const fnEnd = afterFn.search(/\nfunction\s|\nmodule\.exports/);
148
+ const fnBody = fnEnd > 0 ? afterFn.slice(0, fnEnd) : afterFn;
149
+
150
+ // Check for research doc directory references
151
+ const hasResearchDocMove = /docs\/ideas\/(pending|done|rejected|consolidated)/.test(fnBody);
152
+
153
+ if (hasResearchDocMove) {
154
+ assert.fail(
155
+ 'cmdSpecsFinalize still references research doc state directories.\n' +
156
+ 'Research doc flattening is handled by Phase 134 — remove from finalize.'
157
+ );
158
+ }
159
+ });
160
+ });
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { loadConfig, getMilestoneInfo, output, error, getProjectRoot } = require('./core.cjs');
7
+ const { loadConfig, getMilestoneInfo, getMilestonePhaseFilter, output, error, getProjectRoot } = require('./core.cjs');
8
8
  const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
9
9
  const { getPlanningRoot } = require('./paths.cjs');
10
10
 
@@ -69,9 +69,6 @@ function cmdStateLoad(cwd, raw) {
69
69
  const lines = [
70
70
  `model_profile=${c.model_profile}`,
71
71
  `commit_docs=${c.commit_docs}`,
72
- `branching_strategy=${c.branching_strategy}`,
73
- `phase_branch_template=${c.phase_branch_template}`,
74
- `milestone_branch_template=${c.milestone_branch_template}`,
75
72
  `base_branch=${c.base_branch}`,
76
73
  `parallelization=${c.parallelization}`,
77
74
  `research=${c.research}`,
@@ -282,11 +279,19 @@ function cmdStateRecordMetric(cwd, options, raw) {
282
279
  }
283
280
  }
284
281
 
285
- function cmdStateUpdateProgress(cwd, raw) {
282
+ /**
283
+ * Internal helper: recalculates the progress bar from disk and updates STATE.md.
284
+ * Returns { updated, percent, completed, total, bar, reason, statePath, rawValue }
285
+ * WITHOUT calling output()/exit. Callers handle output emission themselves.
286
+ * `rawValue` is the string emitted by the CLI in raw mode (progressStr or 'false').
287
+ */
288
+ function stateUpdateProgressInternal(cwd) {
286
289
  let projectRoot;
287
290
  try { projectRoot = getProjectRoot(cwd); } catch { projectRoot = path.relative(cwd, getPlanningRoot(cwd)) || '.'; }
288
291
  const statePath = path.join(cwd, projectRoot, 'STATE.md');
289
- if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
292
+ if (!fs.existsSync(statePath)) {
293
+ return { result: { error: 'STATE.md not found' }, statePath, rawValue: undefined };
294
+ }
290
295
 
291
296
  let content = fs.readFileSync(statePath, 'utf-8');
292
297
 
@@ -317,13 +322,35 @@ function cmdStateUpdateProgress(cwd, raw) {
317
322
  if (boldProgressPattern.test(content)) {
318
323
  content = content.replace(boldProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
319
324
  writeStateMd(statePath, content, cwd);
320
- output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
325
+ return {
326
+ result: { updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr },
327
+ statePath,
328
+ rawValue: progressStr,
329
+ };
321
330
  } else if (plainProgressPattern.test(content)) {
322
331
  content = content.replace(plainProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
323
332
  writeStateMd(statePath, content, cwd);
324
- output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
333
+ return {
334
+ result: { updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr },
335
+ statePath,
336
+ rawValue: progressStr,
337
+ };
325
338
  } else {
326
- output({ updated: false, reason: 'Progress field not found in STATE.md' }, raw, 'false');
339
+ return {
340
+ result: { updated: false, reason: 'Progress field not found in STATE.md' },
341
+ statePath,
342
+ rawValue: 'false',
343
+ };
344
+ }
345
+ }
346
+
347
+ function cmdStateUpdateProgress(cwd, raw) {
348
+ const { result, rawValue } = stateUpdateProgressInternal(cwd);
349
+ if (rawValue === undefined) {
350
+ // STATE.md missing branch — original used `output({ error: ... }, raw)`
351
+ output(result, raw);
352
+ } else {
353
+ output(result, raw, rawValue);
327
354
  }
328
355
  }
329
356
 
@@ -564,40 +591,6 @@ function cmdStateSnapshot(cwd, raw) {
564
591
 
565
592
  // ─── State Frontmatter Sync ──────────────────────────────────────────────────
566
593
 
567
- /**
568
- * Build a milestone phase filter from ROADMAP.md.
569
- * Returns a function that checks if a phase directory belongs to the current milestone.
570
- */
571
- function getMilestonePhaseFilter(cwd) {
572
- const milestonePhaseNums = new Set();
573
- try {
574
- const roadmap = fs.readFileSync(path.join(getPlanningRoot(cwd), 'ROADMAP.md'), 'utf-8');
575
- const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
576
- let m;
577
- while ((m = phasePattern.exec(roadmap)) !== null) {
578
- milestonePhaseNums.add(m[1]);
579
- }
580
- } catch {}
581
-
582
- if (milestonePhaseNums.size === 0) {
583
- const passAll = () => true;
584
- passAll.phaseCount = 0;
585
- return passAll;
586
- }
587
-
588
- const normalized = new Set(
589
- [...milestonePhaseNums].map(n => (n.replace(/^0+/, '') || '0').toLowerCase())
590
- );
591
-
592
- function isDirInMilestone(dirName) {
593
- const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
594
- if (!m) return false;
595
- return normalized.has(m[1].toLowerCase());
596
- }
597
- isDirInMilestone.phaseCount = milestonePhaseNums.size;
598
- return isDirInMilestone;
599
- }
600
-
601
594
  /**
602
595
  * Extract machine-readable fields from STATE.md markdown body and build
603
596
  * a YAML frontmatter object. Allows hooks and scripts to read state
@@ -632,7 +625,13 @@ function buildStateFrontmatter(bodyContent, cwd) {
632
625
 
633
626
  if (cwd) {
634
627
  try {
635
- const phasesDir = path.join(getPlanningRoot(cwd), 'phases');
628
+ let phasesDir;
629
+ try {
630
+ const projectRoot = getProjectRoot(cwd);
631
+ phasesDir = path.join(cwd, projectRoot, 'phases');
632
+ } catch {
633
+ phasesDir = path.join(getPlanningRoot(cwd), 'phases');
634
+ }
636
635
  if (fs.existsSync(phasesDir)) {
637
636
  const isDirInMilestone = getMilestonePhaseFilter(cwd);
638
637
  const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
@@ -679,7 +678,10 @@ function buildStateFrontmatter(bodyContent, cwd) {
679
678
  normalizedStatus = 'discussing';
680
679
  } else if (statusLower.includes('verif')) {
681
680
  normalizedStatus = 'verifying';
682
- } else if (statusLower.includes('complete') || statusLower.includes('done')) {
681
+ } else if (statusLower === 'completed' || statusLower === 'done' || statusLower === 'project completed') {
682
+ // Only exact matches trigger 'completed' — "Phase X execution complete" or
683
+ // "Milestone shipped" should NOT mark the project as completed.
684
+ // Project completion is a manual action via /dgs:complete-project.
683
685
  normalizedStatus = 'completed';
684
686
  } else if (statusLower.includes('ready to execute')) {
685
687
  normalizedStatus = 'executing';
@@ -751,11 +753,12 @@ function cmdStateJson(cwd, raw) {
751
753
 
752
754
  // ─── Quick Task Archival ─────────────────────────────────────────────────────
753
755
 
754
- function cmdStateArchiveQuickTasks(cwd, raw) {
755
- const statePath = resolveStatePath(cwd);
756
+ // Internal helper: archive the Quick Tasks Completed section from a single
757
+ // STATE.md file. Returns a status object describing the outcome; does not
758
+ // emit output directly so the caller can aggregate across multiple files.
759
+ function _archiveSingleStateFile(statePath, cwd) {
756
760
  if (!fs.existsSync(statePath)) {
757
- output({ error: 'STATE.md not found' }, raw);
758
- return;
761
+ return { archived: false, state_path: statePath, reason: 'not_found', row_count: 0 };
759
762
  }
760
763
 
761
764
  let content = fs.readFileSync(statePath, 'utf-8');
@@ -765,8 +768,7 @@ function cmdStateArchiveQuickTasks(cwd, raw) {
765
768
  const sectionMatch = content.match(sectionPattern);
766
769
 
767
770
  if (!sectionMatch) {
768
- output({ archived: false, reason: 'no_section', row_count: 0 }, raw);
769
- return;
771
+ return { archived: false, state_path: statePath, reason: 'no_section', row_count: 0 };
770
772
  }
771
773
 
772
774
  const sectionBody = sectionMatch[2];
@@ -800,8 +802,7 @@ function cmdStateArchiveQuickTasks(cwd, raw) {
800
802
  }
801
803
 
802
804
  if (dataRows.length <= 20) {
803
- output({ archived: false, reason: 'under_threshold', row_count: dataRows.length }, raw);
804
- return;
805
+ return { archived: false, state_path: statePath, reason: 'under_threshold', row_count: dataRows.length };
805
806
  }
806
807
 
807
808
  // Archive down to 15 rows (buffer below the 20 threshold)
@@ -818,7 +819,8 @@ function cmdStateArchiveQuickTasks(cwd, raw) {
818
819
  content = content.replace(sectionPattern, (_match, header) => `${header}${rebuiltSection}`);
819
820
  writeStateMd(statePath, content, cwd);
820
821
 
821
- // Build HISTORY.md path
822
+ // Build HISTORY.md path — naturally lands beside the STATE.md being archived
823
+ // (product → planning-root quick/HISTORY.md; project → projects/<p>/quick/HISTORY.md).
822
824
  const historyPath = path.join(path.dirname(statePath), 'quick', 'HISTORY.md');
823
825
 
824
826
  // Ensure quick/ directory exists
@@ -869,7 +871,94 @@ Archived quick/fast task records from STATE.md.
869
871
 
870
872
  fs.writeFileSync(historyPath, historyContent, 'utf-8');
871
873
 
872
- output({ archived: true, count: archiveCount, remaining: 15, history_path: historyPath }, raw);
874
+ return { archived: true, state_path: statePath, count: archiveCount, remaining: 15, history_path: historyPath };
875
+ }
876
+
877
+ // Dual-scan archival: quick tasks can land in either the product-level
878
+ // STATE.md (planning root, used by `/dgs:quick --main` and product-mode
879
+ // quicks) or the project-level STATE.md (milestone-context quicks). Both
880
+ // files can accumulate tracking rows independently, so archival has to
881
+ // scan both and archive each over-threshold section into its own
882
+ // sibling quick/HISTORY.md file.
883
+ function cmdStateArchiveQuickTasks(cwd, raw) {
884
+ const productStatePath = path.join(getPlanningRoot(cwd), 'STATE.md');
885
+ const projectStatePath = resolveStatePath(cwd);
886
+
887
+ // Deduplicate — root layout without a project subdir resolves both to the
888
+ // same file, so only scan once to avoid double-processing.
889
+ const candidates = [productStatePath];
890
+ if (path.resolve(projectStatePath) !== path.resolve(productStatePath)) {
891
+ candidates.push(projectStatePath);
892
+ }
893
+
894
+ const results = candidates.map((p) => _archiveSingleStateFile(p, cwd));
895
+ const anyArchived = results.some((r) => r.archived);
896
+
897
+ output({ archived: anyArchived, results, scanned: candidates }, raw);
898
+ }
899
+
900
+ // ─── Milestone Completion ────────────────────────────────────────────────────
901
+
902
+ /**
903
+ * Mark the current milestone as complete in STATE.md.
904
+ * Updates frontmatter status to "complete", progress.percent to 100,
905
+ * last_updated to current ISO timestamp, and adds completed_date.
906
+ *
907
+ * Called by the complete-milestone workflow after all code repos have
908
+ * successfully merged.
909
+ *
910
+ * @param {string} cwd - Planning root directory
911
+ * @returns {{ success: boolean, milestone: string, completed_date: string }}
912
+ */
913
+ function markMilestoneComplete(cwd) {
914
+ const statePath = resolveStatePath(cwd);
915
+ if (!fs.existsSync(statePath)) {
916
+ error('STATE.md not found');
917
+ }
918
+
919
+ let content = fs.readFileSync(statePath, 'utf-8');
920
+ const fm = extractFrontmatter(content);
921
+ const milestone = fm.milestone || 'unknown';
922
+ const today = new Date().toISOString().split('T')[0];
923
+ const now = new Date().toISOString();
924
+
925
+ // Update frontmatter fields — milestone complete, NOT project complete
926
+ // Project completion is a separate manual action via /dgs:complete-project
927
+ fm.status = 'milestone_shipped';
928
+ if (!fm.progress) fm.progress = {};
929
+ fm.progress.percent = 100;
930
+ fm.last_updated = now;
931
+ fm.completed_date = today;
932
+
933
+ // Reconstruct frontmatter and preserve body
934
+ let body = content.replace(/^---\n[\s\S]*?\n---\n*/, '');
935
+
936
+ // Update markdown body to reflect completion
937
+ const milestoneName = fm.milestone_name || fm.milestone || 'unknown';
938
+ const totalPhases = (fm.progress && fm.progress.total_phases) || '?';
939
+ const totalPlans = (fm.progress && fm.progress.total_plans) || '?';
940
+ const lastPhase = (fm.progress && fm.progress.completed_phases) || totalPhases;
941
+
942
+ // Update progress bar
943
+ body = body.replace(/Progress:\s*\[[^\]]*\]\s*\d+%/, 'Progress: [██████████] 100%');
944
+ // Update current focus
945
+ body = body.replace(/\*\*Current focus:\*\*\s*.+/, `**Current focus:** Milestone ${milestone} complete — shipped ${today}`);
946
+ // Update status line in Current Position
947
+ body = body.replace(/Status:\s*.+/, `Status: Milestone ${milestone} shipped ${today}`);
948
+ // Update last activity in Current Position
949
+ body = body.replace(/(Last activity:\s*).+/, `$1${today} -- Milestone ${milestone} shipped (${totalPhases} phases, ${totalPlans} plans)`);
950
+
951
+ const yamlStr = reconstructFrontmatter(fm);
952
+ content = `---\n${yamlStr}\n---\n\n${body}`;
953
+
954
+ fs.writeFileSync(statePath, content, 'utf-8');
955
+
956
+ return { success: true, milestone: milestone, completed_date: today };
957
+ }
958
+
959
+ function cmdMarkMilestoneComplete(cwd, raw) {
960
+ const result = markMilestoneComplete(cwd);
961
+ output(result, raw);
873
962
  }
874
963
 
875
964
  module.exports = {
@@ -883,6 +972,7 @@ module.exports = {
883
972
  cmdStateAdvancePlan,
884
973
  cmdStateRecordMetric,
885
974
  cmdStateUpdateProgress,
975
+ stateUpdateProgressInternal,
886
976
  cmdStateAddDecision,
887
977
  cmdStateAddBlocker,
888
978
  cmdStateResolveBlocker,
@@ -890,4 +980,6 @@ module.exports = {
890
980
  cmdStateSnapshot,
891
981
  cmdStateJson,
892
982
  cmdStateArchiveQuickTasks,
983
+ markMilestoneComplete,
984
+ cmdMarkMilestoneComplete,
893
985
  };