@ktpartners/dgs-platform 2.7.5 → 2.8.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 (55) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/agents/dgs-executor.md +0 -52
  3. package/deliver-great-systems/bin/dgs-tools.cjs +66 -10
  4. package/deliver-great-systems/bin/lib/commands.cjs +1 -8
  5. package/deliver-great-systems/bin/lib/config.cjs +9 -90
  6. package/deliver-great-systems/bin/lib/context.cjs +2 -2
  7. package/deliver-great-systems/bin/lib/context.test.cjs +100 -100
  8. package/deliver-great-systems/bin/lib/core.cjs +17 -57
  9. package/deliver-great-systems/bin/lib/core.test.cjs +166 -170
  10. package/deliver-great-systems/bin/lib/docs.cjs +3 -3
  11. package/deliver-great-systems/bin/lib/docs.test.cjs +14 -7
  12. package/deliver-great-systems/bin/lib/execution.cjs +2 -2
  13. package/deliver-great-systems/bin/lib/execution.test.cjs +65 -67
  14. package/deliver-great-systems/bin/lib/ideas.cjs +4 -4
  15. package/deliver-great-systems/bin/lib/ideas.test.cjs +45 -44
  16. package/deliver-great-systems/bin/lib/init.cjs +9 -4
  17. package/deliver-great-systems/bin/lib/init.test.cjs +242 -175
  18. package/deliver-great-systems/bin/lib/jobs.cjs +1 -1
  19. package/deliver-great-systems/bin/lib/jobs.test.cjs +203 -202
  20. package/deliver-great-systems/bin/lib/migration.cjs +256 -281
  21. package/deliver-great-systems/bin/lib/migration.test.cjs +385 -440
  22. package/deliver-great-systems/bin/lib/milestone.cjs +1 -1
  23. package/deliver-great-systems/bin/lib/overlap.cjs +4 -4
  24. package/deliver-great-systems/bin/lib/overlap.test.cjs +45 -44
  25. package/deliver-great-systems/bin/lib/path-audit.test.cjs +16 -22
  26. package/deliver-great-systems/bin/lib/paths.cjs +60 -59
  27. package/deliver-great-systems/bin/lib/paths.test.cjs +192 -225
  28. package/deliver-great-systems/bin/lib/phase.cjs +5 -4
  29. package/deliver-great-systems/bin/lib/projects.cjs +8 -8
  30. package/deliver-great-systems/bin/lib/projects.test.cjs +75 -74
  31. package/deliver-great-systems/bin/lib/repos.cjs +94 -230
  32. package/deliver-great-systems/bin/lib/repos.test.cjs +84 -75
  33. package/deliver-great-systems/bin/lib/search.cjs +4 -4
  34. package/deliver-great-systems/bin/lib/specs.cjs +2 -2
  35. package/deliver-great-systems/bin/lib/sync.cjs +1 -1
  36. package/deliver-great-systems/bin/lib/template.cjs +3 -3
  37. package/deliver-great-systems/bin/lib/test-helpers.cjs +59 -162
  38. package/deliver-great-systems/bin/lib/verify.cjs +3 -3
  39. package/deliver-great-systems/references/planning-config.md +7 -8
  40. package/deliver-great-systems/workflows/add-tests.md +1 -1
  41. package/deliver-great-systems/workflows/approve-spec.md +1 -11
  42. package/deliver-great-systems/workflows/complete-milestone.md +2 -2
  43. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  44. package/deliver-great-systems/workflows/create-milestone-job.md +2 -2
  45. package/deliver-great-systems/workflows/discuss-phase.md +2 -2
  46. package/deliver-great-systems/workflows/execute-phase.md +63 -4
  47. package/deliver-great-systems/workflows/execute-plan.md +0 -51
  48. package/deliver-great-systems/workflows/find-related-ideas.md +1 -1
  49. package/deliver-great-systems/workflows/help.md +25 -58
  50. package/deliver-great-systems/workflows/init-product.md +14 -451
  51. package/deliver-great-systems/workflows/map-codebase.md +109 -0
  52. package/deliver-great-systems/workflows/new-project.md +0 -1
  53. package/deliver-great-systems/workflows/quick.md +2 -2
  54. package/deliver-great-systems/workflows/run-job.md +56 -0
  55. package/package.json +1 -1
@@ -8,13 +8,12 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const os = require('os');
10
10
 
11
- const { createTempDir, cleanupDir } = require('./test-helpers.cjs');
11
+ const { createTempDir, cleanupDir , initGitRepo } = require('./test-helpers.cjs');
12
+ const { resetPaths } = require('./paths.cjs');
12
13
 
13
- // Helper to create .planning/REPOS.md with given content
14
+ // Helper to create REPOS.md with given content at root
14
15
  function writeReposFile(cwd, content) {
15
- const planningDir = path.join(cwd, '.planning');
16
- fs.mkdirSync(planningDir, { recursive: true });
17
- fs.writeFileSync(path.join(planningDir, 'REPOS.md'), content);
16
+ fs.writeFileSync(path.join(cwd, 'REPOS.md'), content);
18
17
  }
19
18
 
20
19
  const VALID_REPOS_MD = `# Repos
@@ -45,8 +44,8 @@ const {
45
44
 
46
45
  describe('parseReposMd', () => {
47
46
  let tmpDir;
48
- beforeEach(() => { tmpDir = createTempDir(); });
49
- afterEach(() => { cleanupDir(tmpDir); });
47
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
48
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
50
49
 
51
50
  it('returns structured array from valid REPOS.md', () => {
52
51
  writeReposFile(tmpDir, VALID_REPOS_MD);
@@ -60,7 +59,7 @@ describe('parseReposMd', () => {
60
59
  });
61
60
 
62
61
  it('returns null when file does not exist', () => {
63
- fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
62
+
64
63
  const result = parseReposMd(tmpDir);
65
64
  assert.strictEqual(result, null);
66
65
  });
@@ -147,18 +146,18 @@ Registered repositories.
147
146
 
148
147
  describe('writeReposMd', () => {
149
148
  let tmpDir;
150
- beforeEach(() => { tmpDir = createTempDir(); fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); });
151
- afterEach(() => { cleanupDir(tmpDir); });
149
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
150
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
152
151
 
153
152
  it('creates file with # Repos header', () => {
154
153
  writeReposMd(tmpDir, [{ name: 'app', path: './app', url: '', description: 'App' }]);
155
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'REPOS.md'), 'utf-8');
154
+ const content = fs.readFileSync(path.join(tmpDir, 'REPOS.md'), 'utf-8');
156
155
  assert.ok(content.startsWith('# Repos'));
157
156
  });
158
157
 
159
158
  it('includes explanatory text', () => {
160
159
  writeReposMd(tmpDir, []);
161
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'REPOS.md'), 'utf-8');
160
+ const content = fs.readFileSync(path.join(tmpDir, 'REPOS.md'), 'utf-8');
162
161
  assert.ok(content.includes('Managed by DGS'));
163
162
  });
164
163
 
@@ -166,7 +165,7 @@ describe('writeReposMd', () => {
166
165
  writeReposMd(tmpDir, [
167
166
  { name: 'web', path: './web', url: 'https://github.com/org/web', description: 'Frontend' },
168
167
  ]);
169
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'REPOS.md'), 'utf-8');
168
+ const content = fs.readFileSync(path.join(tmpDir, 'REPOS.md'), 'utf-8');
170
169
  assert.ok(content.includes('| Name | Path | GitHub URL | Description |'));
171
170
  assert.ok(content.includes('|------|------|------------|-------------|'));
172
171
  assert.ok(content.includes('| web | ./web | https://github.com/org/web | Frontend |'));
@@ -174,16 +173,16 @@ describe('writeReposMd', () => {
174
173
 
175
174
  it('handles empty repos array', () => {
176
175
  writeReposMd(tmpDir, []);
177
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'REPOS.md'), 'utf-8');
176
+ const content = fs.readFileSync(path.join(tmpDir, 'REPOS.md'), 'utf-8');
178
177
  assert.ok(content.includes('| Name | Path | GitHub URL | Description |'));
179
178
  // Should NOT have any data rows
180
179
  const lines = content.split('\n').filter(l => l.startsWith('|'));
181
180
  assert.strictEqual(lines.length, 2); // header + separator only
182
181
  });
183
182
 
184
- it('writes to .planning/REPOS.md', () => {
183
+ it('writes REPOS.md at root', () => {
185
184
  writeReposMd(tmpDir, []);
186
- assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'REPOS.md')));
185
+ assert.ok(fs.existsSync(path.join(tmpDir, 'REPOS.md')));
187
186
  });
188
187
  });
189
188
 
@@ -227,8 +226,8 @@ describe('resolveFileToRepo', () => {
227
226
  assert.strictEqual(result, null);
228
227
  });
229
228
 
230
- it('returns null for .planning/STATE.md', () => {
231
- const result = resolveFileToRepo('.planning/STATE.md', repos);
229
+ it('returns null for STATE.md', () => {
230
+ const result = resolveFileToRepo('STATE.md', repos);
232
231
  assert.strictEqual(result, null);
233
232
  });
234
233
 
@@ -281,8 +280,8 @@ describe('resolveFileToRepo', () => {
281
280
 
282
281
  describe('discoverRepos', () => {
283
282
  let tmpDir;
284
- beforeEach(() => { tmpDir = createTempDir(); });
285
- afterEach(() => { cleanupDir(tmpDir); });
283
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
284
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
286
285
 
287
286
  it('finds directories containing .git/', () => {
288
287
  fs.mkdirSync(path.join(tmpDir, 'my-repo', '.git'), { recursive: true });
@@ -299,8 +298,8 @@ describe('discoverRepos', () => {
299
298
  assert.strictEqual(result[0].name, 'real-repo');
300
299
  });
301
300
 
302
- it('excludes .planning', () => {
303
- fs.mkdirSync(path.join(tmpDir, '.planning', '.git'), { recursive: true });
301
+ it('excludes system directories', () => {
302
+ fs.mkdirSync(path.join(tmpDir, '.git'), { recursive: true });
304
303
  const result = discoverRepos(tmpDir);
305
304
  assert.strictEqual(result.length, 0);
306
305
  });
@@ -361,8 +360,8 @@ describe('discoverRepos', () => {
361
360
 
362
361
  describe('discoverSiblingRepos', () => {
363
362
  let tmpDir;
364
- beforeEach(() => { tmpDir = createTempDir(); });
365
- afterEach(() => { cleanupDir(tmpDir); });
363
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
364
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
366
365
 
367
366
  it('finds sibling directories containing .git/', () => {
368
367
  // tmpDir acts as parent; product-root and sibling-repo are siblings
@@ -427,8 +426,8 @@ describe('discoverSiblingRepos', () => {
427
426
 
428
427
  describe('syncGitignore', () => {
429
428
  let tmpDir;
430
- beforeEach(() => { tmpDir = createTempDir(); });
431
- afterEach(() => { cleanupDir(tmpDir); });
429
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
430
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
432
431
 
433
432
  it('creates .gitignore with DGS section when file does not exist', () => {
434
433
  syncGitignore(tmpDir, ['./web-app', './server']);
@@ -515,8 +514,8 @@ describe('syncGitignore', () => {
515
514
 
516
515
  describe('validateRepoPaths', () => {
517
516
  let tmpDir;
518
- beforeEach(() => { tmpDir = createTempDir(); });
519
- afterEach(() => { cleanupDir(tmpDir); });
517
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
518
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
520
519
 
521
520
  it('returns valid and missing arrays', () => {
522
521
  fs.mkdirSync(path.join(tmpDir, 'existing'));
@@ -589,25 +588,25 @@ describe('hasPathConflict', () => {
589
588
 
590
589
  describe('writeProjectsMd', () => {
591
590
  let tmpDir;
592
- beforeEach(() => { tmpDir = createTempDir(); fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); });
593
- afterEach(() => { cleanupDir(tmpDir); });
591
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
592
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
594
593
 
595
594
  it('creates file with # Projects header', () => {
596
595
  writeProjectsMd(tmpDir, []);
597
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
596
+ const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
598
597
  assert.ok(content.startsWith('# Projects'));
599
598
  });
600
599
 
601
600
  it('includes Active and Completed sections', () => {
602
601
  writeProjectsMd(tmpDir, []);
603
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
602
+ const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
604
603
  assert.ok(content.includes('## Active'));
605
604
  assert.ok(content.includes('## Completed'));
606
605
  });
607
606
 
608
- it('writes to .planning/PROJECTS.md', () => {
607
+ it('writes PROJECTS.md at root', () => {
609
608
  writeProjectsMd(tmpDir, []);
610
- assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'PROJECTS.md')));
609
+ assert.ok(fs.existsSync(path.join(tmpDir, 'PROJECTS.md')));
611
610
  });
612
611
  });
613
612
 
@@ -615,8 +614,8 @@ describe('writeProjectsMd', () => {
615
614
 
616
615
  describe('writeReposMd -> parseReposMd roundtrip', () => {
617
616
  let tmpDir;
618
- beforeEach(() => { tmpDir = createTempDir(); fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); });
619
- afterEach(() => { cleanupDir(tmpDir); });
617
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
618
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
620
619
 
621
620
  it('write then parse produces identical data', () => {
622
621
  const repos = [
@@ -1379,10 +1378,12 @@ describe('cmdReposAdd ../name format enforcement', () => {
1379
1378
  const { execSync } = require('child_process');
1380
1379
 
1381
1380
  beforeEach(() => {
1382
- tmpDir = createTempDir();
1383
- // Create a flat layout: product-root with .planning, and sibling repos
1381
+ resetPaths();
1382
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir);
1383
+ // Create a flat layout: product-root with its own git repo and sibling repos
1384
1384
  const productRoot = path.join(tmpDir, 'product-root');
1385
- fs.mkdirSync(path.join(productRoot, '.planning'), { recursive: true });
1385
+ fs.mkdirSync(productRoot, { recursive: true });
1386
+ initGitRepo(productRoot);
1386
1387
  // Write initial REPOS.md
1387
1388
  const reposMd = `# Repos
1388
1389
 
@@ -1391,7 +1392,8 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1391
1392
  | Name | Path | GitHub URL | Description |
1392
1393
  |------|------|------------|-------------|
1393
1394
  `;
1394
- fs.writeFileSync(path.join(productRoot, '.planning', 'REPOS.md'), reposMd);
1395
+ fs.writeFileSync(path.join(productRoot, 'REPOS.md'), reposMd);
1396
+ execSync('git add REPOS.md && git commit -m "add repos"', { cwd: productRoot, stdio: 'pipe' });
1395
1397
  // Create sibling repo with .git
1396
1398
  const siblingRepo = path.join(tmpDir, 'my-repo');
1397
1399
  fs.mkdirSync(path.join(siblingRepo, '.git'), { recursive: true });
@@ -1399,7 +1401,7 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1399
1401
  const noGitRepo = path.join(tmpDir, 'no-git-repo');
1400
1402
  fs.mkdirSync(noGitRepo, { recursive: true });
1401
1403
  });
1402
- afterEach(() => { cleanupDir(tmpDir); });
1404
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
1403
1405
 
1404
1406
  // Helper to test cmdReposAdd via subprocess (it calls error() which does process.exit)
1405
1407
  function runCmdReposAdd(cwd, repoPath, opts) {
@@ -1491,9 +1493,11 @@ describe('cmdReposAdd trailing slash normalization', () => {
1491
1493
  const { execSync } = require('child_process');
1492
1494
 
1493
1495
  beforeEach(() => {
1494
- tmpDir = createTempDir();
1496
+ resetPaths();
1497
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir);
1495
1498
  const productRoot = path.join(tmpDir, 'product-root');
1496
- fs.mkdirSync(path.join(productRoot, '.planning'), { recursive: true });
1499
+ fs.mkdirSync(productRoot, { recursive: true });
1500
+ initGitRepo(productRoot);
1497
1501
  const reposMd = `# Repos
1498
1502
 
1499
1503
  Registered repositories for this product. Managed by DGS — manual edits may be overwritten.
@@ -1501,12 +1505,13 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1501
1505
  | Name | Path | GitHub URL | Description |
1502
1506
  |------|------|------------|-------------|
1503
1507
  `;
1504
- fs.writeFileSync(path.join(productRoot, '.planning', 'REPOS.md'), reposMd);
1508
+ fs.writeFileSync(path.join(productRoot, 'REPOS.md'), reposMd);
1509
+ execSync('git add REPOS.md && git commit -m "add repos"', { cwd: productRoot, stdio: 'pipe' });
1505
1510
  // Create sibling repo with .git
1506
1511
  const siblingRepo = path.join(tmpDir, 'sibling-repo');
1507
1512
  fs.mkdirSync(path.join(siblingRepo, '.git'), { recursive: true });
1508
1513
  });
1509
- afterEach(() => { cleanupDir(tmpDir); });
1514
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
1510
1515
 
1511
1516
  function runCmdReposAdd(cwd, repoPath, opts) {
1512
1517
  const reposPath = require.resolve('./repos.cjs');
@@ -1541,7 +1546,7 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1541
1546
  }
1542
1547
 
1543
1548
  function getStoredPath(productRoot) {
1544
- const content = fs.readFileSync(path.join(productRoot, '.planning', 'REPOS.md'), 'utf-8');
1549
+ const content = fs.readFileSync(path.join(productRoot, 'REPOS.md'), 'utf-8');
1545
1550
  const lines = content.split('\n').filter(l => l.startsWith('|') && !l.includes('---') && !l.includes('Name'));
1546
1551
  if (lines.length === 0) return null;
1547
1552
  const cols = lines[0].split('|').map(c => c.trim()).filter(Boolean);
@@ -1577,12 +1582,13 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1577
1582
 
1578
1583
  describe('validateReposMdEager', () => {
1579
1584
  let tmpDir;
1580
- beforeEach(() => { tmpDir = createTempDir(); });
1581
- afterEach(() => { cleanupDir(tmpDir); });
1585
+ beforeEach(() => { resetPaths(); tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); });
1586
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
1582
1587
 
1583
1588
  function setupFlatLayout(tmpDir, entries) {
1584
1589
  const productRoot = path.join(tmpDir, 'product-root');
1585
- fs.mkdirSync(path.join(productRoot, '.planning'), { recursive: true });
1590
+ fs.mkdirSync(productRoot, { recursive: true });
1591
+ initGitRepo(productRoot);
1586
1592
 
1587
1593
  let reposMd = `# Repos
1588
1594
 
@@ -1600,7 +1606,7 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1600
1606
  fs.mkdirSync(path.join(tmpDir, entry.name, '.git'), { recursive: true });
1601
1607
  }
1602
1608
  }
1603
- fs.writeFileSync(path.join(productRoot, '.planning', 'REPOS.md'), reposMd);
1609
+ fs.writeFileSync(path.join(productRoot, 'REPOS.md'), reposMd);
1604
1610
  return productRoot;
1605
1611
  }
1606
1612
 
@@ -1627,7 +1633,9 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1627
1633
  const repoDir = path.join(tmpDir, 'shared');
1628
1634
  fs.mkdirSync(path.join(repoDir, '.git'), { recursive: true });
1629
1635
  const productRoot = path.join(tmpDir, 'product-root');
1630
- fs.mkdirSync(path.join(productRoot, '.planning'), { recursive: true });
1636
+ fs.mkdirSync(productRoot, { recursive: true });
1637
+ initGitRepo(productRoot);
1638
+ resetPaths();
1631
1639
 
1632
1640
  const reposMd = `# Repos
1633
1641
 
@@ -1638,7 +1646,7 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1638
1646
  | shared-a | ../shared | | First ref |
1639
1647
  | shared-b | ../shared | | Duplicate ref |
1640
1648
  `;
1641
- fs.writeFileSync(path.join(productRoot, '.planning', 'REPOS.md'), reposMd);
1649
+ fs.writeFileSync(path.join(productRoot, 'REPOS.md'), reposMd);
1642
1650
  const result = validateReposMdEager(productRoot);
1643
1651
  assert.ok(result.errors.length > 0);
1644
1652
  assert.ok(result.errors[0].includes('Duplicate path'));
@@ -1656,7 +1664,9 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1656
1664
 
1657
1665
  it('rejects ./repo path format', () => {
1658
1666
  const productRoot = path.join(tmpDir, 'product-root');
1659
- fs.mkdirSync(path.join(productRoot, '.planning'), { recursive: true });
1667
+ fs.mkdirSync(productRoot, { recursive: true });
1668
+ initGitRepo(productRoot);
1669
+ resetPaths();
1660
1670
  fs.mkdirSync(path.join(productRoot, 'local-repo', '.git'), { recursive: true });
1661
1671
 
1662
1672
  const reposMd = `# Repos
@@ -1665,7 +1675,7 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1665
1675
  |------|------|------------|-------------|
1666
1676
  | local-repo | ./local-repo | | Local repo |
1667
1677
  `;
1668
- fs.writeFileSync(path.join(productRoot, '.planning', 'REPOS.md'), reposMd);
1678
+ fs.writeFileSync(path.join(productRoot, 'REPOS.md'), reposMd);
1669
1679
  const result = validateReposMdEager(productRoot);
1670
1680
  assert.ok(result.errors.length > 0);
1671
1681
  assert.ok(result.errors[0].includes('Repo paths must use ../name format'));
@@ -1676,8 +1686,8 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1676
1686
 
1677
1687
  describe('validateRepoPaths with ../repo paths', () => {
1678
1688
  let tmpDir;
1679
- beforeEach(() => { tmpDir = createTempDir(); });
1680
- afterEach(() => { cleanupDir(tmpDir); });
1689
+ beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
1690
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
1681
1691
 
1682
1692
  it('validates ../repo paths that exist on disk', () => {
1683
1693
  const productRoot = path.join(tmpDir, 'product-root');
@@ -1707,15 +1717,15 @@ describe('syncGitignore skip for flat layout', () => {
1707
1717
  let tmpDir;
1708
1718
  const { execSync } = require('child_process');
1709
1719
 
1710
- beforeEach(() => { tmpDir = createTempDir(); });
1711
- afterEach(() => { cleanupDir(tmpDir); });
1720
+ beforeEach(() => { resetPaths(); tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); });
1721
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
1712
1722
 
1713
- it('cmdReposInitProduct with no local repos does NOT create .gitignore DGS section', () => {
1714
- // Set up a product root with no subdirectory repos (flat layout)
1715
- // discoverRepos will find nothing -> localPaths is empty -> syncGitignore not called
1723
+ it('cmdReposInitProduct creates root layout with .gitignore', () => {
1724
+ // Set up a product root with its own git repo (root layout)
1716
1725
  const productRoot = path.join(tmpDir, 'product-root');
1717
- fs.mkdirSync(path.join(productRoot, '.planning'), { recursive: true });
1718
- // Create sibling repos (outside product root — discoverRepos won't find them)
1726
+ fs.mkdirSync(productRoot, { recursive: true });
1727
+ initGitRepo(productRoot);
1728
+ // Create sibling repos (outside product root -- discoverRepos won't find them)
1719
1729
  fs.mkdirSync(path.join(tmpDir, 'sibling-app', '.git'), { recursive: true });
1720
1730
 
1721
1731
  const reposPath = require.resolve('./repos.cjs');
@@ -1745,20 +1755,18 @@ describe('syncGitignore skip for flat layout', () => {
1745
1755
  }).trim();
1746
1756
  const parsed = JSON.parse(result);
1747
1757
  assert.strictEqual(parsed.initialized, true);
1748
- assert.strictEqual(parsed.gitignore_synced, false);
1758
+ assert.strictEqual(parsed.layout, 'root');
1749
1759
 
1750
- // .gitignore should not exist or should not contain DGS markers
1760
+ // .gitignore should exist with root-layout DGS markers
1751
1761
  const gitignorePath = path.join(productRoot, '.gitignore');
1752
- if (fs.existsSync(gitignorePath)) {
1753
- const content = fs.readFileSync(gitignorePath, 'utf-8');
1754
- assert.ok(!content.includes('# DGS managed repos'), '.gitignore should not contain DGS markers');
1755
- }
1762
+ assert.ok(fs.existsSync(gitignorePath), '.gitignore should exist');
1756
1763
  });
1757
1764
 
1758
1765
  it('cmdReposAdd with ../path does NOT modify .gitignore', () => {
1759
- // Set up product root with REPOS.md and a sibling repo
1766
+ // Set up product root with its own git repo and a sibling repo
1760
1767
  const productRoot = path.join(tmpDir, 'product-root');
1761
- fs.mkdirSync(path.join(productRoot, '.planning'), { recursive: true });
1768
+ fs.mkdirSync(productRoot, { recursive: true });
1769
+ initGitRepo(productRoot);
1762
1770
  const reposMd = `# Repos
1763
1771
 
1764
1772
  Registered repositories for this product. Managed by DGS — manual edits may be overwritten.
@@ -1766,7 +1774,7 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1766
1774
  | Name | Path | GitHub URL | Description |
1767
1775
  |------|------|------------|-------------|
1768
1776
  `;
1769
- fs.writeFileSync(path.join(productRoot, '.planning', 'REPOS.md'), reposMd);
1777
+ fs.writeFileSync(path.join(productRoot, 'REPOS.md'), reposMd);
1770
1778
  // Create sibling repo with .git
1771
1779
  fs.mkdirSync(path.join(tmpDir, 'sibling-repo', '.git'), { recursive: true });
1772
1780
 
@@ -1803,9 +1811,10 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1803
1811
  });
1804
1812
 
1805
1813
  it('cmdReposRemove with ../path does NOT modify .gitignore', () => {
1806
- // Set up product root with REPOS.md containing a ../repo entry
1814
+ // Set up product root with its own git repo and REPOS.md containing a ../repo entry
1807
1815
  const productRoot = path.join(tmpDir, 'product-root');
1808
- fs.mkdirSync(path.join(productRoot, '.planning'), { recursive: true });
1816
+ fs.mkdirSync(productRoot, { recursive: true });
1817
+ initGitRepo(productRoot);
1809
1818
  const reposMd = `# Repos
1810
1819
 
1811
1820
  Registered repositories for this product. Managed by DGS — manual edits may be overwritten.
@@ -1814,7 +1823,7 @@ Registered repositories for this product. Managed by DGS — manual edits may be
1814
1823
  |------|------|------------|-------------|
1815
1824
  | sibling-repo | ../sibling-repo | | Test repo |
1816
1825
  `;
1817
- fs.writeFileSync(path.join(productRoot, '.planning', 'REPOS.md'), reposMd);
1826
+ fs.writeFileSync(path.join(productRoot, 'REPOS.md'), reposMd);
1818
1827
  // Create sibling repo with .git
1819
1828
  fs.mkdirSync(path.join(tmpDir, 'sibling-repo', '.git'), { recursive: true });
1820
1829
 
@@ -161,7 +161,7 @@ function parseFrontmatter(content) {
161
161
 
162
162
  /**
163
163
  * Scan ideas directories for searchable idea files.
164
- * Scans .planning/ideas/{pending,done,rejected}/ for .md files.
164
+ * Scans ideas/{pending,done,rejected}/ for .md files.
165
165
  *
166
166
  * @param {string} cwd - Working directory
167
167
  * @param {object} options - { include_rejected, tags }
@@ -218,7 +218,7 @@ function scanIdeas(cwd, options) {
218
218
 
219
219
  /**
220
220
  * Scan specs directory for searchable spec files.
221
- * Scans .planning/specs/ for .md files.
221
+ * Scans specs/ for .md files.
222
222
  *
223
223
  * @param {string} cwd - Working directory
224
224
  * @returns {Array<{ type: string, id: string, title: string, filePath: string, status: string, author: string, content: string }>}
@@ -258,8 +258,8 @@ function scanSpecs(cwd) {
258
258
 
259
259
  /**
260
260
  * Scan docs directories for searchable documents.
261
- * Scans .planning/docs/ (product), .planning/ideas/star/docs/ (idea-scoped),
262
- * .planning/specs/star/docs/ (spec-scoped).
261
+ * Scans docs/ (product), ideas/{state}/{idea-slug}/docs/ (idea-scoped),
262
+ * specs/{spec-slug}/docs/ (spec-scoped).
263
263
  * Uses .extracted.txt sidecars when available.
264
264
  *
265
265
  * @param {string} cwd - Working directory
@@ -4,7 +4,7 @@
4
4
  * Provides the data model and operations for the spec lifecycle:
5
5
  * create, list, update-status, append-review-history, and finalize.
6
6
  * Specs are stored as markdown files with YAML frontmatter in
7
- * .planning/specs/ (flat directory, no subdirectories).
7
+ * specs/ (flat directory, no subdirectories).
8
8
  */
9
9
 
10
10
  const fs = require('fs');
@@ -32,7 +32,7 @@ function normalizeStatus(status) {
32
32
  }
33
33
 
34
34
  /**
35
- * Ensure .planning/specs/ directory exists.
35
+ * Ensure specs/ directory exists.
36
36
  *
37
37
  * @param {string} cwd - Working directory
38
38
  */
@@ -140,7 +140,7 @@ function classifyError(result, operation, repoName, repoPath) {
140
140
  function collectSyncRepos(cwd) {
141
141
  const repos = [];
142
142
 
143
- // First entry: the planning repo itself (cwd is the product root containing .planning/)
143
+ // First entry: the planning repo itself (cwd is the product root)
144
144
  const resolvedCwd = path.resolve(cwd);
145
145
  const basename = path.basename(resolvedCwd);
146
146
 
@@ -144,9 +144,9 @@ function cmdTemplateFill(cwd, templateType, options, raw) {
144
144
  '- **Output:** [Concrete deliverable]',
145
145
  '',
146
146
  '## Context',
147
- '@.planning/PROJECT.md',
148
- '@.planning/ROADMAP.md',
149
- '@.planning/STATE.md',
147
+ '@PROJECT.md',
148
+ '@ROADMAP.md',
149
+ '@STATE.md',
150
150
  '',
151
151
  '## Tasks',
152
152
  '',