@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,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, toPosixPath, output, error, getProjectRoot } = require('./core.cjs');
7
+ const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, toPosixPath, output, error, getProjectRoot, loadConfig, execGit } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
9
  const { writeStateMd } = require('./state.cjs');
10
10
 
@@ -381,6 +381,17 @@ function cmdPhaseAdd(cwd, description, raw) {
381
381
  updatedContent = content + phaseEntry;
382
382
  }
383
383
 
384
+ // Update milestone summary range (e.g., "Phases 124-128" -> "Phases 124-129")
385
+ const rangePattern = /Phases\s+(\d+)-(\d+)\s*\(in progress\)/;
386
+ const rangeMatch = updatedContent.match(rangePattern);
387
+ if (rangeMatch) {
388
+ const rangeEnd = parseInt(rangeMatch[2], 10);
389
+ if (newPhaseNum > rangeEnd) {
390
+ updatedContent = updatedContent.replace(rangePattern,
391
+ `Phases ${rangeMatch[1]}-${newPhaseNum} (in progress)`);
392
+ }
393
+ }
394
+
384
395
  fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
385
396
 
386
397
  const relPhasesDir = path.relative(cwd, phasesDir);
@@ -732,7 +743,16 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
732
743
  output(result, raw);
733
744
  }
734
745
 
735
- function cmdPhaseComplete(cwd, phaseNum, raw) {
746
+ /**
747
+ * Internal helper: executes the phase-complete logic (ROADMAP/STATE/REQUIREMENTS
748
+ * updates + next-phase lookup) WITHOUT emitting output/exit.
749
+ *
750
+ * Returns { result, roadmapPath, statePath, requirementsPath, phaseDir }.
751
+ * The `result` object matches what cmdPhaseComplete emits. Paths are absolute.
752
+ * `requirementsPath` is returned even when the file doesn't exist (caller decides).
753
+ * `phaseDir` is the absolute path to the phase directory on disk.
754
+ */
755
+ function phaseCompleteInternal(cwd, phaseNum) {
736
756
  if (!phaseNum) {
737
757
  error('phase number required for phase complete');
738
758
  }
@@ -740,6 +760,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
740
760
  const projRoot = resolveProjectRoot(cwd);
741
761
  const roadmapPath = path.join(cwd, projRoot, 'ROADMAP.md');
742
762
  const statePath = path.join(cwd, projRoot, 'STATE.md');
763
+ const requirementsPath = path.join(cwd, projRoot, 'REQUIREMENTS.md');
743
764
  const phasesDir = resolvePhasesDir(cwd);
744
765
  const normalized = normalizePhaseName(phaseNum);
745
766
  const today = new Date().toISOString().split('T')[0];
@@ -750,6 +771,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
750
771
  error(`Phase ${phaseNum} not found`);
751
772
  }
752
773
 
774
+ // Absolute phase directory (phaseInfo.directory is relative to cwd)
775
+ const phaseDir = phaseInfo.directory
776
+ ? path.join(cwd, phaseInfo.directory)
777
+ : null;
778
+
753
779
  const planCount = phaseInfo.plans.length;
754
780
  const summaryCount = phaseInfo.summaries.length;
755
781
 
@@ -791,8 +817,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
791
817
  const reqPath = path.join(cwd, projRoot, 'REQUIREMENTS.md');
792
818
  if (fs.existsSync(reqPath)) {
793
819
  // Extract Requirements line from roadmap for this phase
820
+ // Match both **Requirements:** (colon inside bold) and **Requirements**: (colon outside bold)
794
821
  const reqMatch = roadmapContent.match(
795
- new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
822
+ new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements(?::\\*\\*|\\*\\*:)\\s*([^\\n]+)`, 'i')
796
823
  );
797
824
 
798
825
  if (reqMatch) {
@@ -896,6 +923,120 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
896
923
  state_updated: fs.existsSync(statePath),
897
924
  };
898
925
 
926
+ return { result, roadmapPath, statePath, requirementsPath, phaseDir };
927
+ }
928
+
929
+ /**
930
+ * CLI: `phase complete <phase>`. Thin wrapper over phaseCompleteInternal that
931
+ * emits the result and exits. External behavior unchanged from prior versions.
932
+ */
933
+ function cmdPhaseComplete(cwd, phaseNum, raw) {
934
+ const { result } = phaseCompleteInternal(cwd, phaseNum);
935
+ output(result, raw);
936
+ }
937
+
938
+ /**
939
+ * CLI: `phase finalize <phase> [--push]`.
940
+ *
941
+ * Runs phaseCompleteInternal (ROADMAP/STATE/REQUIREMENTS updates) AND commits
942
+ * the modified tracking files + any `*-VERIFICATION.md` files in the phase dir
943
+ * in a single atomic call. Respects config.commit_docs (skip commit if false;
944
+ * still writes files). Handles --push for sync_push=auto|prompt modes.
945
+ *
946
+ * Does NOT call cmdCommit (which exits). Uses execGit directly.
947
+ */
948
+ // Collect the list of still-dirty paths in `gitCwd` after a commit. Purely
949
+ // informational — populates `result.dirty_after`. Duplicated from
950
+ // commands.cjs/quick.cjs instead of extracted because the three call sites
951
+ // use slightly different cwd variables and a shared module would obscure that.
952
+ function collectDirtyAfter(gitCwd) {
953
+ const porcelain = execGit(gitCwd, ['status', '--porcelain']);
954
+ if (porcelain.exitCode !== 0) return [];
955
+ return (porcelain.stdout || '')
956
+ .split('\n')
957
+ .map(l => l.trim())
958
+ .filter(Boolean)
959
+ .map(l => l.replace(/^..\s+/, ''));
960
+ }
961
+
962
+ function cmdPhaseFinalize(cwd, phaseNum, options, raw) {
963
+ if (!phaseNum) {
964
+ error('phase number required for phase finalize');
965
+ }
966
+
967
+ const { result, roadmapPath, statePath, requirementsPath, phaseDir } =
968
+ phaseCompleteInternal(cwd, phaseNum);
969
+
970
+ // Gather tracking files that exist on disk (skip missing files gracefully)
971
+ const filesToStage = [];
972
+ for (const p of [roadmapPath, statePath, requirementsPath]) {
973
+ if (p && fs.existsSync(p)) {
974
+ filesToStage.push(path.relative(cwd, p));
975
+ }
976
+ }
977
+ // Include every *-VERIFICATION.md in the phase directory
978
+ if (phaseDir && fs.existsSync(phaseDir)) {
979
+ try {
980
+ const entries = fs.readdirSync(phaseDir).filter(f => /-VERIFICATION\.md$/i.test(f));
981
+ for (const f of entries) {
982
+ filesToStage.push(path.relative(cwd, path.join(phaseDir, f)));
983
+ }
984
+ } catch { /* ignore */ }
985
+ }
986
+
987
+ // Honor config.commit_docs — file writes already happened, just skip commit.
988
+ const config = loadConfig(cwd);
989
+ if (!config.commit_docs) {
990
+ result.committed = false;
991
+ result.commit_reason = 'skipped_commit_docs_false';
992
+ result.files_committed = [];
993
+ output(result, raw);
994
+ return;
995
+ }
996
+
997
+ // Stage + commit atomically (cmdCommit calls output/exit — inline instead).
998
+ for (const f of filesToStage) {
999
+ execGit(cwd, ['add', f]);
1000
+ }
1001
+ const message = `docs(phase-${phaseNum}): complete phase execution`;
1002
+ const commitResult = execGit(cwd, ['commit', '-m', message]);
1003
+ if (commitResult.exitCode !== 0) {
1004
+ const nothing =
1005
+ commitResult.stdout.includes('nothing to commit') ||
1006
+ commitResult.stderr.includes('nothing to commit');
1007
+ result.committed = false;
1008
+ result.commit_reason = nothing ? 'nothing_to_commit' : 'commit_failed';
1009
+ if (!nothing) result.commit_error = commitResult.stderr;
1010
+ result.files_committed = [];
1011
+ result.dirty_after = collectDirtyAfter(cwd);
1012
+ output(result, raw);
1013
+ return;
1014
+ }
1015
+ const hashResult = execGit(cwd, ['rev-parse', '--short', 'HEAD']);
1016
+ result.committed = true;
1017
+ result.hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
1018
+ result.commit_reason = 'committed';
1019
+ result.files_committed = filesToStage;
1020
+ result.dirty_after = collectDirtyAfter(cwd);
1021
+
1022
+ // Optional push (same semantics as cmdCommit)
1023
+ if (options && options.push) {
1024
+ const syncPush = config.sync_push || 'off';
1025
+ if (syncPush === 'auto') {
1026
+ try {
1027
+ const { pushAll } = require('./sync.cjs');
1028
+ const pushRes = pushAll(cwd, { force: true });
1029
+ result.pushed = pushRes.ok;
1030
+ result.push_result = pushRes;
1031
+ } catch (err) {
1032
+ result.pushed = false;
1033
+ result.push_result = { ok: false, error: err.message };
1034
+ }
1035
+ } else if (syncPush === 'prompt') {
1036
+ result.needs_push = true;
1037
+ }
1038
+ }
1039
+
899
1040
  output(result, raw);
900
1041
  }
901
1042
 
@@ -908,4 +1049,6 @@ module.exports = {
908
1049
  cmdPhaseInsert,
909
1050
  cmdPhaseRemove,
910
1051
  cmdPhaseComplete,
1052
+ cmdPhaseFinalize,
1053
+ phaseCompleteInternal,
911
1054
  };
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Tests for phase.cjs::cmdPhaseFinalize and commands.cjs::cmdPlanFinalize.
3
+ *
4
+ * Atomic "update tracking files + commit" behavior in a real temp git repo.
5
+ * Uses Node.js built-in test runner (node:test) and assert (node:assert).
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
+ const { execSync } = require('child_process');
13
+
14
+ const { createTempProject } = require('./test-helpers.cjs');
15
+
16
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Capture stdout output from CLI commands that call output() (process.stdout.write
20
+ * + process.exit). Mocks both so multiple CLI invocations can run in sequence.
21
+ * Returns { stdout, exitCode, json }.
22
+ */
23
+ function captureStdout(fn) {
24
+ const chunks = [];
25
+ const origWrite = process.stdout.write.bind(process.stdout);
26
+ const origExit = process.exit;
27
+ let exitCode = null;
28
+ process.stdout.write = (data) => { chunks.push(String(data)); return true; };
29
+ process.exit = (code) => {
30
+ exitCode = code == null ? 0 : code;
31
+ throw new Error('__EXIT__');
32
+ };
33
+ try {
34
+ fn();
35
+ } catch (e) {
36
+ if (e && e.message !== '__EXIT__') throw e;
37
+ } finally {
38
+ process.stdout.write = origWrite;
39
+ process.exit = origExit;
40
+ }
41
+ const stdout = chunks.join('');
42
+ let json = null;
43
+ try { json = JSON.parse(stdout); } catch { /* not JSON */ }
44
+ return { stdout, exitCode, json };
45
+ }
46
+
47
+ function gitLog(cwd) {
48
+ return execSync('git log --oneline', { cwd, encoding: 'utf-8' })
49
+ .trim().split('\n').filter(Boolean);
50
+ }
51
+
52
+ function gitShowFiles(cwd, ref) {
53
+ return execSync(`git show --name-only --format= ${ref}`, { cwd, encoding: 'utf-8' })
54
+ .trim().split('\n').filter(Boolean);
55
+ }
56
+
57
+ function gitLastMessage(cwd) {
58
+ return execSync('git log -1 --format=%s', { cwd, encoding: 'utf-8' }).trim();
59
+ }
60
+
61
+ /**
62
+ * Write a rich fixture atop createTempProject for Phase 01 finalize tests.
63
+ * Returns { cwd, cleanup } from createTempProject after adding ROADMAP, STATE,
64
+ * REQUIREMENTS, phase dir, and VERIFICATION.md — all committed to a clean tree.
65
+ */
66
+ function makePhaseFinalizeFixture() {
67
+ const fixture = createTempProject({ withGit: false, withPhases: false, withConfig: {} });
68
+
69
+ // Remove v2 markers so this is treated as a v1/root-layout project (phases at root)
70
+ fs.unlinkSync(path.join(fixture.cwd, 'PROJECTS.md'));
71
+ fs.unlinkSync(path.join(fixture.cwd, 'REPOS.md'));
72
+
73
+ // Richer ROADMAP.md with parseable Phase 01 entry + progress table row
74
+ fs.writeFileSync(path.join(fixture.cwd, 'ROADMAP.md'),
75
+ '# Roadmap\n\n' +
76
+ '## Progress\n\n' +
77
+ '| Phase | Plans | Status | Date |\n' +
78
+ '|-------|-------|--------|------|\n' +
79
+ '| 01. Test | 1/1 | In Progress | |\n\n' +
80
+ '## Phases\n\n' +
81
+ '- [ ] **Phase 01: Test** - desc\n\n' +
82
+ '## Phase 01: Test Phase\n\n' +
83
+ '**Plans:** 1/1 plans executed\n' +
84
+ '**Requirements:** [REQ-01]\n'
85
+ );
86
+
87
+ // STATE.md with all the fields cmdPhaseComplete regex-replaces
88
+ fs.writeFileSync(path.join(fixture.cwd, 'STATE.md'),
89
+ '# State\n\n' +
90
+ '**Current Phase:** 01\n' +
91
+ '**Current Phase Name:** Test Phase\n' +
92
+ '**Status:** In Progress\n' +
93
+ '**Current Plan:** 01-01\n' +
94
+ '**Last Activity:** 2025-01-01\n' +
95
+ '**Last Activity Description:** Working\n'
96
+ );
97
+
98
+ // REQUIREMENTS.md
99
+ fs.writeFileSync(path.join(fixture.cwd, 'REQUIREMENTS.md'),
100
+ '# Requirements\n\n' +
101
+ '- [ ] **REQ-01** Do thing\n\n' +
102
+ '| ID | Phase | Status |\n' +
103
+ '|----|-------|--------|\n' +
104
+ '| REQ-01 | Phase 01 | Pending |\n'
105
+ );
106
+
107
+ // Phase dir + PLAN + SUMMARY so findPhaseInternal finds it
108
+ fs.mkdirSync(path.join(fixture.cwd, 'phases/01-test-phase'), { recursive: true });
109
+ fs.writeFileSync(path.join(fixture.cwd, 'phases/01-test-phase/01-01-PLAN.md'), '# Plan\n');
110
+ fs.writeFileSync(path.join(fixture.cwd, 'phases/01-test-phase/01-01-SUMMARY.md'), '# Summary\n');
111
+
112
+ // Commit fixture additions so finalize produces a clean new commit
113
+ execSync('git add .', { cwd: fixture.cwd, stdio: 'pipe' });
114
+ execSync('git commit -m "fixture setup"', { cwd: fixture.cwd, stdio: 'pipe' });
115
+
116
+ // Write VERIFICATION.md AFTER the commit — this mirrors the real workflow where
117
+ // the verifier writes it just before finalize. It's untracked, so finalize must
118
+ // stage + commit it.
119
+ fs.writeFileSync(path.join(fixture.cwd, 'phases/01-test-phase/01-VERIFICATION.md'), '# Verification\n\nPassed.\n');
120
+
121
+ return fixture;
122
+ }
123
+
124
+ /**
125
+ * Write a rich fixture for Plan 01 finalize tests. Similar to above but the
126
+ * PLAN.md has real frontmatter with plan_name + requirements.
127
+ */
128
+ function makePlanFinalizeFixture() {
129
+ const fixture = createTempProject({ withGit: false, withPhases: false, withConfig: {} });
130
+
131
+ // Remove v2 markers so this is treated as a v1/root-layout project (phases at root)
132
+ fs.unlinkSync(path.join(fixture.cwd, 'PROJECTS.md'));
133
+ fs.unlinkSync(path.join(fixture.cwd, 'REPOS.md'));
134
+
135
+ // ROADMAP.md with Phase 04 (same structure pattern)
136
+ fs.writeFileSync(path.join(fixture.cwd, 'ROADMAP.md'),
137
+ '# Roadmap\n\n' +
138
+ '## Progress\n\n' +
139
+ '| Phase | Plans | Status | Date |\n' +
140
+ '|-------|-------|--------|------|\n' +
141
+ '| 04. Auth | 0/1 | Planned | |\n\n' +
142
+ '## Phases\n\n' +
143
+ '- [ ] **Phase 04: Auth** - auth layer\n\n' +
144
+ '## Phase 04: Auth\n\n' +
145
+ '**Plans:** 0/1 plans executed\n' +
146
+ '**Requirements:** [AUTH-01]\n'
147
+ );
148
+
149
+ fs.writeFileSync(path.join(fixture.cwd, 'STATE.md'),
150
+ '# State\n\n' +
151
+ '**Current Phase:** 04\n' +
152
+ '**Current Plan:** 04-01\n' +
153
+ 'Progress: [░░░░░░░░░░] 0%\n'
154
+ );
155
+
156
+ fs.writeFileSync(path.join(fixture.cwd, 'REQUIREMENTS.md'),
157
+ '# Requirements\n\n' +
158
+ '- [ ] **AUTH-01** Login\n\n' +
159
+ '| ID | Phase | Status |\n' +
160
+ '|----|-------|--------|\n' +
161
+ '| AUTH-01 | Phase 04 | Pending |\n'
162
+ );
163
+
164
+ // Phase dir + PLAN.md (committed before finalize runs)
165
+ fs.mkdirSync(path.join(fixture.cwd, 'phases/04-auth'), { recursive: true });
166
+ fs.writeFileSync(path.join(fixture.cwd, 'phases/04-auth/04-01-PLAN.md'),
167
+ '---\n' +
168
+ 'phase: 04-auth\n' +
169
+ 'plan: 01\n' +
170
+ 'plan_name: auth-login\n' +
171
+ 'requirements: [AUTH-01]\n' +
172
+ '---\n' +
173
+ '# Plan\n'
174
+ );
175
+
176
+ execSync('git add .', { cwd: fixture.cwd, stdio: 'pipe' });
177
+ execSync('git commit -m "fixture setup"', { cwd: fixture.cwd, stdio: 'pipe' });
178
+
179
+ // SUMMARY.md written AFTER the initial commit — matches real workflow where
180
+ // the executor produces SUMMARY.md as its final artifact before finalize runs.
181
+ // PLAN.md also gets touched so it's re-staged (the real workflow updates it
182
+ // with executed_by / completion metadata during the session).
183
+ fs.writeFileSync(path.join(fixture.cwd, 'phases/04-auth/04-01-SUMMARY.md'), '# Summary\n');
184
+ fs.appendFileSync(path.join(fixture.cwd, 'phases/04-auth/04-01-PLAN.md'), '\nexecuted.\n');
185
+
186
+ return fixture;
187
+ }
188
+
189
+ // ─── Tests ────────────────────────────────────────────────────────────────────
190
+
191
+ describe('cmdPhaseFinalize', () => {
192
+ let fixture;
193
+ let phase;
194
+
195
+ beforeEach(() => {
196
+ fixture = makePhaseFinalizeFixture();
197
+ // Reload phase.cjs each time so the fixture cwd's config is re-read fresh
198
+ delete require.cache[require.resolve('./phase.cjs')];
199
+ phase = require('./phase.cjs');
200
+ });
201
+
202
+ afterEach(() => {
203
+ fixture.cleanup();
204
+ });
205
+
206
+ it('creates a single commit containing tracking files + VERIFICATION.md', () => {
207
+ const before = gitLog(fixture.cwd).length;
208
+ const { json } = captureStdout(() =>
209
+ phase.cmdPhaseFinalize(fixture.cwd, '01', { push: false }, true)
210
+ );
211
+
212
+ assert.ok(json, 'should emit JSON output');
213
+ assert.equal(json.committed, true);
214
+ assert.ok(json.hash, 'should return commit hash');
215
+ assert.equal(gitLog(fixture.cwd).length, before + 1, 'exactly one new commit');
216
+
217
+ const files = gitShowFiles(fixture.cwd, 'HEAD');
218
+ assert.ok(files.includes('ROADMAP.md'), 'ROADMAP.md staged');
219
+ assert.ok(files.includes('STATE.md'), 'STATE.md staged');
220
+ assert.ok(files.includes('REQUIREMENTS.md'), 'REQUIREMENTS.md staged');
221
+ assert.ok(
222
+ files.some(f => /VERIFICATION\.md$/.test(f)),
223
+ 'VERIFICATION.md staged'
224
+ );
225
+ });
226
+
227
+ it('commit message uses docs(phase-NN): format', () => {
228
+ captureStdout(() =>
229
+ phase.cmdPhaseFinalize(fixture.cwd, '01', { push: false }, true)
230
+ );
231
+ assert.equal(gitLastMessage(fixture.cwd), 'docs(phase-01): complete phase execution');
232
+ });
233
+
234
+ it('succeeds when VERIFICATION.md is absent', () => {
235
+ // Simply delete the untracked VERIFICATION.md (never committed)
236
+ fs.unlinkSync(path.join(fixture.cwd, 'phases/01-test-phase/01-VERIFICATION.md'));
237
+
238
+ const { json } = captureStdout(() =>
239
+ phase.cmdPhaseFinalize(fixture.cwd, '01', { push: false }, true)
240
+ );
241
+
242
+ assert.equal(json.committed, true);
243
+ const files = gitShowFiles(fixture.cwd, 'HEAD');
244
+ assert.ok(!files.some(f => /VERIFICATION\.md$/.test(f)), 'no VERIFICATION in commit');
245
+ assert.ok(files.includes('ROADMAP.md'), 'ROADMAP still committed');
246
+ });
247
+
248
+ it('succeeds when REQUIREMENTS.md is absent', () => {
249
+ // VERIFICATION.md is untracked at this point — stage only the REQUIREMENTS removal.
250
+ fs.unlinkSync(path.join(fixture.cwd, 'REQUIREMENTS.md'));
251
+ execSync('git add REQUIREMENTS.md', { cwd: fixture.cwd, stdio: 'pipe' });
252
+ execSync('git commit -m "remove req"', { cwd: fixture.cwd, stdio: 'pipe' });
253
+
254
+ const { json } = captureStdout(() =>
255
+ phase.cmdPhaseFinalize(fixture.cwd, '01', { push: false }, true)
256
+ );
257
+
258
+ assert.equal(json.committed, true);
259
+ const files = gitShowFiles(fixture.cwd, 'HEAD');
260
+ assert.ok(!files.includes('REQUIREMENTS.md'), 'no REQUIREMENTS in commit');
261
+ assert.ok(files.includes('ROADMAP.md'), 'ROADMAP still committed');
262
+ assert.ok(files.includes('STATE.md'), 'STATE still committed');
263
+ assert.ok(
264
+ files.some(f => /VERIFICATION\.md$/.test(f)),
265
+ 'VERIFICATION still committed'
266
+ );
267
+ });
268
+
269
+ it('honors commit_docs=false (no commit, but file writes still happen)', () => {
270
+ const cfgPath = path.join(fixture.cwd, 'config.json');
271
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
272
+ cfg.commit_docs = false;
273
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg));
274
+ // Commit only the config change (VERIFICATION.md is untracked; leave it)
275
+ execSync('git add config.json', { cwd: fixture.cwd, stdio: 'pipe' });
276
+ execSync('git commit -m "disable commit_docs"', { cwd: fixture.cwd, stdio: 'pipe' });
277
+
278
+ const before = gitLog(fixture.cwd).length;
279
+ const { json } = captureStdout(() =>
280
+ phase.cmdPhaseFinalize(fixture.cwd, '01', { push: false }, true)
281
+ );
282
+
283
+ assert.equal(json.committed, false);
284
+ assert.equal(json.commit_reason, 'skipped_commit_docs_false');
285
+ assert.equal(gitLog(fixture.cwd).length, before, 'no new commit created');
286
+
287
+ // File writes still happened: ROADMAP should have [x]
288
+ const roadmap = fs.readFileSync(path.join(fixture.cwd, 'ROADMAP.md'), 'utf-8');
289
+ assert.match(roadmap, /\[x\]/, 'roadmap checkbox updated despite no commit');
290
+ });
291
+
292
+ it('returns committed=false with nothing_to_commit when no files to stage', () => {
293
+ // Remove all tracking files and VERIFICATION.md BEFORE finalize. findPhaseInternal
294
+ // still succeeds via the phase dir + PLAN.md, but no files exist to stage.
295
+ fs.unlinkSync(path.join(fixture.cwd, 'phases/01-test-phase/01-VERIFICATION.md'));
296
+ fs.unlinkSync(path.join(fixture.cwd, 'ROADMAP.md'));
297
+ fs.unlinkSync(path.join(fixture.cwd, 'STATE.md'));
298
+ fs.unlinkSync(path.join(fixture.cwd, 'REQUIREMENTS.md'));
299
+ execSync('git add -A', { cwd: fixture.cwd, stdio: 'pipe' });
300
+ execSync('git commit -m "remove all tracking"', { cwd: fixture.cwd, stdio: 'pipe' });
301
+
302
+ const before = gitLog(fixture.cwd).length;
303
+ const { json } = captureStdout(() =>
304
+ phase.cmdPhaseFinalize(fixture.cwd, '01', { push: false }, true)
305
+ );
306
+ assert.equal(json.committed, false);
307
+ assert.equal(json.commit_reason, 'nothing_to_commit');
308
+ assert.equal(gitLog(fixture.cwd).length, before, 'no new commit');
309
+ });
310
+ });
311
+
312
+ describe('cmdPlanFinalize', () => {
313
+ let fixture;
314
+ let commands;
315
+
316
+ beforeEach(() => {
317
+ fixture = makePlanFinalizeFixture();
318
+ delete require.cache[require.resolve('./commands.cjs')];
319
+ commands = require('./commands.cjs');
320
+ });
321
+
322
+ afterEach(() => {
323
+ fixture.cleanup();
324
+ });
325
+
326
+ it('creates a single commit containing PLAN + SUMMARY + tracking files', () => {
327
+ const before = gitLog(fixture.cwd).length;
328
+ const { json } = captureStdout(() =>
329
+ commands.cmdPlanFinalize(fixture.cwd, '04', '01', { push: false }, true)
330
+ );
331
+
332
+ assert.ok(json, 'should emit JSON output');
333
+ assert.equal(json.committed, true);
334
+ assert.ok(json.hash);
335
+ assert.equal(gitLog(fixture.cwd).length, before + 1, 'exactly one new commit');
336
+
337
+ const files = gitShowFiles(fixture.cwd, 'HEAD');
338
+ assert.ok(files.some(f => f.endsWith('04-01-PLAN.md')), 'PLAN.md staged');
339
+ assert.ok(files.some(f => f.endsWith('04-01-SUMMARY.md')), 'SUMMARY.md staged');
340
+ assert.ok(files.includes('STATE.md'), 'STATE.md staged');
341
+ assert.ok(files.includes('ROADMAP.md'), 'ROADMAP.md staged');
342
+ assert.ok(files.includes('REQUIREMENTS.md'), 'REQUIREMENTS.md staged');
343
+ });
344
+
345
+ it('commit message uses plan_name from PLAN.md frontmatter', () => {
346
+ captureStdout(() =>
347
+ commands.cmdPlanFinalize(fixture.cwd, '04', '01', { push: false }, true)
348
+ );
349
+ assert.equal(gitLastMessage(fixture.cwd), 'docs(04-01): complete auth-login plan');
350
+ });
351
+
352
+ it('--plan-name flag overrides frontmatter plan_name', () => {
353
+ captureStdout(() =>
354
+ commands.cmdPlanFinalize(fixture.cwd, '04', '01', { push: false, planName: 'override-name' }, true)
355
+ );
356
+ assert.equal(gitLastMessage(fixture.cwd), 'docs(04-01): complete override-name plan');
357
+ });
358
+
359
+ it('marks requirements from frontmatter complete', () => {
360
+ const { json } = captureStdout(() =>
361
+ commands.cmdPlanFinalize(fixture.cwd, '04', '01', { push: false }, true)
362
+ );
363
+ assert.equal(json.committed, true);
364
+ assert.deepEqual(json.requirements.marked_complete, ['AUTH-01']);
365
+
366
+ const reqContent = fs.readFileSync(path.join(fixture.cwd, 'REQUIREMENTS.md'), 'utf-8');
367
+ assert.match(reqContent, /- \[x\] \*\*AUTH-01\*\*/, 'checkbox marked');
368
+ assert.match(reqContent, /\|\s*AUTH-01\s*\|[^|]+\|\s*Complete\s*\|/, 'traceability Complete');
369
+ });
370
+
371
+ it('succeeds gracefully when REQUIREMENTS.md is absent', () => {
372
+ fs.unlinkSync(path.join(fixture.cwd, 'REQUIREMENTS.md'));
373
+ // Stage only the REQUIREMENTS removal — leave PLAN/SUMMARY untracked/dirty
374
+ // so finalize picks them up in the atomic commit.
375
+ execSync('git add REQUIREMENTS.md', { cwd: fixture.cwd, stdio: 'pipe' });
376
+ execSync('git commit -m "remove req"', { cwd: fixture.cwd, stdio: 'pipe' });
377
+
378
+ const { json } = captureStdout(() =>
379
+ commands.cmdPlanFinalize(fixture.cwd, '04', '01', { push: false }, true)
380
+ );
381
+ assert.equal(json.committed, true);
382
+ const files = gitShowFiles(fixture.cwd, 'HEAD');
383
+ assert.ok(!files.includes('REQUIREMENTS.md'), 'REQUIREMENTS not committed');
384
+ assert.ok(files.some(f => f.endsWith('04-01-PLAN.md')), 'PLAN still committed');
385
+ assert.ok(files.some(f => f.endsWith('04-01-SUMMARY.md')), 'SUMMARY still committed');
386
+ assert.ok(files.includes('STATE.md'), 'STATE still committed');
387
+ assert.ok(files.includes('ROADMAP.md'), 'ROADMAP still committed');
388
+ });
389
+
390
+ it('honors commit_docs=false (no commit, but file writes still happen)', () => {
391
+ const cfgPath = path.join(fixture.cwd, 'config.json');
392
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
393
+ cfg.commit_docs = false;
394
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg));
395
+ execSync('git add -A', { cwd: fixture.cwd, stdio: 'pipe' });
396
+ execSync('git commit -m "disable commit_docs"', { cwd: fixture.cwd, stdio: 'pipe' });
397
+
398
+ const before = gitLog(fixture.cwd).length;
399
+ const { json } = captureStdout(() =>
400
+ commands.cmdPlanFinalize(fixture.cwd, '04', '01', { push: false }, true)
401
+ );
402
+ assert.equal(json.committed, false);
403
+ assert.equal(json.commit_reason, 'skipped_commit_docs_false');
404
+ assert.equal(gitLog(fixture.cwd).length, before, 'no new commit');
405
+ });
406
+
407
+ it('falls back to "execution" when plan_name missing and no flag', () => {
408
+ // Remove plan_name from frontmatter
409
+ const planPath = path.join(fixture.cwd, 'phases/04-auth/04-01-PLAN.md');
410
+ const content = fs.readFileSync(planPath, 'utf-8').replace(/plan_name:.*\n/, '');
411
+ fs.writeFileSync(planPath, content);
412
+ execSync('git add -A', { cwd: fixture.cwd, stdio: 'pipe' });
413
+ execSync('git commit -m "strip plan_name"', { cwd: fixture.cwd, stdio: 'pipe' });
414
+
415
+ captureStdout(() =>
416
+ commands.cmdPlanFinalize(fixture.cwd, '04', '01', { push: false }, true)
417
+ );
418
+ assert.equal(gitLastMessage(fixture.cwd), 'docs(04-01): complete execution plan');
419
+ });
420
+ });
@@ -0,0 +1,48 @@
1
+ // deliver-great-systems/bin/lib/plan-number-validity.test.cjs
2
+ // REL-04 regression test scaffold — initially RED. Turns GREEN after plan 02
3
+ // adds Dimension 10 (plan-number validity) to agents/dgs-plan-checker.md.
4
+ //
5
+ // Strategy: this is an agent-prompt assertion, not a CLI test. Tests read
6
+ // agents/dgs-plan-checker.md and assert the dimension exists with the required
7
+ // remediation text + soft-fail marker + named-follow-up-phase reference.
8
+
9
+ const test = require('node:test');
10
+ const assert = require('node:assert');
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+
14
+ const repoRoot = path.resolve(__dirname, '../../..');
15
+ const checkerPath = path.join(repoRoot, 'agents/dgs-plan-checker.md');
16
+
17
+ test('REL-04: dgs-plan-checker has Dimension 10 (plan-number validity)', () => {
18
+ const content = fs.readFileSync(checkerPath, 'utf-8');
19
+ assert.match(content, /## Dimension 10: Plan Number Validity/, 'Dimension 10 must exist');
20
+ });
21
+
22
+ test('REL-04: Dimension 10 enumerates rejected plan-number forms', () => {
23
+ const content = fs.readFileSync(checkerPath, 'utf-8');
24
+ for (const form of ['plan: 00', 'plan: 0', 'positive integer']) {
25
+ assert.ok(content.includes(form), `Dimension 10 must mention "${form}"`);
26
+ }
27
+ });
28
+
29
+ test('REL-04: Dimension 10 includes exact rename remediation command', () => {
30
+ const content = fs.readFileSync(checkerPath, 'utf-8');
31
+ assert.match(content, /git mv .*-00-PLAN\.md.*-01-PLAN\.md/, 'remediation must include git mv command');
32
+ });
33
+
34
+ test('REL-04: Dimension 10 is soft-fail (warning, not block) on initial rollout', () => {
35
+ const content = fs.readFileSync(checkerPath, 'utf-8');
36
+ const dim10Match = content.match(/## Dimension 10[\s\S]+?(?=## Dimension|$)/);
37
+ assert.ok(dim10Match, 'Dimension 10 block must exist');
38
+ const dim10Text = dim10Match[0];
39
+ assert.ok(/soft-fail|warning|warn/i.test(dim10Text), 'Dimension 10 must mark itself as soft-fail/warning');
40
+ });
41
+
42
+ test('REL-04: Dimension 10 references the named follow-up phase for hard-reject promotion', () => {
43
+ const content = fs.readFileSync(checkerPath, 'utf-8');
44
+ assert.ok(/follow-up phase|hard-reject promotion/i.test(content),
45
+ 'Dimension 10 must reference hard-reject deferral to a named follow-up phase');
46
+ });
47
+
48
+ // REL-04 sentinel — flag this file as a Wave-0 RED scaffold for plan 02.