@ktpartners/dgs-platform 3.0.4 → 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 (115) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +8 -1
  3. package/agents/dgs-executor.md +124 -3
  4. package/agents/dgs-idea-researcher.md +447 -0
  5. package/agents/dgs-plan-checker.md +32 -0
  6. package/agents/dgs-planner.md +41 -8
  7. package/bin/install.js +44 -0
  8. package/commands/dgs/audit-milestone.md +2 -1
  9. package/commands/dgs/diff-report.md +124 -0
  10. package/commands/dgs/new-project.md +8 -21
  11. package/commands/dgs/package-scan.md +43 -0
  12. package/commands/dgs/research-idea.md +1 -0
  13. package/commands/dgs/switch-project.md +13 -0
  14. package/deliver-great-systems/bin/dgs-tools.cjs +120 -5
  15. package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
  16. package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
  17. package/deliver-great-systems/bin/lib/commands.cjs +311 -16
  18. package/deliver-great-systems/bin/lib/commands.test.cjs +115 -0
  19. package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
  20. package/deliver-great-systems/bin/lib/config.cjs +41 -0
  21. package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
  22. package/deliver-great-systems/bin/lib/core.cjs +7 -3
  23. package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
  24. package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
  25. package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
  26. package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
  27. package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
  28. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
  29. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
  30. package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
  31. package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
  32. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
  33. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
  34. package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
  35. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
  36. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
  37. package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
  38. package/deliver-great-systems/bin/lib/governance.cjs +211 -0
  39. package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
  40. package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
  41. package/deliver-great-systems/bin/lib/init.cjs +56 -27
  42. package/deliver-great-systems/bin/lib/init.test.cjs +212 -5
  43. package/deliver-great-systems/bin/lib/jobs.cjs +7 -4
  44. package/deliver-great-systems/bin/lib/milestone.cjs +101 -3
  45. package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
  46. package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
  47. package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
  48. package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
  49. package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
  50. package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
  51. package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
  52. package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
  53. package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
  54. package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
  55. package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
  56. package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
  57. package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
  58. package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
  59. package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
  60. package/deliver-great-systems/bin/lib/phase.cjs +18 -1
  61. package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
  62. package/deliver-great-systems/bin/lib/projects.cjs +38 -3
  63. package/deliver-great-systems/bin/lib/projects.test.cjs +112 -2
  64. package/deliver-great-systems/bin/lib/quick.cjs +178 -23
  65. package/deliver-great-systems/bin/lib/quick.test.cjs +138 -4
  66. package/deliver-great-systems/bin/lib/repos.cjs +12 -12
  67. package/deliver-great-systems/bin/lib/review.cjs +1821 -0
  68. package/deliver-great-systems/bin/lib/state.cjs +7 -3
  69. package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
  70. package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
  71. package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
  72. package/deliver-great-systems/bin/lib/verify.cjs +118 -6
  73. package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
  74. package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
  75. package/deliver-great-systems/bin/lib/worktrees.cjs +27 -1
  76. package/deliver-great-systems/bin/lib/worktrees.test.cjs +76 -0
  77. package/deliver-great-systems/references/agent-step-reliability.md +60 -0
  78. package/deliver-great-systems/references/conflict-resolution.md +4 -0
  79. package/deliver-great-systems/references/context-tiers.md +4 -0
  80. package/deliver-great-systems/references/package-scan-config.md +151 -0
  81. package/deliver-great-systems/references/questioning.md +0 -30
  82. package/deliver-great-systems/references/spec-review-loop.md +1 -2
  83. package/deliver-great-systems/references/workflow-conventions.md +29 -0
  84. package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
  85. package/deliver-great-systems/templates/REVIEW.md +35 -0
  86. package/deliver-great-systems/templates/VALIDATION.md +1 -1
  87. package/deliver-great-systems/templates/claude-md.md +11 -0
  88. package/deliver-great-systems/templates/package-scan-report.md +108 -0
  89. package/deliver-great-systems/templates/project.md +6 -170
  90. package/deliver-great-systems/templates/summary.md +3 -1
  91. package/deliver-great-systems/workflows/add-phase.md +5 -0
  92. package/deliver-great-systems/workflows/audit-milestone.md +66 -10
  93. package/deliver-great-systems/workflows/cancel-job.md +1 -1
  94. package/deliver-great-systems/workflows/codereview.md +103 -9
  95. package/deliver-great-systems/workflows/complete-milestone.md +26 -7
  96. package/deliver-great-systems/workflows/complete-quick.md +40 -2
  97. package/deliver-great-systems/workflows/discuss-phase.md +3 -2
  98. package/deliver-great-systems/workflows/execute-phase.md +89 -2
  99. package/deliver-great-systems/workflows/execute-plan.md +10 -1
  100. package/deliver-great-systems/workflows/help.md +51 -18
  101. package/deliver-great-systems/workflows/import-spec.md +65 -7
  102. package/deliver-great-systems/workflows/init-product.md +46 -152
  103. package/deliver-great-systems/workflows/new-milestone.md +115 -14
  104. package/deliver-great-systems/workflows/new-project.md +60 -331
  105. package/deliver-great-systems/workflows/package-scan.md +59 -0
  106. package/deliver-great-systems/workflows/plan-phase.md +79 -1
  107. package/deliver-great-systems/workflows/quick-complete.md +40 -2
  108. package/deliver-great-systems/workflows/quick.md +183 -10
  109. package/deliver-great-systems/workflows/research-idea.md +80 -142
  110. package/deliver-great-systems/workflows/run-job.md +21 -35
  111. package/deliver-great-systems/workflows/settings.md +13 -77
  112. package/deliver-great-systems/workflows/write-spec.md +9 -11
  113. package/hooks/dist/dgs-enforce-discipline.js +196 -0
  114. package/package.json +1 -1
  115. package/scripts/build-hooks.js +1 -0
@@ -678,7 +678,10 @@ function buildStateFrontmatter(bodyContent, cwd) {
678
678
  normalizedStatus = 'discussing';
679
679
  } else if (statusLower.includes('verif')) {
680
680
  normalizedStatus = 'verifying';
681
- } 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.
682
685
  normalizedStatus = 'completed';
683
686
  } else if (statusLower.includes('ready to execute')) {
684
687
  normalizedStatus = 'executing';
@@ -919,8 +922,9 @@ function markMilestoneComplete(cwd) {
919
922
  const today = new Date().toISOString().split('T')[0];
920
923
  const now = new Date().toISOString();
921
924
 
922
- // Update frontmatter fields
923
- fm.status = 'complete';
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';
924
928
  if (!fm.progress) fm.progress = {};
925
929
  fm.progress.percent = 100;
926
930
  fm.last_updated = now;
@@ -0,0 +1,54 @@
1
+ // deliver-great-systems/bin/lib/summary-frontmatter.cjs
2
+ // REL-07: canonical SUMMARY-frontmatter writer.
3
+ // Reads PLAN.md frontmatter `requirements:` field and writes a SUMMARY.md
4
+ // frontmatter that ALWAYS populates the canonical YAML key (CANONICAL_KEY)
5
+ // with those exact values.
6
+ //
7
+ // Canonical key: requirements_completed (underscore — pinned by 157-Q2-FINDINGS.md).
8
+
9
+ const fs = require('node:fs');
10
+ const { extractFrontmatter } = require('./frontmatter.cjs');
11
+
12
+ const CANONICAL_KEY = 'requirements_completed';
13
+
14
+ function writeSummaryFrontmatter({ planPath, summaryPath, extra = {} }) {
15
+ if (!fs.existsSync(planPath)) {
16
+ throw new Error(`writeSummaryFrontmatter: plan not found at ${planPath}`);
17
+ }
18
+ const planContent = fs.readFileSync(planPath, 'utf-8');
19
+ const planFm = extractFrontmatter(planContent);
20
+ const requirements = Array.isArray(planFm.requirements) ? planFm.requirements : [];
21
+
22
+ // Build SUMMARY frontmatter with the canonical key populated verbatim.
23
+ const fm = {
24
+ phase: extra.phase || planFm.phase || 'unknown',
25
+ plan: extra.plan || planFm.plan || '01',
26
+ subsystem: extra.subsystem || 'unspecified',
27
+ tags: extra.tags || [],
28
+ duration: extra.duration || '0min',
29
+ completed: extra.completed || new Date().toISOString().slice(0, 10),
30
+ [CANONICAL_KEY]: requirements,
31
+ ...(extra.executed_by ? { executed_by: extra.executed_by } : {}),
32
+ };
33
+
34
+ // Serialise to YAML frontmatter. Minimal stable serialiser to avoid
35
+ // introducing a new dependency.
36
+ const lines = ['---'];
37
+ for (const [key, value] of Object.entries(fm)) {
38
+ if (Array.isArray(value)) {
39
+ if (value.length === 0) {
40
+ lines.push(`${key}: []`);
41
+ } else {
42
+ lines.push(`${key}: [${value.join(', ')}]`);
43
+ }
44
+ } else {
45
+ lines.push(`${key}: ${value}`);
46
+ }
47
+ }
48
+ lines.push('---', '', `# Phase ${fm.phase} Plan ${fm.plan}: Summary`, '');
49
+
50
+ fs.writeFileSync(summaryPath, lines.join('\n'));
51
+ return { summaryPath, canonicalKey: CANONICAL_KEY, requirementsCopied: requirements.length };
52
+ }
53
+
54
+ module.exports = { writeSummaryFrontmatter, CANONICAL_KEY };
@@ -0,0 +1,78 @@
1
+ // deliver-great-systems/bin/lib/summary-frontmatter.test.cjs
2
+ // REL-07 regression test scaffold — initially RED. Turns GREEN after plan 02
3
+ // pins the canonical YAML key and updates writer/template/agent/workflow/reader.
4
+
5
+ const test = require('node:test');
6
+ const assert = require('node:assert');
7
+ const fs = require('node:fs');
8
+ const path = require('node:path');
9
+ const os = require('node:os');
10
+ const { extractFrontmatter } = require('./frontmatter.cjs');
11
+
12
+ // CANONICAL_KEY: pinned by 157-Q2-FINDINGS.md (underscore variant).
13
+ // Plan 01 leaves the placeholder; plan 02 confirms the writer/template/etc. agree.
14
+ const CANONICAL_KEY = 'requirements_completed';
15
+
16
+ test('REL-07: SUMMARY writer copies PLAN requirements verbatim into canonical key', () => {
17
+ // RED: this assertion fails because no canonical writer module exists yet.
18
+ let writer;
19
+ try {
20
+ writer = require('./summary-frontmatter.cjs');
21
+ } catch (err) {
22
+ assert.fail('REL-07 writer module not yet implemented: bin/lib/summary-frontmatter.cjs (plan 02 task)');
23
+ }
24
+
25
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rel07-'));
26
+ const planPath = path.join(tmpDir, 'test-PLAN.md');
27
+ const summaryPath = path.join(tmpDir, 'test-SUMMARY.md');
28
+ fs.writeFileSync(planPath, '---\nphase: test\nplan: 01\nrequirements:\n - TEST-01\n - TEST-02\n---\n');
29
+
30
+ writer.writeSummaryFrontmatter({ planPath, summaryPath });
31
+
32
+ const summary = fs.readFileSync(summaryPath, 'utf-8');
33
+ const fm = extractFrontmatter(summary);
34
+ assert.deepStrictEqual(fm[CANONICAL_KEY], ['TEST-01', 'TEST-02']);
35
+ });
36
+
37
+ test('REL-07: SUMMARY writer leaves canonical key empty when PLAN requirements is empty', () => {
38
+ let writer;
39
+ try {
40
+ writer = require('./summary-frontmatter.cjs');
41
+ } catch (err) {
42
+ assert.fail('REL-07 writer module not yet implemented: bin/lib/summary-frontmatter.cjs (plan 02 task)');
43
+ }
44
+
45
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rel07-empty-'));
46
+ const planPath = path.join(tmpDir, 'test-PLAN.md');
47
+ const summaryPath = path.join(tmpDir, 'test-SUMMARY.md');
48
+ fs.writeFileSync(planPath, '---\nphase: test\nplan: 01\nrequirements: []\n---\n');
49
+
50
+ writer.writeSummaryFrontmatter({ planPath, summaryPath });
51
+
52
+ const summary = fs.readFileSync(summaryPath, 'utf-8');
53
+ const fm = extractFrontmatter(summary);
54
+ assert.deepStrictEqual(fm[CANONICAL_KEY] || [], []);
55
+ });
56
+
57
+ test('REL-07: canonical YAML key is consistent across template, agent prompt, workflow, reader, schema', () => {
58
+ // Convergence assertion. RED until plan 02 lands all five edits.
59
+ // __dirname = .../deliver-great-systems/bin/lib
60
+ // bin/lib/.. = .../deliver-great-systems/bin
61
+ // bin/.. = .../deliver-great-systems
62
+ // deliver-great-systems/.. = repo root (which contains the top-level agents/ and the nested deliver-great-systems/)
63
+ const repoRoot = path.resolve(__dirname, '../../..');
64
+ const sources = {
65
+ template: fs.readFileSync(path.join(repoRoot, 'deliver-great-systems/templates/summary.md'), 'utf-8'),
66
+ agent: fs.readFileSync(path.join(repoRoot, 'agents/dgs-executor.md'), 'utf-8'),
67
+ workflow: fs.readFileSync(path.join(repoRoot, 'deliver-great-systems/workflows/execute-plan.md'), 'utf-8'),
68
+ reader: fs.readFileSync(path.join(repoRoot, 'deliver-great-systems/bin/lib/commands.cjs'), 'utf-8'),
69
+ schema: fs.readFileSync(path.join(repoRoot, 'deliver-great-systems/bin/lib/frontmatter.cjs'), 'utf-8'),
70
+ };
71
+
72
+ // Each file must reference the CANONICAL_KEY at least once.
73
+ for (const [name, content] of Object.entries(sources)) {
74
+ assert.ok(content.includes(CANONICAL_KEY), `${name} must reference canonical key ${CANONICAL_KEY}`);
75
+ }
76
+ });
77
+
78
+ // REL-07 sentinel — flag this file as a Wave-0 RED scaffold for plan 02.
@@ -0,0 +1,263 @@
1
+ /**
2
+ * RED test scaffold for REL-02 (Phase 156 plan 03).
3
+ *
4
+ * The `computePhaseSweep` helper is implemented in plan 03; running this
5
+ * file before plan 03 lands MUST produce 6 failed tests with
6
+ * 'not yet implemented — REL-02' style messages.
7
+ *
8
+ * Behaviour under test:
9
+ * - union: commit list = git-discovered phase-dir paths UNION
10
+ * executor-reported modified_files (deduplicated)
11
+ * - underreport defence: empty modified_files still commits the
12
+ * git-discovered phase-dir paths
13
+ * - scope filter — sibling phase: dirty file in a different phase
14
+ * dir is NOT included
15
+ * - scope filter — out-of-scope dirs: dirty files in ideas/, specs/,
16
+ * and project root are NOT included
17
+ * - reported-path escape: a modified_files entry pointing outside the
18
+ * phase dir is filtered out (or surfaced via dropped_out_of_scope)
19
+ * - idempotency: running the helper twice on the same fixture returns
20
+ * identical results
21
+ *
22
+ * Conventions:
23
+ * - Uses node:test runner + node:assert (matches state-transition-gate.test.cjs)
24
+ * - Each test creates and tears down its own temp git repo via os.tmpdir()
25
+ * - Until plan 03 lands, every test fails with 'not yet implemented'
26
+ * so the file is RED in a controlled way (no parse errors).
27
+ */
28
+
29
+ const test = require('node:test');
30
+ const assert = require('node:assert');
31
+ const fs = require('fs');
32
+ const os = require('os');
33
+ const path = require('path');
34
+ const { execSync } = require('child_process');
35
+
36
+ const NOT_IMPL = 'computePhaseSweep not yet implemented — REL-02';
37
+
38
+ // ─── Helpers ──────────────────────────────────────────────────────────────
39
+
40
+ function tryRequireCommands() {
41
+ try {
42
+ // Reset the planning-root cache between tests so each
43
+ // makeTempPlanningRoot() sees a fresh root; getPlanningRoot caches
44
+ // per-process and would otherwise pin the first temp dir for the
45
+ // whole test run.
46
+ try { require('./paths.cjs').resetPaths(); } catch { /* ignore */ }
47
+ return require('./commands.cjs');
48
+ } catch (err) {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function makeTempPlanningRoot() {
54
+ // Resolve real path to handle symlink wrap on macOS (/tmp -> /private/tmp).
55
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'sweep-scope-test-')));
56
+ execSync('git init --quiet', { cwd: dir });
57
+ execSync('git config user.email test@example.com', { cwd: dir });
58
+ execSync('git config user.name "Test User"', { cwd: dir });
59
+ // Seed an initial commit so HEAD exists
60
+ fs.writeFileSync(path.join(dir, 'README.md'), '# seed\n');
61
+ execSync('git add README.md', { cwd: dir });
62
+ execSync('git commit --quiet -m "seed"', { cwd: dir });
63
+ return dir;
64
+ }
65
+
66
+ function cleanupRoot(dir) {
67
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
68
+ }
69
+
70
+ function writeFile(root, rel, content) {
71
+ const abs = path.join(root, rel);
72
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
73
+ fs.writeFileSync(abs, content);
74
+ }
75
+
76
+ // ─── Test 1: union ────────────────────────────────────────────────────────
77
+
78
+ test('REL-02 union: commit list = git-discovered phase-dir paths UNION executor-reported modified_files', () => {
79
+ const cmds = tryRequireCommands();
80
+ if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
81
+ assert.fail(NOT_IMPL);
82
+ return;
83
+ }
84
+ const root = makeTempPlanningRoot();
85
+ try {
86
+ const phasesDir = 'phases';
87
+ const phaseDir = '156-test';
88
+ // git-dirty: A and B exist in the phase dir, untracked
89
+ writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
90
+ writeFile(root, `${phasesDir}/${phaseDir}/B.md`, 'b\n');
91
+ // C is reported by the executor but does not exist on disk yet
92
+ writeFile(root, `${phasesDir}/${phaseDir}/C.md`, 'c\n');
93
+ const result = cmds.computePhaseSweep(root, {
94
+ phasesDir,
95
+ phaseDir,
96
+ modifiedFiles: [
97
+ `${phasesDir}/${phaseDir}/B.md`,
98
+ `${phasesDir}/${phaseDir}/C.md`,
99
+ ],
100
+ }, true);
101
+ const swept = (result && result.swept) || [];
102
+ const expected = [
103
+ `${phasesDir}/${phaseDir}/A.md`,
104
+ `${phasesDir}/${phaseDir}/B.md`,
105
+ `${phasesDir}/${phaseDir}/C.md`,
106
+ ].sort();
107
+ assert.deepStrictEqual(swept.slice().sort(), expected, 'union must equal A,B,C');
108
+ } finally {
109
+ cleanupRoot(root);
110
+ }
111
+ });
112
+
113
+ // ─── Test 2: underreport defence ──────────────────────────────────────────
114
+
115
+ test('REL-02 underreport: executor returns empty modified_files but git-discovered phase-dir paths still committed', () => {
116
+ const cmds = tryRequireCommands();
117
+ if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
118
+ assert.fail(NOT_IMPL);
119
+ return;
120
+ }
121
+ const root = makeTempPlanningRoot();
122
+ try {
123
+ const phasesDir = 'phases';
124
+ const phaseDir = '156-test';
125
+ writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
126
+ writeFile(root, `${phasesDir}/${phaseDir}/B.md`, 'b\n');
127
+ const result = cmds.computePhaseSweep(root, {
128
+ phasesDir,
129
+ phaseDir,
130
+ modifiedFiles: [],
131
+ }, true);
132
+ const swept = (result && result.swept) || [];
133
+ assert.strictEqual(swept.length, 2, 'expected 2 git-dirty files');
134
+ assert.ok(swept.includes(`${phasesDir}/${phaseDir}/A.md`));
135
+ assert.ok(swept.includes(`${phasesDir}/${phaseDir}/B.md`));
136
+ } finally {
137
+ cleanupRoot(root);
138
+ }
139
+ });
140
+
141
+ // ─── Test 3: scope filter — sibling phase ─────────────────────────────────
142
+
143
+ test('REL-02 scope filter: dirty file in sibling phase dir is NOT included', () => {
144
+ const cmds = tryRequireCommands();
145
+ if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
146
+ assert.fail(NOT_IMPL);
147
+ return;
148
+ }
149
+ const root = makeTempPlanningRoot();
150
+ try {
151
+ const phasesDir = 'phases';
152
+ const phaseDir = '156-test';
153
+ const sibling = '155-other';
154
+ writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
155
+ writeFile(root, `${phasesDir}/${sibling}/X.md`, 'x\n');
156
+ const result = cmds.computePhaseSweep(root, {
157
+ phasesDir,
158
+ phaseDir,
159
+ modifiedFiles: [],
160
+ }, true);
161
+ const swept = (result && result.swept) || [];
162
+ assert.strictEqual(swept.length, 1);
163
+ assert.ok(swept.includes(`${phasesDir}/${phaseDir}/A.md`));
164
+ assert.ok(!swept.some(p => p.includes(sibling)),
165
+ 'sibling phase paths must NOT appear in swept');
166
+ } finally {
167
+ cleanupRoot(root);
168
+ }
169
+ });
170
+
171
+ // ─── Test 4: scope filter — ideas / specs / project root ──────────────────
172
+
173
+ test('REL-02 scope filter: dirty file in ideas/, specs/, project root is NOT included', () => {
174
+ const cmds = tryRequireCommands();
175
+ if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
176
+ assert.fail(NOT_IMPL);
177
+ return;
178
+ }
179
+ const root = makeTempPlanningRoot();
180
+ try {
181
+ const phasesDir = 'phases';
182
+ const phaseDir = '156-test';
183
+ writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
184
+ writeFile(root, 'ideas/foo.md', 'idea\n');
185
+ writeFile(root, 'specs/bar.md', 'spec\n');
186
+ writeFile(root, 'NOTES.md', 'top-level\n');
187
+ const result = cmds.computePhaseSweep(root, {
188
+ phasesDir,
189
+ phaseDir,
190
+ modifiedFiles: [],
191
+ }, true);
192
+ const swept = (result && result.swept) || [];
193
+ assert.strictEqual(swept.length, 1, 'only the in-scope phase file should be swept');
194
+ assert.ok(swept.includes(`${phasesDir}/${phaseDir}/A.md`));
195
+ for (const out of ['ideas/foo.md', 'specs/bar.md', 'NOTES.md']) {
196
+ assert.ok(!swept.includes(out), `${out} must NOT be swept`);
197
+ }
198
+ } finally {
199
+ cleanupRoot(root);
200
+ }
201
+ });
202
+
203
+ // ─── Test 5: reported-path escape ─────────────────────────────────────────
204
+
205
+ test('REL-02 reported-path escape: a modified_files entry that points outside the phase dir is filtered out', () => {
206
+ const cmds = tryRequireCommands();
207
+ if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
208
+ assert.fail(NOT_IMPL);
209
+ return;
210
+ }
211
+ const root = makeTempPlanningRoot();
212
+ try {
213
+ const phasesDir = 'phases';
214
+ const phaseDir = '156-test';
215
+ const sibling = '155-other';
216
+ writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
217
+ writeFile(root, `${phasesDir}/${sibling}/X.md`, 'x\n');
218
+ const result = cmds.computePhaseSweep(root, {
219
+ phasesDir,
220
+ phaseDir,
221
+ modifiedFiles: [
222
+ `${phasesDir}/${phaseDir}/A.md`,
223
+ `${phasesDir}/${sibling}/X.md`, // escape attempt
224
+ ],
225
+ }, true);
226
+ const swept = (result && result.swept) || [];
227
+ assert.strictEqual(swept.length, 1, 'only the in-scope path should be swept');
228
+ assert.ok(!swept.includes(`${phasesDir}/${sibling}/X.md`),
229
+ 'sibling phase path must be filtered out of swept');
230
+ // Implementations may or may not surface a dropped_out_of_scope array;
231
+ // the contract is only that the dropped path is NOT in `swept`.
232
+ } finally {
233
+ cleanupRoot(root);
234
+ }
235
+ });
236
+
237
+ // ─── Test 6: idempotency ──────────────────────────────────────────────────
238
+
239
+ test('REL-02 idempotency: running the sweep twice on the same fixture returns the same set', () => {
240
+ const cmds = tryRequireCommands();
241
+ if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
242
+ assert.fail(NOT_IMPL);
243
+ return;
244
+ }
245
+ const root = makeTempPlanningRoot();
246
+ try {
247
+ const phasesDir = 'phases';
248
+ const phaseDir = '156-test';
249
+ writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
250
+ writeFile(root, `${phasesDir}/${phaseDir}/B.md`, 'b\n');
251
+ const opts = {
252
+ phasesDir,
253
+ phaseDir,
254
+ modifiedFiles: [`${phasesDir}/${phaseDir}/B.md`],
255
+ };
256
+ const r1 = cmds.computePhaseSweep(root, opts, true);
257
+ const r2 = cmds.computePhaseSweep(root, opts, true);
258
+ assert.deepStrictEqual((r1 && r1.swept) || [], (r2 && r2.swept) || [],
259
+ 'two identical calls must return identical swept sets');
260
+ } finally {
261
+ cleanupRoot(root);
262
+ }
263
+ });
@@ -550,16 +550,13 @@ function cmdValidateHealth(cwd, options, raw) {
550
550
  return;
551
551
  }
552
552
 
553
- // ─── Check 2: PROJECT.md exists and has required sections ─────────────────
553
+ // ─── Check 2: PROJECT.md exists and has a heading ────────────────────────
554
554
  if (!fs.existsSync(projectPath)) {
555
555
  addIssue('error', 'E002', 'PROJECT.md not found', 'Run /dgs:new-project to create');
556
556
  } else {
557
557
  const content = fs.readFileSync(projectPath, 'utf-8');
558
- const requiredSections = ['## What This Is', '## Core Value', '## Requirements'];
559
- for (const section of requiredSections) {
560
- if (!content.includes(section)) {
561
- addIssue('warning', 'W001', `PROJECT.md missing section: ${section}`, 'Add section manually');
562
- }
558
+ if (!/^#\s+\S/m.test(content)) {
559
+ addIssue('warning', 'W001', 'PROJECT.md missing top-level heading', 'Add a # Title line');
563
560
  }
564
561
  }
565
562
 
@@ -788,6 +785,121 @@ function cmdValidateHealth(cwd, options, raw) {
788
785
  }
789
786
  } catch { /* non-git or git error — skip silently */ }
790
787
 
788
+ // ─── Check 11: Untracked scaffolding (.gitkeep) — REL-12 ────────────────
789
+ // Walks planning root for `.gitkeep` files in standard scaffold locations
790
+ // (specs/, docs/product/, quick/, projects/*/{phases,quick,debug,research}/)
791
+ // and warns if any exist on disk but aren't tracked in git. User-facing analog
792
+ // of REL-09 (init-product .gitkeep commit) — REL-09 prevented the leak,
793
+ // REL-12 catches any reintroduction immediately on the next health-check run.
794
+ try {
795
+ const { execGit } = require('./core.cjs');
796
+ const isGitWT = (() => {
797
+ try {
798
+ const res = execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
799
+ return res && res.exitCode === 0 && (res.stdout || '').trim() === 'true';
800
+ } catch { return false; }
801
+ })();
802
+
803
+ if (isGitWT) {
804
+ const candidates = [];
805
+
806
+ // Direct scaffold locations under planning root
807
+ const directDirs = ['specs', 'docs/product', 'quick'];
808
+ for (const d of directDirs) {
809
+ const p = path.join(planningDir, d, '.gitkeep');
810
+ if (fs.existsSync(p)) candidates.push(path.join(d, '.gitkeep'));
811
+ }
812
+
813
+ // Per-project scaffold locations: projects/*/{phases,quick,debug,research}/.gitkeep
814
+ const projectsDir = path.join(planningDir, 'projects');
815
+ if (fs.existsSync(projectsDir)) {
816
+ try {
817
+ const projects = fs.readdirSync(projectsDir, { withFileTypes: true });
818
+ for (const proj of projects) {
819
+ if (!proj.isDirectory()) continue;
820
+ for (const sub of ['phases', 'quick', 'debug', 'research']) {
821
+ const p = path.join(planningDir, 'projects', proj.name, sub, '.gitkeep');
822
+ if (fs.existsSync(p)) {
823
+ candidates.push(path.join('projects', proj.name, sub, '.gitkeep'));
824
+ }
825
+ }
826
+ }
827
+ } catch { /* ignore */ }
828
+ }
829
+
830
+ // For each candidate, check whether it's tracked in git via
831
+ // `git ls-files <rel>` — empty stdout means untracked.
832
+ const untracked = [];
833
+ for (const rel of candidates) {
834
+ const lsResult = execGit(cwd, ['ls-files', '--', rel]);
835
+ const tracked = lsResult && lsResult.exitCode === 0 && (lsResult.stdout || '').trim().length > 0;
836
+ if (!tracked) untracked.push(rel);
837
+ }
838
+
839
+ if (untracked.length > 0) {
840
+ addIssue(
841
+ 'warning',
842
+ 'untracked-scaffolding',
843
+ `Found ${untracked.length} untracked .gitkeep file(s) in standard scaffold locations: ${untracked.join(', ')}`,
844
+ `Run 'git add ${untracked.join(' ')} && git commit -m "docs: track scaffolding files"' to commit them; closes idea #29 (init-product .gitkeep leak) on the next health-check run.`,
845
+ false
846
+ );
847
+ }
848
+ }
849
+ } catch { /* non-git or git error — skip silently per existing convention */ }
850
+
851
+ // ─── Check 12: untracked-phase-artifacts (REL-11, Phase 156) ───────────────
852
+ // Walk every phase directory under projects/<project>/phases/ (or
853
+ // top-level phases/ in v1) and report any uncommitted PLAN.md /
854
+ // CONTEXT.md / RESEARCH.md / UAT.md / VERIFICATION.md files. This is
855
+ // the loud-fail safety net for REL-01 / REL-02 — if either of those
856
+ // commit-step contracts ever regresses, this health check surfaces
857
+ // the dangling artifact on the next /dgs:health run.
858
+ try {
859
+ const untracked = [];
860
+ if (fs.existsSync(phasesDir)) {
861
+ const ARTIFACT_RE = /^[0-9].*-(PLAN|CONTEXT|RESEARCH|UAT|VERIFICATION)\.md$/;
862
+ const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
863
+ for (const entry of phaseEntries) {
864
+ if (!entry.isDirectory()) continue;
865
+ const phaseDirAbs = path.join(phasesDir, entry.name);
866
+ let files;
867
+ try { files = fs.readdirSync(phaseDirAbs); } catch { continue; }
868
+ for (const f of files) {
869
+ if (!ARTIFACT_RE.test(f)) continue;
870
+ const absPath = path.join(phaseDirAbs, f);
871
+ const relPath = path.relative(planningDir, absPath);
872
+ // `git ls-files -- <relPath>` returns the path if tracked,
873
+ // empty string if untracked. Run with cwd = planningDir so
874
+ // the relative path resolves correctly.
875
+ const lsResult = execGit(planningDir, ['ls-files', '--', relPath]);
876
+ const tracked = lsResult.exitCode === 0 && (lsResult.stdout || '').trim().length > 0;
877
+ if (!tracked) untracked.push(relPath);
878
+ }
879
+ }
880
+ }
881
+ if (untracked.length > 0) {
882
+ const remediation =
883
+ `Run 'node $TOOLS commit "docs: catch up untracked phase artifacts" --files ${untracked.join(' ')}' ` +
884
+ `or run /dgs:health --repair`;
885
+ addIssue(
886
+ 'warning',
887
+ 'W010',
888
+ `untracked-phase-artifacts: ${untracked.length} uncommitted phase artifact(s) — ${untracked.join(', ')}`,
889
+ remediation,
890
+ false
891
+ );
892
+ }
893
+ } catch (err) {
894
+ addIssue(
895
+ 'warning',
896
+ 'W011',
897
+ `untracked-phase-artifacts check failed: ${err.message}`,
898
+ 'Investigate verify.cjs walker',
899
+ false
900
+ );
901
+ }
902
+
791
903
  // ─── Perform repairs if requested ─────────────────────────────────────────
792
904
  const repairActions = [];
793
905
  if (options.repair && repairs.length > 0) {
@@ -0,0 +1,82 @@
1
+ // deliver-great-systems/bin/lib/verify.test.cjs
2
+ // REL-12 untracked-scaffolding health check — initially RED. Turns GREEN after plan 04
3
+ // adds Check 11 to bin/lib/verify.cjs::cmdValidateHealth.
4
+
5
+ const test = require('node:test');
6
+ const assert = require('node:assert');
7
+ const fs = require('node:fs');
8
+ const path = require('node:path');
9
+ const os = require('node:os');
10
+ const { execSync } = require('node:child_process');
11
+
12
+ function setupREL12Fixture({ trackedKeeps, untrackedKeeps }) {
13
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rel12-'));
14
+ execSync('git init -q', { cwd: root });
15
+ execSync('git config user.email test@test', { cwd: root });
16
+ execSync('git config user.name test', { cwd: root });
17
+ fs.writeFileSync(path.join(root, 'PROJECT.md'),
18
+ '# Test\n## What This Is\n## Core Value\n## Requirements\n');
19
+ fs.writeFileSync(path.join(root, 'ROADMAP.md'), '# Roadmap\n');
20
+ fs.writeFileSync(path.join(root, 'STATE.md'), '# State\n');
21
+ fs.mkdirSync(path.join(root, 'phases'), { recursive: true });
22
+ // Create + track requested .gitkeep paths
23
+ for (const p of trackedKeeps) {
24
+ fs.mkdirSync(path.join(root, path.dirname(p)), { recursive: true });
25
+ fs.writeFileSync(path.join(root, p), '');
26
+ execSync(`git add "${p}"`, { cwd: root });
27
+ }
28
+ // Always commit at least PROJECT.md so HEAD exists even when no keeps are tracked
29
+ execSync('git add PROJECT.md ROADMAP.md STATE.md', { cwd: root });
30
+ execSync('git commit -q -m "init"', { cwd: root });
31
+ // Create untracked .gitkeep paths (not added to git)
32
+ for (const p of untrackedKeeps) {
33
+ fs.mkdirSync(path.join(root, path.dirname(p)), { recursive: true });
34
+ fs.writeFileSync(path.join(root, p), '');
35
+ }
36
+ return root;
37
+ }
38
+
39
+ function captureHealthOutput(root) {
40
+ // cmdValidateHealth calls output() which process.exit(0)s — run in a child
41
+ // process and capture stdout JSON. Pattern matches health-untracked-phase.test.cjs.
42
+ const verifyPath = path.resolve(__dirname, 'verify.cjs');
43
+ const shim = [
44
+ `const v = require(${JSON.stringify(verifyPath)});`,
45
+ `v.cmdValidateHealth(${JSON.stringify(root)}, { raw: true }, true);`,
46
+ ].join('\n');
47
+ const shimPath = path.join(os.tmpdir(), `rel12-shim-${process.pid}-${Date.now()}.cjs`);
48
+ fs.writeFileSync(shimPath, shim);
49
+ try {
50
+ const stdout = execSync(`node ${JSON.stringify(shimPath)}`, { encoding: 'utf-8' });
51
+ try { return JSON.parse(stdout); } catch { return { stdout, warnings: [] }; }
52
+ } catch (err) {
53
+ const out = err.stdout && err.stdout.toString();
54
+ try { return JSON.parse(out); } catch { return { stdout: out, warnings: [] }; }
55
+ } finally {
56
+ try { fs.unlinkSync(shimPath); } catch { /* ignore */ }
57
+ }
58
+ }
59
+
60
+ test('REL-12: cmdValidateHealth flags untracked .gitkeep in standard scaffold locations (untracked-scaffolding)', () => {
61
+ const root = setupREL12Fixture({
62
+ trackedKeeps: ['specs/.gitkeep'],
63
+ untrackedKeeps: ['docs/product/.gitkeep', 'quick/.gitkeep'],
64
+ });
65
+ const result = captureHealthOutput(root);
66
+ const scaffoldingWarnings = (result.warnings || []).filter(w => w.code === 'untracked-scaffolding');
67
+ assert.strictEqual(scaffoldingWarnings.length, 1, 'exactly one untracked-scaffolding warning expected');
68
+ assert.match(scaffoldingWarnings[0].message, /docs\/product\/\.gitkeep/);
69
+ assert.match(scaffoldingWarnings[0].message, /quick\/\.gitkeep/);
70
+ });
71
+
72
+ test('REL-12: cmdValidateHealth does NOT flag tracked .gitkeep files', () => {
73
+ const root = setupREL12Fixture({
74
+ trackedKeeps: ['specs/.gitkeep', 'docs/product/.gitkeep', 'quick/.gitkeep'],
75
+ untrackedKeeps: [],
76
+ });
77
+ const result = captureHealthOutput(root);
78
+ const scaffoldingWarnings = (result.warnings || []).filter(w => w.code === 'untracked-scaffolding');
79
+ assert.strictEqual(scaffoldingWarnings.length, 0, 'no untracked-scaffolding warning when all keeps tracked');
80
+ });
81
+
82
+ // REL-12 sentinel — flag this block as a Wave-0 RED scaffold for plan 04.