@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
@@ -0,0 +1,199 @@
1
+ // deliver-great-systems/bin/lib/fast-routing.cjs
2
+ //
3
+ // REL-05/06: Fast-mode commit routing helpers for /dgs:fast.
4
+ //
5
+ // Exports:
6
+ // surveyDirty(planningRoot) -> { planningRoot: { dirty, paths }, subRepos: [{ name, path, dirty, paths }], submodules: [{ path, dirty }] }
7
+ // decideRouting(survey) -> { action: 'route-to-planning-root'|'route-to-sub-repo'|'fail', repoCwd?, exitCode?, warnings?: [string], message?: string }
8
+ // enumerateSubmodules(repoPath) -> [{ path, dirty }]
9
+ //
10
+ // REL-05: decideRouting routes commits to the correct repo on multi-repo products.
11
+ // REL-06: surveyDirty + a thin pre-edit caller (in dgs-tools dispatcher) implements pre-edit dirt check.
12
+ //
13
+ // Both helpers honour the fail-loudly contract: surveyDirty returns structured data;
14
+ // decideRouting names the exit-code label (`multi-repo-dirt`) and includes a 1-3 line
15
+ // remediation message suitable for direct display.
16
+
17
+ const fs = require('node:fs');
18
+ const path = require('node:path');
19
+ const { execGit } = require('./core.cjs');
20
+
21
+ // Inline REPOS.md parser — does NOT use repos.cjs's parseReposMd because that
22
+ // helper goes through getPlanningRoot() which has a per-process cache that
23
+ // breaks tests using multiple temp planning roots in the same process.
24
+ function parseReposMdLocal(planningRoot) {
25
+ const filePath = path.join(planningRoot, 'REPOS.md');
26
+ let content;
27
+ try {
28
+ content = fs.readFileSync(filePath, 'utf-8');
29
+ } catch {
30
+ return { repos: [] };
31
+ }
32
+ if (!content || !content.startsWith('# Repos')) return { repos: [] };
33
+ const lines = content.split('\n');
34
+ let tableStart = -1;
35
+ for (let i = 0; i < lines.length; i++) {
36
+ if (lines[i].startsWith('|') && lines[i].includes('Name')) { tableStart = i; break; }
37
+ }
38
+ if (tableStart === -1) return { repos: [] };
39
+ const repos = [];
40
+ for (let i = tableStart + 2; i < lines.length; i++) {
41
+ const line = lines[i].trim();
42
+ if (!line.startsWith('|')) break;
43
+ const cells = line.split('|').map(c => c.trim()).filter((_, idx, arr) => idx > 0 && idx < arr.length - 1);
44
+ if (cells.length < 2) continue;
45
+ const [name, p] = cells;
46
+ if (!name || !p) continue;
47
+ repos.push({ name, path: p });
48
+ }
49
+ return { repos };
50
+ }
51
+
52
+ function readConfigLocalJson(planningRoot) {
53
+ const cfgPath = path.join(planningRoot, 'config.local.json');
54
+ if (!fs.existsSync(cfgPath)) return {};
55
+ try {
56
+ return JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
57
+ } catch {
58
+ return {};
59
+ }
60
+ }
61
+
62
+ function enumerateRegisteredRepos(planningRoot) {
63
+ const seen = new Map(); // absolute path -> { name, path }
64
+
65
+ // Source 1: REPOS.md (inline parser, bypassing repos.cjs cache)
66
+ const reposMd = parseReposMdLocal(planningRoot);
67
+ for (const r of (reposMd.repos || [])) {
68
+ const abs = path.resolve(planningRoot, r.path);
69
+ if (!seen.has(abs)) seen.set(abs, { name: r.name, path: abs });
70
+ }
71
+
72
+ // Source 2: config.local.json projects.*.worktrees.*.repos (map of name -> absolute path)
73
+ const cfg = readConfigLocalJson(planningRoot);
74
+ const projects = (cfg.projects || {});
75
+ for (const proj of Object.values(projects)) {
76
+ const wts = (proj.worktrees || {});
77
+ for (const wt of Object.values(wts)) {
78
+ const repos = (wt.repos || {});
79
+ for (const [name, abs] of Object.entries(repos)) {
80
+ if (typeof abs !== 'string') continue;
81
+ const resolved = path.resolve(abs);
82
+ if (!seen.has(resolved)) seen.set(resolved, { name, path: resolved });
83
+ }
84
+ }
85
+ }
86
+
87
+ return Array.from(seen.values());
88
+ }
89
+
90
+ function isGitRepo(p) {
91
+ try {
92
+ const res = execGit(p, ['rev-parse', '--is-inside-work-tree']);
93
+ if (!res || res.exitCode !== 0) return false;
94
+ return typeof res.stdout === 'string' && res.stdout.trim() === 'true';
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ function porcelainPaths(repoPath) {
101
+ if (!isGitRepo(repoPath)) return null;
102
+ let out = '';
103
+ try {
104
+ const res = execGit(repoPath, ['status', '--porcelain']);
105
+ out = (res && res.stdout) || '';
106
+ } catch {
107
+ out = '';
108
+ }
109
+ // git status --porcelain format: "XY <path>" where XY is a 2-char status code
110
+ // Strip the 2-char status + 1 space; trim and filter empties.
111
+ return out
112
+ .split('\n')
113
+ .map(l => l.replace(/^.{2,3}/, '').trim())
114
+ .filter(Boolean);
115
+ }
116
+
117
+ function enumerateSubmodules(repoPath) {
118
+ if (!isGitRepo(repoPath)) return [];
119
+ let out = '';
120
+ try {
121
+ const res = execGit(repoPath, ['submodule', 'status']);
122
+ out = (res && res.stdout) || '';
123
+ } catch {
124
+ out = '';
125
+ }
126
+ return out
127
+ .split('\n')
128
+ .filter(Boolean)
129
+ .map(line => {
130
+ // Format: ` <sha> <path> (<ref>)` — leading char `+` (mismatch),
131
+ // `-` (uninitialised), ` ` (clean), `U` (conflict)
132
+ const lead = line[0];
133
+ const match = line.trim().match(/^[+\- U]?\w+\s+(\S+)/);
134
+ return {
135
+ path: match ? match[1] : line.trim(),
136
+ dirty: Boolean(lead && lead !== ' '),
137
+ };
138
+ });
139
+ }
140
+
141
+ function surveyDirty(planningRoot) {
142
+ const planningPaths = porcelainPaths(planningRoot) || [];
143
+ const planningRootEntry = { dirty: planningPaths.length > 0, paths: planningPaths };
144
+
145
+ const subRepos = [];
146
+ for (const repo of enumerateRegisteredRepos(planningRoot)) {
147
+ if (path.resolve(repo.path) === path.resolve(planningRoot)) continue; // skip if same dir
148
+ const paths = porcelainPaths(repo.path) || [];
149
+ subRepos.push({ name: repo.name, path: repo.path, dirty: paths.length > 0, paths });
150
+ }
151
+
152
+ const submodules = enumerateSubmodules(planningRoot);
153
+
154
+ return { planningRoot: planningRootEntry, subRepos, submodules };
155
+ }
156
+
157
+ function decideRouting(survey) {
158
+ const warnings = [];
159
+ const dirtySubmodules = (survey.submodules || []).filter(s => s.dirty);
160
+ for (const sm of dirtySubmodules) {
161
+ warnings.push(
162
+ `submodule changes detected at ${sm.path}; auto-routing not supported for submodules — commit manually or use /dgs:quick`
163
+ );
164
+ }
165
+
166
+ const planningRoot = survey.planningRoot || { dirty: false };
167
+ const subRepos = survey.subRepos || [];
168
+ const dirtySubRepos = subRepos.filter(r => r.dirty);
169
+
170
+ // Case (c): multiple sub-repos dirty OR planning + at least one sub-repo dirty
171
+ if (dirtySubRepos.length > 1 || (dirtySubRepos.length >= 1 && planningRoot.dirty)) {
172
+ return {
173
+ action: 'fail',
174
+ exitCode: 'multi-repo-dirt',
175
+ message:
176
+ 'Multiple repos have dirty changes — fast-mode cannot auto-route. Review state with `git status` in each repo, run `/dgs:fast --repo <name>` per repo, or use `/dgs:quick` for multi-repo work.',
177
+ warnings,
178
+ };
179
+ }
180
+
181
+ // Case (a): exactly one sub-repo dirty + planning clean
182
+ if (dirtySubRepos.length === 1 && !planningRoot.dirty) {
183
+ return {
184
+ action: 'route-to-sub-repo',
185
+ repoCwd: dirtySubRepos[0].path,
186
+ warnings,
187
+ };
188
+ }
189
+
190
+ // Case (b): only planning root dirty (or nothing dirty — same code path)
191
+ return { action: 'route-to-planning-root', warnings };
192
+ }
193
+
194
+ module.exports = {
195
+ surveyDirty,
196
+ decideRouting,
197
+ enumerateSubmodules,
198
+ enumerateRegisteredRepos,
199
+ };
@@ -0,0 +1,108 @@
1
+ // deliver-great-systems/bin/lib/fast-routing.test.cjs
2
+ // REL-05/06 regression test scaffold — initially RED. Turns GREEN after plan 03
3
+ // implements bin/lib/fast-routing.cjs with surveyDirty + decideRouting helpers.
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 getModule() {
13
+ try {
14
+ return require('./fast-routing.cjs');
15
+ } catch (err) {
16
+ assert.fail('REL-05/06 module not yet implemented: bin/lib/fast-routing.cjs (plan 03 task)');
17
+ }
18
+ }
19
+
20
+ function setupMultiRepoFixture() {
21
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fast-routing-'));
22
+ execSync('git init -q', { cwd: root });
23
+ execSync('git config user.email test@test', { cwd: root });
24
+ execSync('git config user.name test', { cwd: root });
25
+ const subRepo = path.join(root, '..', `fast-routing-sub-${path.basename(root)}`);
26
+ fs.mkdirSync(subRepo);
27
+ execSync('git init -q', { cwd: subRepo });
28
+ execSync('git config user.email test@test', { cwd: subRepo });
29
+ execSync('git config user.name test', { cwd: subRepo });
30
+ fs.writeFileSync(
31
+ path.join(root, 'REPOS.md'),
32
+ `# Repos\n\n| Name | Path | GitHub URL | Description |\n|------|------|------------|-------------|\n| sub | ${subRepo} | git@x:y.git | test sub-repo |\n`
33
+ );
34
+ // Commit something so the planning-root tree isn't entirely empty
35
+ execSync('git add REPOS.md', { cwd: root });
36
+ execSync('git commit -q -m init', { cwd: root });
37
+ return { root, subRepo };
38
+ }
39
+
40
+ test('REL-06: surveyDirty detects pre-existing dirt in planning root (pre-existing-dirt)', () => {
41
+ const m = getModule();
42
+ const { root } = setupMultiRepoFixture();
43
+ fs.writeFileSync(path.join(root, 'dirty.txt'), 'pre-existing');
44
+ const survey = m.surveyDirty(root);
45
+ assert.ok(survey.planningRoot.dirty, 'planning root should be flagged dirty');
46
+ assert.ok((survey.planningRoot.paths || []).some(p => /dirty\.txt/.test(p)), 'dirty.txt must appear in paths');
47
+ });
48
+
49
+ test('REL-06: surveyDirty detects pre-existing dirt in registered sub-repo', () => {
50
+ const m = getModule();
51
+ const { root, subRepo } = setupMultiRepoFixture();
52
+ fs.writeFileSync(path.join(subRepo, 'dirty.txt'), 'pre-existing');
53
+ const survey = m.surveyDirty(root);
54
+ const subEntry = (survey.subRepos || []).find(r => r.name === 'sub');
55
+ assert.ok(subEntry && subEntry.dirty, 'sub-repo should be flagged dirty');
56
+ });
57
+
58
+ test('REL-05: decideRouting returns route-to-sub-repo for single-sub-repo dirty case', () => {
59
+ const m = getModule();
60
+ const survey = {
61
+ planningRoot: { dirty: false, paths: [] },
62
+ subRepos: [{ name: 'sub', path: '/tmp/sub', dirty: true, paths: ['file.txt'] }],
63
+ submodules: [],
64
+ };
65
+ const decision = m.decideRouting(survey);
66
+ assert.strictEqual(decision.action, 'route-to-sub-repo');
67
+ assert.strictEqual(decision.repoCwd, '/tmp/sub');
68
+ });
69
+
70
+ test('REL-05: decideRouting returns route-to-planning-root for planning-only dirty case', () => {
71
+ const m = getModule();
72
+ const survey = {
73
+ planningRoot: { dirty: true, paths: ['a.md'] },
74
+ subRepos: [{ name: 'sub', path: '/tmp/sub', dirty: false, paths: [] }],
75
+ submodules: [],
76
+ };
77
+ const decision = m.decideRouting(survey);
78
+ assert.strictEqual(decision.action, 'route-to-planning-root');
79
+ });
80
+
81
+ test('REL-05: decideRouting fails with multi-repo-dirt for multiple-dirty case', () => {
82
+ const m = getModule();
83
+ const survey = {
84
+ planningRoot: { dirty: true, paths: ['a.md'] },
85
+ subRepos: [{ name: 'sub', path: '/tmp/sub', dirty: true, paths: ['b.txt'] }],
86
+ submodules: [],
87
+ };
88
+ const decision = m.decideRouting(survey);
89
+ assert.strictEqual(decision.action, 'fail');
90
+ assert.strictEqual(decision.exitCode, 'multi-repo-dirt');
91
+ });
92
+
93
+ test('REL-05: decideRouting warns on submodules but does not auto-route them', () => {
94
+ const m = getModule();
95
+ const survey = {
96
+ planningRoot: { dirty: false, paths: [] },
97
+ subRepos: [{ name: 'sub', path: '/tmp/sub', dirty: true, paths: ['file.txt'] }],
98
+ submodules: [{ path: 'vendor/lib', dirty: true }],
99
+ };
100
+ const decision = m.decideRouting(survey);
101
+ assert.strictEqual(decision.action, 'route-to-sub-repo');
102
+ assert.ok(
103
+ Array.isArray(decision.warnings) && decision.warnings.some(w => /submodule/i.test(w)),
104
+ 'submodule warning must be present in decision.warnings'
105
+ );
106
+ });
107
+
108
+ // REL-05/06 sentinel — flag this file as a Wave-0 RED scaffold for plan 03.
@@ -0,0 +1,87 @@
1
+ // deliver-great-systems/bin/lib/final-commit-precondition.test.cjs
2
+ // REL-08 regression test scaffold — initially RED. Turns GREEN after plan 02
3
+ // adds the pre-commit precondition gate to executor's <final_commit> step.
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
+ // Resolve dgs-tools.cjs CLI: __dirname = .../deliver-great-systems/bin/lib
13
+ // CLI lives at .../deliver-great-systems/bin/dgs-tools.cjs
14
+ const CLI = path.resolve(__dirname, '..', 'dgs-tools.cjs');
15
+
16
+ function setupFixture({ planRequirements, summaryRequirementsCompleted }) {
17
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rel08-'));
18
+ execSync('git init -q', { cwd: tmpDir });
19
+ execSync('git config user.email test@test', { cwd: tmpDir });
20
+ execSync('git config user.name test', { cwd: tmpDir });
21
+ fs.writeFileSync(
22
+ path.join(tmpDir, 'PLAN.md'),
23
+ `---\nphase: test\nplan: 01\nrequirements:\n${planRequirements.map(r => ` - ${r}`).join('\n')}\n---\n`
24
+ );
25
+ fs.writeFileSync(
26
+ path.join(tmpDir, 'SUMMARY.md'),
27
+ `---\nphase: test\nplan: 01\nrequirements_completed: [${summaryRequirementsCompleted.join(', ')}]\n---\n`
28
+ );
29
+ return tmpDir;
30
+ }
31
+
32
+ test('REL-08: precondition aborts with summary-frontmatter-mismatch when PLAN non-empty AND SUMMARY empty', () => {
33
+ const fixture = setupFixture({ planRequirements: ['TEST-01'], summaryRequirementsCompleted: [] });
34
+
35
+ let exitCode = 0;
36
+ let combined = '';
37
+ try {
38
+ execSync(`node ${CLI} final-commit-precondition --plan ${fixture}/PLAN.md --summary ${fixture}/SUMMARY.md`, { stdio: 'pipe' });
39
+ } catch (err) {
40
+ exitCode = err.status;
41
+ combined = (err.stderr ? err.stderr.toString() : '') + (err.stdout ? err.stdout.toString() : '');
42
+ }
43
+
44
+ // RED: the CLI subcommand doesn't exist yet, so execSync throws with a different error
45
+ // (CLI says "Unknown command: final-commit-precondition" or similar — does NOT include the literal label).
46
+ assert.notStrictEqual(exitCode, 0, 'should exit non-zero on mismatch');
47
+ assert.match(combined, /summary-frontmatter-mismatch/, 'stderr must include the exit-code label');
48
+ });
49
+
50
+ test('REL-08: precondition leaves working tree unchanged on abort (REL-08 fail-loudly contract)', () => {
51
+ const fixture = setupFixture({ planRequirements: ['TEST-01'], summaryRequirementsCompleted: [] });
52
+ // Snapshot working-tree state BEFORE precondition runs
53
+ const before = execSync('git status --porcelain', { cwd: fixture }).toString();
54
+
55
+ try {
56
+ execSync(`node ${CLI} final-commit-precondition --plan ${fixture}/PLAN.md --summary ${fixture}/SUMMARY.md`, { stdio: 'pipe' });
57
+ } catch (_) { /* expected non-zero */ }
58
+
59
+ const after = execSync('git status --porcelain', { cwd: fixture }).toString();
60
+ assert.strictEqual(after, before, 'working tree must be unchanged after precondition abort');
61
+ });
62
+
63
+ test('REL-08: precondition is a no-op (exit 0) when PLAN requirements is empty', () => {
64
+ const fixture = setupFixture({ planRequirements: [], summaryRequirementsCompleted: [] });
65
+
66
+ let exitCode = 0;
67
+ try {
68
+ execSync(`node ${CLI} final-commit-precondition --plan ${fixture}/PLAN.md --summary ${fixture}/SUMMARY.md`, { stdio: 'pipe' });
69
+ } catch (err) {
70
+ exitCode = err.status;
71
+ }
72
+ assert.strictEqual(exitCode, 0, 'precondition is no-op when PLAN requirements is empty');
73
+ });
74
+
75
+ test('REL-08: precondition succeeds (exit 0) when SUMMARY requirements_completed matches PLAN', () => {
76
+ const fixture = setupFixture({ planRequirements: ['TEST-01'], summaryRequirementsCompleted: ['TEST-01'] });
77
+
78
+ let exitCode = 0;
79
+ try {
80
+ execSync(`node ${CLI} final-commit-precondition --plan ${fixture}/PLAN.md --summary ${fixture}/SUMMARY.md`, { stdio: 'pipe' });
81
+ } catch (err) {
82
+ exitCode = err.status;
83
+ }
84
+ assert.strictEqual(exitCode, 0, 'precondition exits 0 when SUMMARY matches PLAN');
85
+ });
86
+
87
+ // REL-08 sentinel — flag this file as a Wave-0 RED scaffold for plan 02.
@@ -0,0 +1,21 @@
1
+ {
2
+ "results": [
3
+ {
4
+ "type": "unpatched_gem",
5
+ "advisory": {
6
+ "id": "CVE-2020-8164",
7
+ "url": "https://groups.google.com/g/rubyonrails-security/c/f6ioe4sdpbY",
8
+ "title": "Possible Strong Parameters Bypass in ActionPack",
9
+ "description": "There is a strong parameters bypass vector in ActionPack versions 5.2.4.3 and below.",
10
+ "cvss_v3": 7.5,
11
+ "cvss_v2": 5.0,
12
+ "criticality": "high",
13
+ "solution": "Upgrade to activerecord >= 5.2.4.3"
14
+ },
15
+ "gem": {
16
+ "name": "activerecord",
17
+ "version": "5.2.3"
18
+ }
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,186 @@
1
+ ---
2
+ type: "package-scan"
3
+ date: "2026-04-18"
4
+ tool: "mixed"
5
+ snyk_org: null
6
+ repos_scanned: 3
7
+ critical: 1
8
+ high: 1
9
+ medium: 2
10
+ low: 0
11
+ duration: 5
12
+ findings:
13
+ - id: "pkg-001"
14
+ test_source: "package-scan"
15
+ gap_type: "dependency-security"
16
+ severity: "high"
17
+ resource_id: "lodash@4.17.15"
18
+ repo: "api"
19
+ manifest_path: "packages/api/package.json"
20
+ title: "Command Injection in lodash"
21
+ description: "Versions of lodash prior to 4.17.21 are vulnerable to Command Injection."
22
+ remediation: "upgrade to lodash@4.17.21"
23
+ reference: "https://snyk.io/vuln/SNYK-JS-LODASH-1040724"
24
+ cve: "CVE-2021-23337"
25
+ cvss: 7.2
26
+ dependency_chain:
27
+ - "api@1.0.0"
28
+ - "lodash@4.17.15"
29
+ chain_available: true
30
+ direct_or_transitive: "direct"
31
+ tool: "snyk"
32
+ introduced_in_commit: null
33
+ introduced_in_plan: null
34
+ - id: "pkg-002"
35
+ test_source: "package-scan"
36
+ gap_type: "dependency-security"
37
+ severity: "medium"
38
+ resource_id: "gpl-licensed-dep@2.0.0"
39
+ repo: "api"
40
+ manifest_path: "packages/api/package.json"
41
+ title: "Prototype Pollution in gpl-licensed-dep"
42
+ description: |-
43
+ Multi-line
44
+ description with
45
+ embedded newlines.
46
+ remediation: null
47
+ reference: "https://example.com/advisory"
48
+ cve: null
49
+ cvss: null
50
+ dependency_chain: null
51
+ chain_available: false
52
+ direct_or_transitive: "transitive"
53
+ tool: "snyk"
54
+ introduced_in_commit: null
55
+ introduced_in_plan: null
56
+ - id: "pkg-002-lic"
57
+ test_source: "package-scan"
58
+ gap_type: "dependency-licence"
59
+ severity: "high"
60
+ resource_id: "gpl-licensed-dep@2.0.0"
61
+ repo: "api"
62
+ manifest_path: "packages/api/package.json"
63
+ title: "Restrictive licence: GPL-3.0"
64
+ description: "Package gpl-licensed-dep@2.0.0 is licensed under GPL-3.0. Using this dependency may impose copyleft obligations on your project."
65
+ remediation: "Review licence compatibility or replace with a permissive-licensed alternative."
66
+ reference: null
67
+ cve: null
68
+ cvss: null
69
+ dependency_chain: null
70
+ chain_available: false
71
+ direct_or_transitive: "transitive"
72
+ tool: "snyk"
73
+ introduced_in_commit: null
74
+ introduced_in_plan: null
75
+ - id: "pkg-003"
76
+ test_source: "package-scan"
77
+ gap_type: "dependency-security"
78
+ severity: "medium"
79
+ resource_id: "requests@2.25.0"
80
+ repo: "worker"
81
+ manifest_path: null
82
+ title: "Unintended leak of Proxy-Authorization header"
83
+ description: "Requests is a HTTP library."
84
+ remediation: "pip install requests==2.31.0"
85
+ reference: null
86
+ cve: "CVE-2023-32681"
87
+ cvss: null
88
+ dependency_chain: null
89
+ chain_available: false
90
+ direct_or_transitive: null
91
+ tool: "pip-audit"
92
+ introduced_in_commit: null
93
+ introduced_in_plan: null
94
+ - id: "pkg-004"
95
+ test_source: "package-scan"
96
+ gap_type: "dependency-security"
97
+ severity: "critical"
98
+ resource_id: "express"
99
+ repo: "_product_root"
100
+ manifest_path: null
101
+ title: "express Critical vulnerability"
102
+ description: null
103
+ remediation: null
104
+ reference: "https://github.com/advisories/GHSA-xxxx"
105
+ cve: null
106
+ cvss: 9.8
107
+ dependency_chain: null
108
+ chain_available: false
109
+ direct_or_transitive: "direct"
110
+ tool: "npm-audit"
111
+ introduced_in_commit: null
112
+ introduced_in_plan: null
113
+ ---
114
+
115
+ # Package Scan Report
116
+
117
+ ## Summary
118
+
119
+ | Repo | Ecosystem | Tool | .snyk policy | Critical | High | Medium | Low | Status |
120
+ |------|-----------|------|--------------|----------|------|--------|-----|--------|
121
+ | api | node | snyk | — | 0 | 1 | 1 | 0 | ok |
122
+ | worker | python | pip-audit | — | 0 | 0 | 1 | 0 | ok |
123
+ | _product_root | node | npm-audit | — | 1 | 0 | 0 | 0 | ok |
124
+
125
+ ## Licence Compliance
126
+
127
+ > Licence scan incomplete -- use Snyk for full coverage.
128
+
129
+ ## Critical
130
+
131
+ ### _product_root: express — express Critical vulnerability
132
+ - **CVE:** unavailable
133
+ - **CVSS:** 9.8
134
+ - **Tool:** npm-audit
135
+ - **Manifest:** repo root
136
+ - **Direct/Transitive:** direct
137
+ - **Dependency chain:** unavailable (chain_available: false — recommend Snyk for full chain analysis)
138
+ - **Fix:** no upgrade path available — manual review required
139
+ - **Reference:** https://github.com/advisories/GHSA-xxxx
140
+ - **Introduced in:** unknown
141
+
142
+ ## High
143
+
144
+ ### api: lodash@4.17.15 — Command Injection in lodash
145
+ - **CVE:** CVE-2021-23337
146
+ - **CVSS:** 7.2
147
+ - **Tool:** snyk
148
+ - **Manifest:** `packages/api/package.json`
149
+ - **Direct/Transitive:** direct
150
+ - **Dependency chain:** api@1.0.0 → lodash@4.17.15
151
+ - **Fix:** upgrade to lodash@4.17.21
152
+ - **Reference:** https://snyk.io/vuln/SNYK-JS-LODASH-1040724
153
+ - **Introduced in:** unknown
154
+
155
+ > Versions of lodash prior to 4.17.21 are vulnerable to Command Injection.
156
+
157
+ ## Medium
158
+
159
+ ### api: gpl-licensed-dep@2.0.0 — Prototype Pollution in gpl-licensed-dep
160
+ - **CVE:** unavailable
161
+ - **CVSS:** unavailable
162
+ - **Tool:** snyk
163
+ - **Manifest:** `packages/api/package.json`
164
+ - **Direct/Transitive:** transitive
165
+ - **Dependency chain:** unavailable (chain_available: false — recommend Snyk for full chain analysis)
166
+ - **Fix:** no upgrade path available — manual review required
167
+ - **Reference:** https://example.com/advisory
168
+ - **Introduced in:** unknown
169
+
170
+ > Multi-line
171
+ > description with
172
+ > embedded newlines.
173
+
174
+ ### worker: requests@2.25.0 — Unintended leak of Proxy-Authorization header
175
+ - **CVE:** CVE-2023-32681
176
+ - **CVSS:** unavailable
177
+ - **Tool:** pip-audit
178
+ - **Manifest:** repo root
179
+ - **Direct/Transitive:** unknown
180
+ - **Dependency chain:** unavailable (chain_available: false — recommend Snyk for full chain analysis)
181
+ - **Fix:** pip install requests==2.31.0
182
+ - **Reference:** unavailable
183
+ - **Introduced in:** unknown
184
+
185
+ > Requests is a HTTP library.
186
+