@ktpartners/dgs-platform 2.8.0 → 3.0.4

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 (94) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +41 -13
  3. package/agents/dgs-plan-checker.md +29 -3
  4. package/agents/dgs-planner.md +10 -0
  5. package/commands/dgs/abandon-quick.md +28 -0
  6. package/commands/dgs/add-tests.md +2 -2
  7. package/commands/dgs/audit-milestone.md +2 -2
  8. package/commands/dgs/capture-principle.md +11 -11
  9. package/commands/dgs/cleanup.md +2 -2
  10. package/commands/dgs/complete-milestone.md +11 -11
  11. package/commands/dgs/complete-quick.md +28 -0
  12. package/commands/dgs/create-milestone-job.md +2 -2
  13. package/commands/dgs/debug.md +3 -3
  14. package/commands/dgs/develop-idea.md +1 -1
  15. package/commands/dgs/fast.md +3 -1
  16. package/commands/dgs/health.md +1 -1
  17. package/commands/dgs/map-codebase.md +6 -6
  18. package/commands/dgs/new-milestone.md +5 -5
  19. package/commands/dgs/new-project.md +6 -6
  20. package/commands/dgs/plan-milestone-gaps.md +1 -1
  21. package/commands/dgs/progress.md +3 -3
  22. package/commands/dgs/quick-abandon.md +8 -0
  23. package/commands/dgs/quick-complete.md +8 -0
  24. package/commands/dgs/quick.md +10 -3
  25. package/commands/dgs/research-idea.md +2 -2
  26. package/commands/dgs/research-phase.md +3 -3
  27. package/commands/dgs/switch-project.md +1 -1
  28. package/commands/dgs/write-spec.md +3 -3
  29. package/deliver-great-systems/bin/dgs-tools.cjs +284 -30
  30. package/deliver-great-systems/bin/lib/commands.cjs +316 -31
  31. package/deliver-great-systems/bin/lib/commands.test.cjs +336 -0
  32. package/deliver-great-systems/bin/lib/config.cjs +39 -6
  33. package/deliver-great-systems/bin/lib/context.cjs +120 -0
  34. package/deliver-great-systems/bin/lib/core.cjs +28 -11
  35. package/deliver-great-systems/bin/lib/execution.cjs +49 -17
  36. package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
  37. package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
  38. package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
  39. package/deliver-great-systems/bin/lib/init.cjs +306 -39
  40. package/deliver-great-systems/bin/lib/init.test.cjs +416 -6
  41. package/deliver-great-systems/bin/lib/jobs.cjs +124 -21
  42. package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
  43. package/deliver-great-systems/bin/lib/migration.cjs +409 -1
  44. package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
  45. package/deliver-great-systems/bin/lib/milestone.cjs +54 -29
  46. package/deliver-great-systems/bin/lib/phase.cjs +128 -2
  47. package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
  48. package/deliver-great-systems/bin/lib/projects.cjs +28 -8
  49. package/deliver-great-systems/bin/lib/projects.test.cjs +86 -0
  50. package/deliver-great-systems/bin/lib/quick.cjs +584 -0
  51. package/deliver-great-systems/bin/lib/quick.test.cjs +596 -0
  52. package/deliver-great-systems/bin/lib/repos.cjs +25 -1
  53. package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
  54. package/deliver-great-systems/bin/lib/specs.cjs +3 -81
  55. package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
  56. package/deliver-great-systems/bin/lib/state.cjs +142 -54
  57. package/deliver-great-systems/bin/lib/sync.cjs +75 -0
  58. package/deliver-great-systems/bin/lib/verify.cjs +80 -1
  59. package/deliver-great-systems/bin/lib/worktrees.cjs +764 -0
  60. package/deliver-great-systems/bin/lib/worktrees.test.cjs +887 -0
  61. package/deliver-great-systems/templates/claude-md.md +16 -0
  62. package/deliver-great-systems/workflows/abandon-quick.md +89 -0
  63. package/deliver-great-systems/workflows/add-idea.md +3 -3
  64. package/deliver-great-systems/workflows/add-tests.md +14 -0
  65. package/deliver-great-systems/workflows/add-todo.md +1 -0
  66. package/deliver-great-systems/workflows/approve-spec.md +25 -4
  67. package/deliver-great-systems/workflows/audit-phase.md +15 -5
  68. package/deliver-great-systems/workflows/cancel-job.md +1 -1
  69. package/deliver-great-systems/workflows/check-todos.md +2 -3
  70. package/deliver-great-systems/workflows/complete-milestone.md +197 -22
  71. package/deliver-great-systems/workflows/complete-quick.md +68 -0
  72. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  73. package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
  74. package/deliver-great-systems/workflows/develop-idea.md +11 -11
  75. package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
  76. package/deliver-great-systems/workflows/discuss-idea.md +1 -1
  77. package/deliver-great-systems/workflows/execute-phase.md +121 -32
  78. package/deliver-great-systems/workflows/execute-plan.md +12 -21
  79. package/deliver-great-systems/workflows/help.md +33 -29
  80. package/deliver-great-systems/workflows/init-product.md +2 -18
  81. package/deliver-great-systems/workflows/new-milestone.md +40 -24
  82. package/deliver-great-systems/workflows/new-project.md +22 -680
  83. package/deliver-great-systems/workflows/progress-all.md +133 -0
  84. package/deliver-great-systems/workflows/quick-abandon.md +89 -0
  85. package/deliver-great-systems/workflows/quick-complete.md +68 -0
  86. package/deliver-great-systems/workflows/quick.md +152 -23
  87. package/deliver-great-systems/workflows/refine-spec.md +1 -1
  88. package/deliver-great-systems/workflows/research-idea.md +8 -8
  89. package/deliver-great-systems/workflows/resume-project.md +2 -2
  90. package/deliver-great-systems/workflows/run-job.md +8 -8
  91. package/deliver-great-systems/workflows/validate-phase.md +39 -1
  92. package/deliver-great-systems/workflows/verify-work.md +14 -0
  93. package/deliver-great-systems/workflows/write-spec.md +2 -2
  94. package/package.json +1 -1
@@ -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 })
@@ -751,11 +750,12 @@ function cmdStateJson(cwd, raw) {
751
750
 
752
751
  // ─── Quick Task Archival ─────────────────────────────────────────────────────
753
752
 
754
- function cmdStateArchiveQuickTasks(cwd, raw) {
755
- const statePath = resolveStatePath(cwd);
753
+ // Internal helper: archive the Quick Tasks Completed section from a single
754
+ // STATE.md file. Returns a status object describing the outcome; does not
755
+ // emit output directly so the caller can aggregate across multiple files.
756
+ function _archiveSingleStateFile(statePath, cwd) {
756
757
  if (!fs.existsSync(statePath)) {
757
- output({ error: 'STATE.md not found' }, raw);
758
- return;
758
+ return { archived: false, state_path: statePath, reason: 'not_found', row_count: 0 };
759
759
  }
760
760
 
761
761
  let content = fs.readFileSync(statePath, 'utf-8');
@@ -765,8 +765,7 @@ function cmdStateArchiveQuickTasks(cwd, raw) {
765
765
  const sectionMatch = content.match(sectionPattern);
766
766
 
767
767
  if (!sectionMatch) {
768
- output({ archived: false, reason: 'no_section', row_count: 0 }, raw);
769
- return;
768
+ return { archived: false, state_path: statePath, reason: 'no_section', row_count: 0 };
770
769
  }
771
770
 
772
771
  const sectionBody = sectionMatch[2];
@@ -800,8 +799,7 @@ function cmdStateArchiveQuickTasks(cwd, raw) {
800
799
  }
801
800
 
802
801
  if (dataRows.length <= 20) {
803
- output({ archived: false, reason: 'under_threshold', row_count: dataRows.length }, raw);
804
- return;
802
+ return { archived: false, state_path: statePath, reason: 'under_threshold', row_count: dataRows.length };
805
803
  }
806
804
 
807
805
  // Archive down to 15 rows (buffer below the 20 threshold)
@@ -818,7 +816,8 @@ function cmdStateArchiveQuickTasks(cwd, raw) {
818
816
  content = content.replace(sectionPattern, (_match, header) => `${header}${rebuiltSection}`);
819
817
  writeStateMd(statePath, content, cwd);
820
818
 
821
- // Build HISTORY.md path
819
+ // Build HISTORY.md path — naturally lands beside the STATE.md being archived
820
+ // (product → planning-root quick/HISTORY.md; project → projects/<p>/quick/HISTORY.md).
822
821
  const historyPath = path.join(path.dirname(statePath), 'quick', 'HISTORY.md');
823
822
 
824
823
  // Ensure quick/ directory exists
@@ -869,7 +868,93 @@ Archived quick/fast task records from STATE.md.
869
868
 
870
869
  fs.writeFileSync(historyPath, historyContent, 'utf-8');
871
870
 
872
- output({ archived: true, count: archiveCount, remaining: 15, history_path: historyPath }, raw);
871
+ return { archived: true, state_path: statePath, count: archiveCount, remaining: 15, history_path: historyPath };
872
+ }
873
+
874
+ // Dual-scan archival: quick tasks can land in either the product-level
875
+ // STATE.md (planning root, used by `/dgs:quick --main` and product-mode
876
+ // quicks) or the project-level STATE.md (milestone-context quicks). Both
877
+ // files can accumulate tracking rows independently, so archival has to
878
+ // scan both and archive each over-threshold section into its own
879
+ // sibling quick/HISTORY.md file.
880
+ function cmdStateArchiveQuickTasks(cwd, raw) {
881
+ const productStatePath = path.join(getPlanningRoot(cwd), 'STATE.md');
882
+ const projectStatePath = resolveStatePath(cwd);
883
+
884
+ // Deduplicate — root layout without a project subdir resolves both to the
885
+ // same file, so only scan once to avoid double-processing.
886
+ const candidates = [productStatePath];
887
+ if (path.resolve(projectStatePath) !== path.resolve(productStatePath)) {
888
+ candidates.push(projectStatePath);
889
+ }
890
+
891
+ const results = candidates.map((p) => _archiveSingleStateFile(p, cwd));
892
+ const anyArchived = results.some((r) => r.archived);
893
+
894
+ output({ archived: anyArchived, results, scanned: candidates }, raw);
895
+ }
896
+
897
+ // ─── Milestone Completion ────────────────────────────────────────────────────
898
+
899
+ /**
900
+ * Mark the current milestone as complete in STATE.md.
901
+ * Updates frontmatter status to "complete", progress.percent to 100,
902
+ * last_updated to current ISO timestamp, and adds completed_date.
903
+ *
904
+ * Called by the complete-milestone workflow after all code repos have
905
+ * successfully merged.
906
+ *
907
+ * @param {string} cwd - Planning root directory
908
+ * @returns {{ success: boolean, milestone: string, completed_date: string }}
909
+ */
910
+ function markMilestoneComplete(cwd) {
911
+ const statePath = resolveStatePath(cwd);
912
+ if (!fs.existsSync(statePath)) {
913
+ error('STATE.md not found');
914
+ }
915
+
916
+ let content = fs.readFileSync(statePath, 'utf-8');
917
+ const fm = extractFrontmatter(content);
918
+ const milestone = fm.milestone || 'unknown';
919
+ const today = new Date().toISOString().split('T')[0];
920
+ const now = new Date().toISOString();
921
+
922
+ // Update frontmatter fields
923
+ fm.status = 'complete';
924
+ if (!fm.progress) fm.progress = {};
925
+ fm.progress.percent = 100;
926
+ fm.last_updated = now;
927
+ fm.completed_date = today;
928
+
929
+ // Reconstruct frontmatter and preserve body
930
+ let body = content.replace(/^---\n[\s\S]*?\n---\n*/, '');
931
+
932
+ // Update markdown body to reflect completion
933
+ const milestoneName = fm.milestone_name || fm.milestone || 'unknown';
934
+ const totalPhases = (fm.progress && fm.progress.total_phases) || '?';
935
+ const totalPlans = (fm.progress && fm.progress.total_plans) || '?';
936
+ const lastPhase = (fm.progress && fm.progress.completed_phases) || totalPhases;
937
+
938
+ // Update progress bar
939
+ body = body.replace(/Progress:\s*\[[^\]]*\]\s*\d+%/, 'Progress: [██████████] 100%');
940
+ // Update current focus
941
+ body = body.replace(/\*\*Current focus:\*\*\s*.+/, `**Current focus:** Milestone ${milestone} complete — shipped ${today}`);
942
+ // Update status line in Current Position
943
+ body = body.replace(/Status:\s*.+/, `Status: Milestone ${milestone} shipped ${today}`);
944
+ // Update last activity in Current Position
945
+ body = body.replace(/(Last activity:\s*).+/, `$1${today} -- Milestone ${milestone} shipped (${totalPhases} phases, ${totalPlans} plans)`);
946
+
947
+ const yamlStr = reconstructFrontmatter(fm);
948
+ content = `---\n${yamlStr}\n---\n\n${body}`;
949
+
950
+ fs.writeFileSync(statePath, content, 'utf-8');
951
+
952
+ return { success: true, milestone: milestone, completed_date: today };
953
+ }
954
+
955
+ function cmdMarkMilestoneComplete(cwd, raw) {
956
+ const result = markMilestoneComplete(cwd);
957
+ output(result, raw);
873
958
  }
874
959
 
875
960
  module.exports = {
@@ -883,6 +968,7 @@ module.exports = {
883
968
  cmdStateAdvancePlan,
884
969
  cmdStateRecordMetric,
885
970
  cmdStateUpdateProgress,
971
+ stateUpdateProgressInternal,
886
972
  cmdStateAddDecision,
887
973
  cmdStateAddBlocker,
888
974
  cmdStateResolveBlocker,
@@ -890,4 +976,6 @@ module.exports = {
890
976
  cmdStateSnapshot,
891
977
  cmdStateJson,
892
978
  cmdStateArchiveQuickTasks,
979
+ markMilestoneComplete,
980
+ cmdMarkMilestoneComplete,
893
981
  };