@ktpartners/dgs-platform 2.7.5 → 2.9.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 (57) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +15 -12
  3. package/agents/dgs-executor.md +0 -52
  4. package/deliver-great-systems/bin/dgs-tools.cjs +66 -10
  5. package/deliver-great-systems/bin/lib/commands.cjs +1 -8
  6. package/deliver-great-systems/bin/lib/config.cjs +9 -90
  7. package/deliver-great-systems/bin/lib/context.cjs +2 -2
  8. package/deliver-great-systems/bin/lib/context.test.cjs +100 -100
  9. package/deliver-great-systems/bin/lib/core.cjs +17 -57
  10. package/deliver-great-systems/bin/lib/core.test.cjs +166 -170
  11. package/deliver-great-systems/bin/lib/docs.cjs +3 -3
  12. package/deliver-great-systems/bin/lib/docs.test.cjs +14 -7
  13. package/deliver-great-systems/bin/lib/execution.cjs +2 -2
  14. package/deliver-great-systems/bin/lib/execution.test.cjs +65 -67
  15. package/deliver-great-systems/bin/lib/ideas.cjs +4 -4
  16. package/deliver-great-systems/bin/lib/ideas.test.cjs +45 -44
  17. package/deliver-great-systems/bin/lib/init.cjs +9 -4
  18. package/deliver-great-systems/bin/lib/init.test.cjs +242 -175
  19. package/deliver-great-systems/bin/lib/jobs.cjs +1 -1
  20. package/deliver-great-systems/bin/lib/jobs.test.cjs +203 -202
  21. package/deliver-great-systems/bin/lib/migration.cjs +256 -281
  22. package/deliver-great-systems/bin/lib/migration.test.cjs +385 -440
  23. package/deliver-great-systems/bin/lib/milestone.cjs +1 -1
  24. package/deliver-great-systems/bin/lib/overlap.cjs +4 -4
  25. package/deliver-great-systems/bin/lib/overlap.test.cjs +45 -44
  26. package/deliver-great-systems/bin/lib/path-audit.test.cjs +16 -22
  27. package/deliver-great-systems/bin/lib/paths.cjs +60 -59
  28. package/deliver-great-systems/bin/lib/paths.test.cjs +192 -225
  29. package/deliver-great-systems/bin/lib/phase.cjs +5 -4
  30. package/deliver-great-systems/bin/lib/projects.cjs +8 -8
  31. package/deliver-great-systems/bin/lib/projects.test.cjs +75 -74
  32. package/deliver-great-systems/bin/lib/repos.cjs +94 -230
  33. package/deliver-great-systems/bin/lib/repos.test.cjs +84 -75
  34. package/deliver-great-systems/bin/lib/search.cjs +4 -4
  35. package/deliver-great-systems/bin/lib/specs.cjs +2 -2
  36. package/deliver-great-systems/bin/lib/sync.cjs +1 -1
  37. package/deliver-great-systems/bin/lib/template.cjs +3 -3
  38. package/deliver-great-systems/bin/lib/test-helpers.cjs +59 -162
  39. package/deliver-great-systems/bin/lib/verify.cjs +3 -3
  40. package/deliver-great-systems/references/planning-config.md +7 -8
  41. package/deliver-great-systems/workflows/add-tests.md +1 -1
  42. package/deliver-great-systems/workflows/approve-spec.md +1 -11
  43. package/deliver-great-systems/workflows/complete-milestone.md +2 -2
  44. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  45. package/deliver-great-systems/workflows/create-milestone-job.md +2 -2
  46. package/deliver-great-systems/workflows/discuss-phase.md +2 -2
  47. package/deliver-great-systems/workflows/execute-phase.md +63 -4
  48. package/deliver-great-systems/workflows/execute-plan.md +0 -51
  49. package/deliver-great-systems/workflows/find-related-ideas.md +1 -1
  50. package/deliver-great-systems/workflows/help.md +55 -84
  51. package/deliver-great-systems/workflows/init-product.md +14 -451
  52. package/deliver-great-systems/workflows/map-codebase.md +109 -0
  53. package/deliver-great-systems/workflows/new-milestone.md +16 -6
  54. package/deliver-great-systems/workflows/new-project.md +22 -681
  55. package/deliver-great-systems/workflows/quick.md +2 -2
  56. package/deliver-great-systems/workflows/run-job.md +56 -0
  57. package/package.json +1 -1
@@ -144,7 +144,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
144
144
  // Archive REQUIREMENTS.md
145
145
  if (fs.existsSync(reqPath)) {
146
146
  const reqContent = fs.readFileSync(reqPath, 'utf-8');
147
- const archiveHeader = `# Requirements Archive: ${version} ${milestoneName}\n\n**Archived:** ${today}\n**Status:** SHIPPED\n\nFor current requirements, see \`.planning/REQUIREMENTS.md\`.\n\n---\n\n`;
147
+ const archiveHeader = `# Requirements Archive: ${version} ${milestoneName}\n\n**Archived:** ${today}\n**Status:** SHIPPED\n\nFor current requirements, see \`REQUIREMENTS.md\`.\n\n---\n\n`;
148
148
  fs.writeFileSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`), archiveHeader + reqContent, 'utf-8');
149
149
  }
150
150
 
@@ -21,7 +21,7 @@ const SEVERITY_ORDER = { HIGH: 0, MEDIUM: 1, LOW: 2 };
21
21
  /**
22
22
  * Scan a project's plan files and extract repos and files from XML tags.
23
23
  *
24
- * Scans all *-PLAN.md files under .planning/projects/<slug>/phases/ subdirectories.
24
+ * Scans all *-PLAN.md files under projects/<slug>/phases/ subdirectories.
25
25
  * For each plan file, extracts all <repos> and <files> tags.
26
26
  *
27
27
  * @param {string} cwd - Working directory (product root)
@@ -97,7 +97,7 @@ function scanProjectPlanFiles(cwd, slug) {
97
97
  * Filter out phase directories that are in archived milestone paths.
98
98
  *
99
99
  * Archived paths match patterns like:
100
- * .planning/milestones/v1.0-phases/01-setup
100
+ * milestones/v1.0-phases/01-setup
101
101
  *
102
102
  * @param {string[]} phaseDirs - Array of full phase directory paths
103
103
  * @returns {string[]} Filtered array with archived paths removed
@@ -181,7 +181,7 @@ function classifyOverlapSeverity(overlapEntry) {
181
181
  * Get slugs of all active (non-completed) projects.
182
182
  *
183
183
  * Reads PROJECTS.md to get active project slugs. Falls back to scanning
184
- * .planning/ subfolders if PROJECTS.md is missing. Applies ghost project
184
+ * subfolders if PROJECTS.md is missing. Applies ghost project
185
185
  * guard (skips folders without STATE.md).
186
186
  *
187
187
  * @param {string} cwd - Working directory (product root)
@@ -200,7 +200,7 @@ function getActiveProjectSlugs(cwd) {
200
200
  });
201
201
  }
202
202
 
203
- // Fallback: scan .planning/ subfolders
203
+ // Fallback: scan project subfolders
204
204
  const folders = getProjectFolders(cwd);
205
205
  return folders.filter(slug => {
206
206
  const state = readProjectState(cwd, slug);
@@ -8,16 +8,17 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const os = require('os');
10
10
 
11
- const { createTempDir, cleanupDir, writeFile } = require('./test-helpers.cjs');
11
+ const { createTempDir, cleanupDir, writeFile , initGitRepo } = require('./test-helpers.cjs');
12
+ const { resetPaths } = require('./paths.cjs');
12
13
 
13
- // Helper to create a minimal project with STATE.md under .planning/projects/<slug>/
14
+ // Helper to create a minimal project with STATE.md under projects/<slug>/
14
15
  function createProject(cwd, slug, status = 'Ready to execute') {
15
- writeFile(cwd, `.planning/projects/${slug}/STATE.md`, `# Project State\n\nPhase: 1\nStatus: ${status}\nProgress: [░░░░░░░░░░] 0%\n`);
16
- writeFile(cwd, `.planning/projects/${slug}/PROJECT.md`, `# Project: ${slug}\n`);
17
- fs.mkdirSync(path.join(cwd, '.planning', 'projects', slug, 'phases'), { recursive: true });
16
+ writeFile(cwd, `projects/${slug}/STATE.md`, `# Project State\n\nPhase: 1\nStatus: ${status}\nProgress: [░░░░░░░░░░] 0%\n`);
17
+ writeFile(cwd, `projects/${slug}/PROJECT.md`, `# Project: ${slug}\n`);
18
+ fs.mkdirSync(path.join(cwd, 'projects', slug, 'phases'), { recursive: true });
18
19
  }
19
20
 
20
- // Helper to create a plan file with repos and files tags under .planning/projects/<slug>/phases/
21
+ // Helper to create a plan file with repos and files tags under projects/<slug>/phases/
21
22
  function createPlanFile(cwd, slug, phaseDir, planName, tasks) {
22
23
  let content = `---\nphase: ${phaseDir}\nplan: 01\n---\n\n<tasks>\n`;
23
24
  for (const task of tasks) {
@@ -28,7 +29,7 @@ function createPlanFile(cwd, slug, phaseDir, planName, tasks) {
28
29
  content += ` <action>Do something</action>\n <verify>Check it</verify>\n <done>Done</done>\n</task>\n`;
29
30
  }
30
31
  content += '\n</tasks>\n';
31
- writeFile(cwd, `.planning/projects/${slug}/phases/${phaseDir}/${planName}`, content);
32
+ writeFile(cwd, `projects/${slug}/phases/${phaseDir}/${planName}`, content);
32
33
  }
33
34
 
34
35
  // Helper to create PROJECTS.md
@@ -45,7 +46,7 @@ function createProjectsMd(cwd, projects) {
45
46
  for (const p of projects.filter(p => p.status === 'completed')) {
46
47
  content += `| ${p.name} | ${p.completed || ''} | |\n`;
47
48
  }
48
- writeFile(cwd, '.planning/PROJECTS.md', content);
49
+ writeFile(cwd, 'PROJECTS.md', content);
49
50
  }
50
51
 
51
52
  const {
@@ -61,13 +62,13 @@ const {
61
62
 
62
63
  describe('scanProjectPlanFiles', () => {
63
64
  let tmpDir;
64
- beforeEach(() => { tmpDir = createTempDir(); });
65
- afterEach(() => { cleanupDir(tmpDir); });
65
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
66
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
66
67
 
67
68
  it('returns empty array when project has no phases directory', () => {
68
69
  createProject(tmpDir, 'project-a');
69
70
  // Remove phases dir
70
- fs.rmSync(path.join(tmpDir, '.planning', 'projects', 'project-a', 'phases'), { recursive: true, force: true });
71
+ fs.rmSync(path.join(tmpDir, 'projects', 'project-a', 'phases'), { recursive: true, force: true });
71
72
  const result = scanProjectPlanFiles(tmpDir, 'project-a');
72
73
  assert.ok(Array.isArray(result));
73
74
  assert.strictEqual(result.length, 0);
@@ -105,9 +106,9 @@ describe('scanProjectPlanFiles', () => {
105
106
  { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
106
107
  ]);
107
108
  // Create a SUMMARY.md file (should be skipped)
108
- writeFile(tmpDir, '.planning/projects/project-a/phases/01-setup/01-01-SUMMARY.md', '<repos>server</repos>\n<files>server/x.js</files>');
109
+ writeFile(tmpDir, 'projects/project-a/phases/01-setup/01-01-SUMMARY.md', '<repos>server</repos>\n<files>server/x.js</files>');
109
110
  // Create a CONTEXT.md file (should be skipped)
110
- writeFile(tmpDir, '.planning/projects/project-a/phases/01-setup/01-CONTEXT.md', '<repos>other</repos>');
111
+ writeFile(tmpDir, 'projects/project-a/phases/01-setup/01-CONTEXT.md', '<repos>other</repos>');
111
112
  const result = scanProjectPlanFiles(tmpDir, 'project-a');
112
113
  assert.strictEqual(result.length, 1);
113
114
  // Only web-app from the PLAN.md, not server from SUMMARY.md
@@ -128,7 +129,7 @@ describe('scanProjectPlanFiles', () => {
128
129
 
129
130
  it('handles plan files without repos or files tags', () => {
130
131
  createProject(tmpDir, 'project-a');
131
- writeFile(tmpDir, '.planning/projects/project-a/phases/01-setup/01-01-PLAN.md',
132
+ writeFile(tmpDir, 'projects/project-a/phases/01-setup/01-01-PLAN.md',
132
133
  '---\nphase: 01\n---\n\n<tasks>\n<task type="auto">\n <name>Task 1</name>\n <action>Do</action>\n</task>\n</tasks>\n'
133
134
  );
134
135
  const result = scanProjectPlanFiles(tmpDir, 'project-a');
@@ -142,13 +143,13 @@ describe('scanProjectPlanFiles', () => {
142
143
 
143
144
  describe('buildOverlapMatrix', () => {
144
145
  let tmpDir;
145
- beforeEach(() => { tmpDir = createTempDir(); });
146
- afterEach(() => { cleanupDir(tmpDir); });
146
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
147
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
147
148
 
148
149
  it('returns empty overlap when only one project exists', () => {
149
150
  createProject(tmpDir, 'project-a');
150
151
  createProjectsMd(tmpDir, [{ name: 'project-a', status: 'Active' }]);
151
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
152
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
152
153
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
153
154
  { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
154
155
  ]);
@@ -164,7 +165,7 @@ describe('buildOverlapMatrix', () => {
164
165
  { name: 'project-a', status: 'Active' },
165
166
  { name: 'project-b', status: 'Active' },
166
167
  ]);
167
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
168
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
168
169
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
169
170
  { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
170
171
  ]);
@@ -185,7 +186,7 @@ describe('buildOverlapMatrix', () => {
185
186
  { name: 'project-a', status: 'Active' },
186
187
  { name: 'project-b', status: 'Active' },
187
188
  ]);
188
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
189
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
189
190
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
190
191
  { name: 'Task 1', files: ['web-app/src/shared.js', 'web-app/src/a.js'], repos: ['web-app'] },
191
192
  ]);
@@ -210,7 +211,7 @@ describe('buildOverlapMatrix', () => {
210
211
  { name: 'project-a', status: 'Active' },
211
212
  { name: 'project-b', status: 'completed', completed: '2026-01-01' },
212
213
  ]);
213
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
214
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
214
215
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
215
216
  { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
216
217
  ]);
@@ -227,7 +228,7 @@ describe('buildOverlapMatrix', () => {
227
228
  { name: 'project-a', status: 'Active' },
228
229
  { name: 'ghost-project', status: 'Active' },
229
230
  ]);
230
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
231
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
231
232
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
232
233
  { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
233
234
  ]);
@@ -244,7 +245,7 @@ describe('buildOverlapMatrix', () => {
244
245
  { name: 'project-a', status: 'Active' },
245
246
  { name: 'project-b', status: 'Active' },
246
247
  ]);
247
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n| server | ./server |\n');
248
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n| server | ./server |\n');
248
249
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
249
250
  { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
250
251
  ]);
@@ -264,7 +265,7 @@ describe('buildOverlapMatrix', () => {
264
265
  { name: 'project-b', status: 'Active' },
265
266
  { name: 'project-c', status: 'Active' },
266
267
  ]);
267
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| shared | ./shared |\n');
268
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| shared | ./shared |\n');
268
269
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
269
270
  { name: 'Task 1', files: ['shared/lib.js'], repos: ['shared'] },
270
271
  ]);
@@ -286,7 +287,7 @@ describe('buildOverlapMatrix', () => {
286
287
  { name: 'project-a', status: 'Active' },
287
288
  { name: 'project-b', status: 'Active' },
288
289
  ]);
289
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| services-api | ./services/api |\n| services-api-gateway | ./services/api-gateway |\n');
290
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| services-api | ./services/api |\n| services-api-gateway | ./services/api-gateway |\n');
290
291
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
291
292
  { name: 'Task 1', files: ['services/api/shared.ts', 'services/api-gateway/handler.ts'], repos: ['services-api', 'services-api-gateway'] },
292
293
  ]);
@@ -314,7 +315,7 @@ describe('buildOverlapMatrix', () => {
314
315
  { name: 'project-a', status: 'Active' },
315
316
  { name: 'project-b', status: 'Active' },
316
317
  ]);
317
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
318
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
318
319
  // Repo-relative paths: src/index.ts instead of web-app/src/index.ts
319
320
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
320
321
  { name: 'Task 1', files: ['src/index.ts', 'src/app.ts'], repos: ['web-app'] },
@@ -341,7 +342,7 @@ describe('buildOverlapMatrix', () => {
341
342
  { name: 'project-a', status: 'Active' },
342
343
  { name: 'project-b', status: 'Active' },
343
344
  ]);
344
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
345
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
345
346
  // Legacy paths without repos tags — files include repo prefix
346
347
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
347
348
  { name: 'Task 1', files: ['web-app/src/shared.js'] },
@@ -361,13 +362,13 @@ describe('buildOverlapMatrix', () => {
361
362
 
362
363
  describe('excludeArchivedPhases', () => {
363
364
  let tmpDir;
364
- beforeEach(() => { tmpDir = createTempDir(); });
365
- afterEach(() => { cleanupDir(tmpDir); });
365
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
366
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
366
367
 
367
368
  it('keeps current phase directories', () => {
368
369
  const dirs = [
369
- path.join(tmpDir, '.planning/projects/project-a/phases/01-setup'),
370
- path.join(tmpDir, '.planning/projects/project-a/phases/02-build'),
370
+ path.join(tmpDir, 'projects/project-a/phases/01-setup'),
371
+ path.join(tmpDir, 'projects/project-a/phases/02-build'),
371
372
  ];
372
373
  const result = excludeArchivedPhases(dirs);
373
374
  assert.strictEqual(result.length, 2);
@@ -375,8 +376,8 @@ describe('excludeArchivedPhases', () => {
375
376
 
376
377
  it('excludes directories in milestones archive path', () => {
377
378
  const dirs = [
378
- path.join(tmpDir, '.planning/projects/project-a/phases/01-setup'),
379
- path.join(tmpDir, '.planning/milestones/v1.0-phases/01-setup'),
379
+ path.join(tmpDir, 'projects/project-a/phases/01-setup'),
380
+ path.join(tmpDir, 'milestones/v1.0-phases/01-setup'),
380
381
  ];
381
382
  const result = excludeArchivedPhases(dirs);
382
383
  assert.strictEqual(result.length, 1);
@@ -517,8 +518,8 @@ describe('classifyOverlapSeverity', () => {
517
518
 
518
519
  describe('buildOverlapMatrix severity', () => {
519
520
  let tmpDir;
520
- beforeEach(() => { tmpDir = createTempDir(); });
521
- afterEach(() => { cleanupDir(tmpDir); });
521
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
522
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
522
523
 
523
524
  it('two projects sharing same file -> severity HIGH', () => {
524
525
  createProject(tmpDir, 'project-a');
@@ -527,7 +528,7 @@ describe('buildOverlapMatrix severity', () => {
527
528
  { name: 'project-a', status: 'Active' },
528
529
  { name: 'project-b', status: 'Active' },
529
530
  ]);
530
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
531
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
531
532
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
532
533
  { name: 'Task 1', files: ['web-app/src/shared.js'], repos: ['web-app'] },
533
534
  ]);
@@ -547,7 +548,7 @@ describe('buildOverlapMatrix severity', () => {
547
548
  { name: 'project-a', status: 'Active' },
548
549
  { name: 'project-b', status: 'Active' },
549
550
  ]);
550
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
551
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
551
552
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
552
553
  { name: 'Task 1', files: ['web-app/src/auth/login.js'], repos: ['web-app'] },
553
554
  ]);
@@ -567,7 +568,7 @@ describe('buildOverlapMatrix severity', () => {
567
568
  { name: 'project-a', status: 'Active' },
568
569
  { name: 'project-b', status: 'Active' },
569
570
  ]);
570
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
571
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
571
572
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
572
573
  { name: 'Task 1', files: ['web-app/src/auth/login.js'], repos: ['web-app'] },
573
574
  ]);
@@ -587,7 +588,7 @@ describe('buildOverlapMatrix severity', () => {
587
588
  { name: 'project-a', status: 'Active' },
588
589
  { name: 'project-b', status: 'Active' },
589
590
  ]);
590
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
591
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
591
592
  createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
592
593
  { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
593
594
  ]);
@@ -607,7 +608,7 @@ describe('buildOverlapMatrix severity', () => {
607
608
  { name: 'project-a', status: 'Active' },
608
609
  { name: 'project-b', status: 'Active' },
609
610
  ]);
610
- writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| server | ./server |\n| web-app | ./web-app |\n');
611
+ writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| server | ./server |\n| web-app | ./web-app |\n');
611
612
  // Use separate plan files per repo so each plan has a single <repos> tag
612
613
  // This ensures files are mapped precisely to one repo each
613
614
  // project-a: server plan (shared.js) + web-app plan (auth)
@@ -694,8 +695,8 @@ describe('formatOverlapReport severity', () => {
694
695
 
695
696
  describe('getActiveProjectSlugs', () => {
696
697
  let tmpDir;
697
- beforeEach(() => { tmpDir = createTempDir(); });
698
- afterEach(() => { cleanupDir(tmpDir); });
698
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
699
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
699
700
 
700
701
  it('returns active slugs from PROJECTS.md', () => {
701
702
  createProject(tmpDir, 'project-a');
@@ -724,7 +725,7 @@ describe('getActiveProjectSlugs', () => {
724
725
  it('falls back to scanning folders when PROJECTS.md missing', () => {
725
726
  createProject(tmpDir, 'project-a');
726
727
  createProject(tmpDir, 'project-b');
727
- // No PROJECTS.md — should scan .planning/ subfolders
728
+ // No PROJECTS.md — should scan projects/ subfolders
728
729
  const result = getActiveProjectSlugs(tmpDir);
729
730
  assert.ok(result.includes('project-a'));
730
731
  assert.ok(result.includes('project-b'));
@@ -733,14 +734,14 @@ describe('getActiveProjectSlugs', () => {
733
734
  it('ghost project guard: skips folders without STATE.md', () => {
734
735
  createProject(tmpDir, 'project-a');
735
736
  // Create a ghost folder without STATE.md
736
- fs.mkdirSync(path.join(tmpDir, '.planning', 'projects', 'ghost-project'), { recursive: true });
737
+ fs.mkdirSync(path.join(tmpDir, 'projects', 'ghost-project'), { recursive: true });
737
738
  const result = getActiveProjectSlugs(tmpDir);
738
739
  assert.ok(result.includes('project-a'));
739
740
  assert.ok(!result.includes('ghost-project'));
740
741
  });
741
742
 
742
743
  it('returns empty array when no projects exist', () => {
743
- fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
744
+ // No projects directory exists
744
745
  const result = getActiveProjectSlugs(tmpDir);
745
746
  assert.strictEqual(result.length, 0);
746
747
  });
@@ -7,9 +7,8 @@
7
7
  * 2. GATES on library source files (path.join(cwd, '.planning') pattern)
8
8
  * 3. Documents baselines for lib module raw string literals
9
9
  *
10
- * Allowlisted files (may legitimately reference .planning/):
11
- * - workflows/init-product.md creates and describes both layouts
12
- * - workflows/help.md — user documentation explaining directory structure
10
+ * All file categories are gated -- no allowlisted files remain.
11
+ * init-product.md and help.md were cleaned in Phase 122.
13
12
  *
14
13
  * Allowlisted line patterns:
15
14
  * - HTML comments: <!-- .planning/ ... -->
@@ -45,30 +44,25 @@ const PROJECT_SCOPED_PATTERN = /\.planning\/(?:STATE|ROADMAP|PROJECT(?!S\.md)|RE
45
44
 
46
45
  /**
47
46
  * Workflow files allowlisted from comprehensive .planning/ scanning.
48
- * These files legitimately reference .planning/ for layout documentation.
47
+ * Empty -- all workflow files were cleaned in Phase 122.
49
48
  */
50
- const ALLOWLISTED_WORKFLOW_FILES = [
51
- 'workflows/init-product.md', // Creates and describes both layouts
52
- 'workflows/help.md', // User documentation explaining directory structure
53
- ];
49
+ const ALLOWLISTED_WORKFLOW_FILES = [];
54
50
 
55
51
  /**
56
52
  * Reference files allowlisted from comprehensive .planning/ scanning.
57
- * These files have legitimate .planning/ references in example content.
53
+ * Empty -- all reference files were cleaned in Phase 122.
58
54
  */
59
- const ALLOWLISTED_REFERENCE_FILES = [
60
- 'references/planning-config.md', // Has .planning/ in .gitignore example block
61
- ];
55
+ const ALLOWLISTED_REFERENCE_FILES = [];
62
56
 
63
57
  /**
64
58
  * Product-level paths that are intentionally at .planning/ root and should
65
59
  * NOT be flagged by the audit.
66
60
  */
67
61
  const SAFE_PRODUCT_PATHS = [
68
- '.planning/config.json',
69
- '.planning/PROJECTS.md',
70
- '.planning/REPOS.md',
71
- '.planning/codebase',
62
+ 'config.json',
63
+ 'PROJECTS.md',
64
+ 'REPOS.md',
65
+ 'codebase',
72
66
  ];
73
67
 
74
68
  /**
@@ -288,9 +282,9 @@ describe('path audit: comprehensive .planning/ reference scanning', () => {
288
282
 
289
283
  // Allowlist must stay minimal
290
284
  assert.ok(
291
- ALLOWLISTED_WORKFLOW_FILES.length <= 2,
292
- `Workflow allowlist too large (${ALLOWLISTED_WORKFLOW_FILES.length}). ` +
293
- 'Only init-product.md and help.md should be allowlisted. Migrate others.'
285
+ ALLOWLISTED_WORKFLOW_FILES.length === 0,
286
+ `Workflow allowlist should be empty (found ${ALLOWLISTED_WORKFLOW_FILES.length}). ` +
287
+ 'All workflow files were cleaned in Phase 122.'
294
288
  );
295
289
  });
296
290
 
@@ -302,9 +296,9 @@ describe('path audit: comprehensive .planning/ reference scanning', () => {
302
296
 
303
297
  // Allowlist must stay minimal
304
298
  assert.ok(
305
- ALLOWLISTED_REFERENCE_FILES.length <= 1,
306
- `Reference allowlist too large (${ALLOWLISTED_REFERENCE_FILES.length}). ` +
307
- 'Only planning-config.md should be allowlisted.'
299
+ ALLOWLISTED_REFERENCE_FILES.length === 0,
300
+ `Reference allowlist should be empty (found ${ALLOWLISTED_REFERENCE_FILES.length}). ` +
301
+ 'All reference files were cleaned in Phase 122.'
308
302
  );
309
303
  });
310
304
 
@@ -1,28 +1,36 @@
1
1
  /**
2
2
  * paths.cjs -- Planning root detection and path constants
3
3
  *
4
- * LEAF MODULE: Zero DGS imports. Uses only Node.js built-in fs and path.
5
- * This module sits at the bottom of the dependency graph to prevent
6
- * circular dependencies with core.cjs and config.cjs.
4
+ * LEAF MODULE: Zero DGS imports. Uses only Node.js built-in fs, path,
5
+ * and child_process. This module sits at the bottom of the dependency
6
+ * graph to prevent circular dependencies with core.cjs and config.cjs.
7
7
  *
8
8
  * Exports:
9
9
  * getPlanningRoot(cwd) -- Returns absolute path to planning root
10
10
  * getPaths(cwd) -- Returns frozen PATHS object with all standard paths
11
11
  * initPaths(cwd) -- Alias for getPaths (explicit cache population)
12
- * resetPaths() -- Clears the cwd-keyed cache (for test isolation)
12
+ * resetPaths() -- Clears caches (for test isolation)
13
+ * isV2Install(cwd) -- Detects V2 multi-project installation
13
14
  *
14
- * Detection cascade (getPlanningRoot):
15
- * 1. .planning/config.json or .planning/config.local.json exists -> .planning/ layout
16
- * 2. config.local.json at root with planningRoot: '.' -> root layout
17
- * (legacy fallback: dgs.config.json at root with planningRoot: '.')
18
- * 3. dgs.config.json at root without planningRoot: '.' -> .planning/ layout
19
- * 4. PROJECT.md at root AND no .planning/ directory -> root layout (auto-detect)
20
- * 5. No signals -> .planning/ layout (default)
15
+ * Root detection (getPlanningRoot):
16
+ * Single `git rev-parse --show-toplevel` call. Errors if not in a git repo.
17
+ * Result cached per-process.
21
18
  */
22
19
 
23
20
  const fs = require('fs');
24
21
  const path = require('path');
22
+ const { execSync } = require('child_process');
25
23
 
24
+ /**
25
+ * Per-process cache for getPlanningRoot result.
26
+ * One git repo = one root per process. Cleared by resetPaths().
27
+ */
28
+ let _planningRootCache = null;
29
+
30
+ /**
31
+ * Per-cwd cache for getPaths results.
32
+ * Different cwd values produce different absolute paths.
33
+ */
26
34
  const _cache = new Map();
27
35
 
28
36
  /**
@@ -34,57 +42,50 @@ const PROJECTS_DIR = 'projects';
34
42
 
35
43
  /**
36
44
  * Detect the planning root directory for the given cwd.
45
+ * Uses `git rev-parse --show-toplevel` to find the git repo root.
46
+ * Result is cached per-process for performance.
37
47
  *
38
48
  * @param {string} [cwd] - Working directory (defaults to process.cwd())
39
- * @returns {string} Absolute path to the planning root
49
+ * @returns {string} Absolute path to the planning root (git repo root)
40
50
  */
41
51
  function getPlanningRoot(cwd) {
42
- const resolved = path.resolve(cwd || process.cwd());
43
- const dotPlanning = path.join(resolved, '.planning');
44
-
45
- // Step 1: .planning/ layout -- config files inside .planning/
46
- if (
47
- fs.existsSync(path.join(dotPlanning, 'config.json')) ||
48
- fs.existsSync(path.join(dotPlanning, 'config.local.json')) ||
49
- fs.existsSync(path.join(dotPlanning, 'dgs.config.json'))
50
- ) {
51
- return dotPlanning;
52
- }
53
-
54
- // Step 2: Root layout -- config.local.json at repo root with planningRoot: '.'
55
- const rootLocalConfigPath = path.join(resolved, 'config.local.json');
56
- if (fs.existsSync(rootLocalConfigPath)) {
57
- try {
58
- const config = JSON.parse(fs.readFileSync(rootLocalConfigPath, 'utf-8'));
59
- if (config.planningRoot === '.') return resolved;
60
- } catch {
61
- // Malformed JSON -- fall through
62
- }
52
+ if (_planningRootCache !== null) return _planningRootCache;
53
+ try {
54
+ const root = execSync('git rev-parse --show-toplevel', {
55
+ cwd: cwd || process.cwd(),
56
+ encoding: 'utf-8',
57
+ stdio: ['pipe', 'pipe', 'pipe'],
58
+ }).trim();
59
+ _planningRootCache = root;
60
+ return root;
61
+ } catch {
62
+ process.stderr.write('Error: DGS requires a git repository. Run this command from inside a git repo.\n');
63
+ process.exit(1);
63
64
  }
65
+ }
64
66
 
65
- // Step 2b: Legacy fallback -- dgs.config.json at repo root
66
- const rootConfigPath = path.join(resolved, 'dgs.config.json');
67
- if (fs.existsSync(rootConfigPath)) {
67
+ /**
68
+ * Detect if this is a v2 (multi-project) installation.
69
+ * Requires both file existence AND valid DGS-generated content.
70
+ * PROJECTS.md must start with "# Projects", REPOS.md with "# Repos".
71
+ *
72
+ * @param {string} cwd - Working directory
73
+ * @returns {boolean}
74
+ */
75
+ function isV2Install(cwd) {
76
+ const checks = [
77
+ { file: 'PROJECTS.md', header: '# Projects' },
78
+ { file: 'REPOS.md', header: '# Repos' },
79
+ ];
80
+ const planRoot = getPlanningRoot(cwd);
81
+ for (const { file, header } of checks) {
82
+ const filePath = path.join(planRoot, file);
68
83
  try {
69
- const config = JSON.parse(fs.readFileSync(rootConfigPath, 'utf-8'));
70
- if (config.planningRoot === '.') return resolved;
71
- } catch {
72
- // Malformed JSON -- fall through to next check
73
- }
74
- // dgs.config.json exists but no planningRoot: '.' or malformed
75
- return dotPlanning;
76
- }
77
-
78
- // Step 3: Auto-detect root layout -- PROJECT.md at root, no .planning/ dir
79
- if (
80
- fs.existsSync(path.join(resolved, 'PROJECT.md')) &&
81
- !fs.existsSync(dotPlanning)
82
- ) {
83
- return resolved;
84
+ const content = fs.readFileSync(filePath, 'utf-8');
85
+ if (content.startsWith(header)) return true;
86
+ } catch { /* File doesn't exist or unreadable */ }
84
87
  }
85
-
86
- // Step 4: Default -- .planning/
87
- return dotPlanning;
88
+ return false;
88
89
  }
89
90
 
90
91
  /**
@@ -99,7 +100,6 @@ function getPaths(cwd) {
99
100
  if (_cache.has(resolved)) return _cache.get(resolved);
100
101
 
101
102
  const root = getPlanningRoot(resolved);
102
- const isRoot = (root === resolved);
103
103
 
104
104
  const paths = Object.freeze({
105
105
  ROOT: root,
@@ -112,14 +112,12 @@ function getPaths(cwd) {
112
112
  MILESTONES: path.join(root, 'milestones'),
113
113
  CONFIG: path.join(root, 'config.json'),
114
114
  CONFIG_LOCAL: path.join(root, 'config.local.json'),
115
- CONFIG_LEGACY: path.join(root, 'dgs.config.json'),
116
115
  QUICK: path.join(root, 'quick'),
117
116
  TODOS: path.join(root, 'todos'),
118
117
  RESEARCH: path.join(root, 'research'),
119
118
  DEBUG: path.join(root, 'debug'),
120
119
  ARCHIVE: path.join(root, 'archive'),
121
120
  PROJECTS: path.join(root, PROJECTS_DIR),
122
- LAYOUT: isRoot ? 'root' : 'dotplanning',
123
121
  });
124
122
 
125
123
  _cache.set(resolved, paths);
@@ -138,10 +136,12 @@ function initPaths(cwd) {
138
136
  }
139
137
 
140
138
  /**
141
- * Clear all cached PATHS entries.
142
- * Call in afterEach for test isolation between test cases.
139
+ * Clear all cached values.
140
+ * Clears both the per-process getPlanningRoot cache and the per-cwd
141
+ * getPaths cache. Call in afterEach for test isolation between test cases.
143
142
  */
144
143
  function resetPaths() {
144
+ _planningRootCache = null;
145
145
  _cache.clear();
146
146
  }
147
147
 
@@ -150,5 +150,6 @@ module.exports = {
150
150
  getPaths,
151
151
  initPaths,
152
152
  resetPaths,
153
+ isV2Install,
153
154
  PROJECTS_DIR,
154
155
  };