@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
@@ -142,9 +142,9 @@ describe('context load-tier none', () => {
142
142
  beforeEach(() => {
143
143
  resetTierCache();
144
144
  fixture = createFixture({
145
- '.planning/config.json': JSON.stringify({}),
146
- '.planning/STATE.md': '# State',
147
- '.planning/PROJECT.md': '# Project',
145
+ 'config.json': JSON.stringify({}),
146
+ 'STATE.md': '# State',
147
+ 'PROJECT.md': '# Project',
148
148
  });
149
149
  });
150
150
 
@@ -154,18 +154,18 @@ describe('context load-tier none', () => {
154
154
  });
155
155
 
156
156
  it('returns empty files array', () => {
157
- const result = loadTierInternal('none', path.join(fixture.cwd, '.planning'), { cwd: fixture.cwd });
157
+ const result = loadTierInternal('none', fixture.cwd, { cwd: fixture.cwd });
158
158
  assert.ok(Array.isArray(result.files), 'files should be array');
159
159
  assert.equal(result.files.length, 0, 'none tier should have 0 files');
160
160
  });
161
161
 
162
162
  it('returns tier name in output', () => {
163
- const result = loadTierInternal('none', path.join(fixture.cwd, '.planning'), { cwd: fixture.cwd });
163
+ const result = loadTierInternal('none', fixture.cwd, { cwd: fixture.cwd });
164
164
  assert.equal(result.tier, 'none', 'tier should be "none"');
165
165
  });
166
166
 
167
167
  it('returns empty scope', () => {
168
- const result = loadTierInternal('none', path.join(fixture.cwd, '.planning'), { cwd: fixture.cwd });
168
+ const result = loadTierInternal('none', fixture.cwd, { cwd: fixture.cwd });
169
169
  assert.deepEqual(result.scope, {}, 'scope should be empty');
170
170
  });
171
171
  });
@@ -186,12 +186,12 @@ describe('context load-tier lite', () => {
186
186
 
187
187
  it('returns files with correct paths for existing files', () => {
188
188
  fixture = createFixture({
189
- '.planning/config.json': JSON.stringify({}),
190
- '.planning/STATE.md': '# State',
191
- '.planning/PROJECT.md': '# Project',
189
+ 'config.json': JSON.stringify({}),
190
+ 'STATE.md': '# State',
191
+ 'PROJECT.md': '# Project',
192
192
  });
193
193
 
194
- const result = loadTierInternal('lite', path.join(fixture.cwd, '.planning'), { cwd: fixture.cwd });
194
+ const result = loadTierInternal('lite', fixture.cwd, { cwd: fixture.cwd });
195
195
  assert.equal(result.tier, 'lite');
196
196
  assert.ok(result.files.length > 0, 'should have some files');
197
197
 
@@ -204,12 +204,12 @@ describe('context load-tier lite', () => {
204
204
 
205
205
  it('returns category labels for each file', () => {
206
206
  fixture = createFixture({
207
- '.planning/config.json': JSON.stringify({}),
208
- '.planning/STATE.md': '# State',
209
- '.planning/PROJECT.md': '# Project',
207
+ 'config.json': JSON.stringify({}),
208
+ 'STATE.md': '# State',
209
+ 'PROJECT.md': '# Project',
210
210
  });
211
211
 
212
- const result = loadTierInternal('lite', path.join(fixture.cwd, '.planning'), { cwd: fixture.cwd });
212
+ const result = loadTierInternal('lite', fixture.cwd, { cwd: fixture.cwd });
213
213
 
214
214
  for (const file of result.files) {
215
215
  assert.ok(file.category, `file ${file.path} should have category`);
@@ -224,11 +224,11 @@ describe('context load-tier lite', () => {
224
224
  it('omits missing files silently', () => {
225
225
  // No STATE.md in this fixture
226
226
  fixture = createFixture({
227
- '.planning/config.json': JSON.stringify({}),
228
- '.planning/PROJECT.md': '# Project',
227
+ 'config.json': JSON.stringify({}),
228
+ 'PROJECT.md': '# Project',
229
229
  });
230
230
 
231
- const result = loadTierInternal('lite', path.join(fixture.cwd, '.planning'), { cwd: fixture.cwd });
231
+ const result = loadTierInternal('lite', fixture.cwd, { cwd: fixture.cwd });
232
232
  const filePaths = result.files.map(f => f.path);
233
233
  assert.ok(!filePaths.some(p => p.includes('STATE.md')), 'STATE.md should be omitted when missing');
234
234
  assert.ok(filePaths.some(p => p.includes('PROJECT.md')), 'PROJECT.md should still be present');
@@ -237,12 +237,12 @@ describe('context load-tier lite', () => {
237
237
 
238
238
  it('all files have exists: true', () => {
239
239
  fixture = createFixture({
240
- '.planning/config.json': JSON.stringify({}),
241
- '.planning/STATE.md': '# State',
242
- '.planning/PROJECT.md': '# Project',
240
+ 'config.json': JSON.stringify({}),
241
+ 'STATE.md': '# State',
242
+ 'PROJECT.md': '# Project',
243
243
  });
244
244
 
245
- const result = loadTierInternal('lite', path.join(fixture.cwd, '.planning'), { cwd: fixture.cwd });
245
+ const result = loadTierInternal('lite', fixture.cwd, { cwd: fixture.cwd });
246
246
  for (const file of result.files) {
247
247
  assert.equal(file.exists, true, `file ${file.path} should have exists: true`);
248
248
  }
@@ -265,15 +265,15 @@ describe('context load-tier planning', () => {
265
265
 
266
266
  it('includes all lite tier files plus planning files', () => {
267
267
  fixture = createFixture({
268
- '.planning/config.json': JSON.stringify({}),
269
- '.planning/STATE.md': '# State',
270
- '.planning/PROJECT.md': '# Project',
271
- '.planning/ROADMAP.md': '# Roadmap',
272
- '.planning/REQUIREMENTS.md': '# Requirements',
273
- '.planning/REPOS.md': '# Repos',
268
+ 'config.json': JSON.stringify({}),
269
+ 'STATE.md': '# State',
270
+ 'PROJECT.md': '# Project',
271
+ 'ROADMAP.md': '# Roadmap',
272
+ 'REQUIREMENTS.md': '# Requirements',
273
+ 'REPOS.md': '# Repos',
274
274
  });
275
275
 
276
- const result = loadTierInternal('planning', path.join(fixture.cwd, '.planning'), { cwd: fixture.cwd });
276
+ const result = loadTierInternal('planning', fixture.cwd, { cwd: fixture.cwd });
277
277
  const filePaths = result.files.map(f => f.path);
278
278
 
279
279
  // Lite tier files
@@ -289,14 +289,14 @@ describe('context load-tier planning', () => {
289
289
 
290
290
  it('includes codebase docs via dynamic glob', () => {
291
291
  fixture = createFixture({
292
- '.planning/config.json': JSON.stringify({}),
293
- '.planning/PROJECT.md': '# Project',
294
- '.planning/ROADMAP.md': '# Roadmap',
295
- '.planning/codebase/CONVENTIONS.md': '# Conventions',
296
- '.planning/codebase/TESTING.md': '# Testing',
292
+ 'config.json': JSON.stringify({}),
293
+ 'PROJECT.md': '# Project',
294
+ 'ROADMAP.md': '# Roadmap',
295
+ 'codebase/CONVENTIONS.md': '# Conventions',
296
+ 'codebase/TESTING.md': '# Testing',
297
297
  });
298
298
 
299
- const result = loadTierInternal('planning', path.join(fixture.cwd, '.planning'), { cwd: fixture.cwd });
299
+ const result = loadTierInternal('planning', fixture.cwd, { cwd: fixture.cwd });
300
300
  const filePaths = result.files.map(f => f.path);
301
301
 
302
302
  assert.ok(filePaths.some(p => p.includes('CONVENTIONS.md')), 'should include CONVENTIONS.md');
@@ -309,14 +309,14 @@ describe('context load-tier planning', () => {
309
309
 
310
310
  it('missing REPOS.md is silently omitted', () => {
311
311
  fixture = createFixture({
312
- '.planning/config.json': JSON.stringify({}),
313
- '.planning/PROJECT.md': '# Project',
314
- '.planning/ROADMAP.md': '# Roadmap',
315
- '.planning/REQUIREMENTS.md': '# Requirements',
312
+ 'config.json': JSON.stringify({}),
313
+ 'PROJECT.md': '# Project',
314
+ 'ROADMAP.md': '# Roadmap',
315
+ 'REQUIREMENTS.md': '# Requirements',
316
316
  // No REPOS.md
317
317
  });
318
318
 
319
- const result = loadTierInternal('planning', path.join(fixture.cwd, '.planning'), { cwd: fixture.cwd });
319
+ const result = loadTierInternal('planning', fixture.cwd, { cwd: fixture.cwd });
320
320
  const filePaths = result.files.map(f => f.path);
321
321
  assert.ok(!filePaths.some(p => p.includes('REPOS.md')), 'REPOS.md should be omitted when missing');
322
322
  assert.ok(filePaths.some(p => p.includes('ROADMAP.md')), 'ROADMAP.md should be present');
@@ -339,17 +339,17 @@ describe('context load-tier with scope flags', () => {
339
339
 
340
340
  it('--phase adds phase-specific files to output', () => {
341
341
  fixture = createFixture({
342
- '.planning/config.json': JSON.stringify({}),
343
- '.planning/PROJECT.md': '# Project',
344
- '.planning/STATE.md': '# State',
345
- '.planning/ROADMAP.md': '# Roadmap\n\n## Phases\n\n### Phase 1: Test Phase\n',
346
- '.planning/phases/01-test-phase/01-CONTEXT.md': '# Context',
347
- '.planning/phases/01-test-phase/01-RESEARCH.md': '# Research',
348
- '.planning/phases/01-test-phase/01-01-PLAN.md': '---\nphase: 01\nplan: 01\n---\n# Plan',
349
- '.planning/phases/01-test-phase/01-01-SUMMARY.md': '---\nphase: 01\nplan: 01\n---\n# Summary',
342
+ 'config.json': JSON.stringify({}),
343
+ 'PROJECT.md': '# Project',
344
+ 'STATE.md': '# State',
345
+ 'ROADMAP.md': '# Roadmap\n\n## Phases\n\n### Phase 1: Test Phase\n',
346
+ 'phases/01-test-phase/01-CONTEXT.md': '# Context',
347
+ 'phases/01-test-phase/01-RESEARCH.md': '# Research',
348
+ 'phases/01-test-phase/01-01-PLAN.md': '---\nphase: 01\nplan: 01\n---\n# Plan',
349
+ 'phases/01-test-phase/01-01-SUMMARY.md': '---\nphase: 01\nplan: 01\n---\n# Summary',
350
350
  });
351
351
 
352
- const result = loadTierInternal('execution', path.join(fixture.cwd, '.planning'), {
352
+ const result = loadTierInternal('execution', fixture.cwd, {
353
353
  cwd: fixture.cwd,
354
354
  phase: '1',
355
355
  });
@@ -363,14 +363,14 @@ describe('context load-tier with scope flags', () => {
363
363
 
364
364
  it('--idea adds idea file and docs to output', () => {
365
365
  fixture = createFixture({
366
- '.planning/config.json': JSON.stringify({}),
367
- '.planning/PROJECT.md': '# Project',
368
- '.planning/ROADMAP.md': '# Roadmap',
369
- '.planning/ideas/pending/005-better-caching.md': '# Better Caching',
370
- '.planning/ideas/pending/005-better-caching/docs/research.md': '# Research on caching',
366
+ 'config.json': JSON.stringify({}),
367
+ 'PROJECT.md': '# Project',
368
+ 'ROADMAP.md': '# Roadmap',
369
+ 'ideas/pending/005-better-caching.md': '# Better Caching',
370
+ 'ideas/pending/005-better-caching/docs/research.md': '# Research on caching',
371
371
  });
372
372
 
373
- const result = loadTierInternal('planning', path.join(fixture.cwd, '.planning'), {
373
+ const result = loadTierInternal('planning', fixture.cwd, {
374
374
  cwd: fixture.cwd,
375
375
  idea: '5',
376
376
  });
@@ -383,14 +383,14 @@ describe('context load-tier with scope flags', () => {
383
383
 
384
384
  it('--spec adds spec file and docs to output', () => {
385
385
  fixture = createFixture({
386
- '.planning/config.json': JSON.stringify({}),
387
- '.planning/PROJECT.md': '# Project',
388
- '.planning/ROADMAP.md': '# Roadmap',
389
- '.planning/specs/dgs-improved-context-spec.md': '---\nid: dgs-improved-context\nstatus: draft\n---\n# Improved Context',
390
- '.planning/specs/dgs-improved-context-spec/docs/analysis.md': '# Analysis',
386
+ 'config.json': JSON.stringify({}),
387
+ 'PROJECT.md': '# Project',
388
+ 'ROADMAP.md': '# Roadmap',
389
+ 'specs/dgs-improved-context-spec.md': '---\nid: dgs-improved-context\nstatus: draft\n---\n# Improved Context',
390
+ 'specs/dgs-improved-context-spec/docs/analysis.md': '# Analysis',
391
391
  });
392
392
 
393
- const result = loadTierInternal('planning', path.join(fixture.cwd, '.planning'), {
393
+ const result = loadTierInternal('planning', fixture.cwd, {
394
394
  cwd: fixture.cwd,
395
395
  spec: 'dgs-improved-context',
396
396
  });
@@ -403,15 +403,15 @@ describe('context load-tier with scope flags', () => {
403
403
 
404
404
  it('missing scope targets silently return no extra files', () => {
405
405
  fixture = createFixture({
406
- '.planning/config.json': JSON.stringify({}),
407
- '.planning/PROJECT.md': '# Project',
408
- '.planning/STATE.md': '# State',
409
- '.planning/ROADMAP.md': '# Roadmap\n\n## Phases\n\n### Phase 1: Test Phase\n',
410
- '.planning/phases/01-test-phase/01-01-PLAN.md': '# Plan',
406
+ 'config.json': JSON.stringify({}),
407
+ 'PROJECT.md': '# Project',
408
+ 'STATE.md': '# State',
409
+ 'ROADMAP.md': '# Roadmap\n\n## Phases\n\n### Phase 1: Test Phase\n',
410
+ 'phases/01-test-phase/01-01-PLAN.md': '# Plan',
411
411
  });
412
412
 
413
413
  // Phase 999 does not exist
414
- const result = loadTierInternal('execution', path.join(fixture.cwd, '.planning'), {
414
+ const result = loadTierInternal('execution', fixture.cwd, {
415
415
  cwd: fixture.cwd,
416
416
  phase: '999',
417
417
  });
@@ -426,13 +426,13 @@ describe('context load-tier with scope flags', () => {
426
426
 
427
427
  it('approved spec is marked as truncated', () => {
428
428
  fixture = createFixture({
429
- '.planning/config.json': JSON.stringify({}),
430
- '.planning/PROJECT.md': '# Project',
431
- '.planning/ROADMAP.md': '# Roadmap',
432
- '.planning/specs/my-spec.md': '---\nid: my-spec\nstatus: approved\n---\n# My Spec\n\n## Section 1\nContent\n\n## Section 2\nContent\n\n## Section 3\nContent\n\n## Section 4\nShould be truncated',
429
+ 'config.json': JSON.stringify({}),
430
+ 'PROJECT.md': '# Project',
431
+ 'ROADMAP.md': '# Roadmap',
432
+ 'specs/my-spec.md': '---\nid: my-spec\nstatus: approved\n---\n# My Spec\n\n## Section 1\nContent\n\n## Section 2\nContent\n\n## Section 3\nContent\n\n## Section 4\nShould be truncated',
433
433
  });
434
434
 
435
- const result = loadTierInternal('planning', path.join(fixture.cwd, '.planning'), {
435
+ const result = loadTierInternal('planning', fixture.cwd, {
436
436
  cwd: fixture.cwd,
437
437
  spec: 'my-spec',
438
438
  });
@@ -458,13 +458,13 @@ describe('graceful degradation', () => {
458
458
  resetTierCache();
459
459
  });
460
460
 
461
- it('empty .planning directory returns empty files for lite tier', () => {
461
+ it('empty directory returns empty files for lite tier', () => {
462
462
  fixture = createFixture({
463
- '.planning/config.json': JSON.stringify({}),
463
+ 'config.json': JSON.stringify({}),
464
464
  // No STATE.md, PROJECT.md etc.
465
465
  });
466
466
 
467
- const result = loadTierInternal('lite', path.join(fixture.cwd, '.planning'), { cwd: fixture.cwd });
467
+ const result = loadTierInternal('lite', fixture.cwd, { cwd: fixture.cwd });
468
468
  assert.equal(result.tier, 'lite');
469
469
  // config.json exists but PROJECT.md and STATE.md don't
470
470
  assert.ok(result.files.length <= 1, 'should have at most config.json');
@@ -472,10 +472,10 @@ describe('graceful degradation', () => {
472
472
 
473
473
  it('non-existent tier name returns error in result', () => {
474
474
  fixture = createFixture({
475
- '.planning/config.json': JSON.stringify({}),
475
+ 'config.json': JSON.stringify({}),
476
476
  });
477
477
 
478
- const result = loadTierInternal('invalid-name', path.join(fixture.cwd, '.planning'), { cwd: fixture.cwd });
478
+ const result = loadTierInternal('invalid-name', fixture.cwd, { cwd: fixture.cwd });
479
479
  assert.ok(result.error, 'should have error field');
480
480
  assert.ok(result.error.includes('Unknown tier'), 'error should mention unknown tier');
481
481
  assert.equal(result.files.length, 0, 'should have empty files');
@@ -483,7 +483,7 @@ describe('graceful degradation', () => {
483
483
 
484
484
  it('invalid tier via CLI produces error on stderr', () => {
485
485
  fixture = createFixture({
486
- '.planning/config.json': JSON.stringify({}),
486
+ 'config.json': JSON.stringify({}),
487
487
  });
488
488
 
489
489
  const stderr = runContextError(fixture.cwd, 'load-tier invalid-name');
@@ -491,14 +491,14 @@ describe('graceful degradation', () => {
491
491
  assert.ok(stderr.includes('invalid tier'), 'error should mention invalid tier');
492
492
  });
493
493
 
494
- it('missing .planning/config.json falls back gracefully', () => {
494
+ it('missing config.json falls back gracefully', () => {
495
495
  fixture = createFixture({
496
- '.planning/PROJECT.md': '# Project',
497
- '.planning/STATE.md': '# State',
496
+ 'PROJECT.md': '# Project',
497
+ 'STATE.md': '# State',
498
498
  });
499
499
 
500
500
  // The config.json from tier definitions won't be found but that's fine
501
- const result = loadTierInternal('lite', path.join(fixture.cwd, '.planning'), { cwd: fixture.cwd });
501
+ const result = loadTierInternal('lite', fixture.cwd, { cwd: fixture.cwd });
502
502
  assert.equal(result.tier, 'lite');
503
503
  // PROJECT.md and STATE.md should be found
504
504
  const filePaths = result.files.map(f => f.path);
@@ -523,9 +523,9 @@ describe('CLI integration', () => {
523
523
 
524
524
  it('context load-tier lite returns JSON with correct structure', () => {
525
525
  fixture = createFixture({
526
- '.planning/config.json': JSON.stringify({}),
527
- '.planning/STATE.md': '# State',
528
- '.planning/PROJECT.md': '# Project',
526
+ 'config.json': JSON.stringify({}),
527
+ 'STATE.md': '# State',
528
+ 'PROJECT.md': '# Project',
529
529
  });
530
530
 
531
531
  const result = runContext(fixture.cwd, 'load-tier lite');
@@ -536,7 +536,7 @@ describe('CLI integration', () => {
536
536
 
537
537
  it('context load-tier none returns empty files via CLI', () => {
538
538
  fixture = createFixture({
539
- '.planning/config.json': JSON.stringify({}),
539
+ 'config.json': JSON.stringify({}),
540
540
  });
541
541
 
542
542
  const result = runContext(fixture.cwd, 'load-tier none');
@@ -546,11 +546,11 @@ describe('CLI integration', () => {
546
546
 
547
547
  it('context load-tier planning includes inherited lite files via CLI', () => {
548
548
  fixture = createFixture({
549
- '.planning/config.json': JSON.stringify({}),
550
- '.planning/STATE.md': '# State',
551
- '.planning/PROJECT.md': '# Project',
552
- '.planning/ROADMAP.md': '# Roadmap',
553
- '.planning/REQUIREMENTS.md': '# Requirements',
549
+ 'config.json': JSON.stringify({}),
550
+ 'STATE.md': '# State',
551
+ 'PROJECT.md': '# Project',
552
+ 'ROADMAP.md': '# Roadmap',
553
+ 'REQUIREMENTS.md': '# Requirements',
554
554
  });
555
555
 
556
556
  const result = runContext(fixture.cwd, 'load-tier planning');
@@ -561,13 +561,13 @@ describe('CLI integration', () => {
561
561
 
562
562
  it('context load-tier with --phase flag works via CLI', () => {
563
563
  fixture = createFixture({
564
- '.planning/config.json': JSON.stringify({}),
565
- '.planning/PROJECT.md': '# Project',
566
- '.planning/STATE.md': '# State',
567
- '.planning/ROADMAP.md': '# Roadmap\n\n## Phases\n\n### Phase 1: Test Phase\n',
568
- '.planning/REQUIREMENTS.md': '# Requirements',
569
- '.planning/phases/01-test-phase/01-CONTEXT.md': '# Context',
570
- '.planning/phases/01-test-phase/01-01-PLAN.md': '---\nphase: 01\nplan: 01\n---\n# Plan',
564
+ 'config.json': JSON.stringify({}),
565
+ 'PROJECT.md': '# Project',
566
+ 'STATE.md': '# State',
567
+ 'ROADMAP.md': '# Roadmap\n\n## Phases\n\n### Phase 1: Test Phase\n',
568
+ 'REQUIREMENTS.md': '# Requirements',
569
+ 'phases/01-test-phase/01-CONTEXT.md': '# Context',
570
+ 'phases/01-test-phase/01-01-PLAN.md': '---\nphase: 01\nplan: 01\n---\n# Plan',
571
571
  });
572
572
 
573
573
  const result = runContext(fixture.cwd, 'load-tier execution --phase 1');
@@ -578,7 +578,7 @@ describe('CLI integration', () => {
578
578
 
579
579
  it('context without subcommand produces error', () => {
580
580
  fixture = createFixture({
581
- '.planning/config.json': JSON.stringify({}),
581
+ 'config.json': JSON.stringify({}),
582
582
  });
583
583
 
584
584
  const stderr = runContextError(fixture.cwd, '');
@@ -589,7 +589,7 @@ describe('CLI integration', () => {
589
589
 
590
590
  it('context load-tier without tier name produces usage error', () => {
591
591
  fixture = createFixture({
592
- '.planning/config.json': JSON.stringify({}),
592
+ 'config.json': JSON.stringify({}),
593
593
  });
594
594
 
595
595
  const stderr = runContextError(fixture.cwd, 'load-tier');
@@ -600,7 +600,7 @@ describe('CLI integration', () => {
600
600
 
601
601
  it('context unknown-subcommand produces error', () => {
602
602
  fixture = createFixture({
603
- '.planning/config.json': JSON.stringify({}),
603
+ 'config.json': JSON.stringify({}),
604
604
  });
605
605
 
606
606
  const stderr = runContextError(fixture.cwd, 'bogus');
@@ -5,7 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { execSync } = require('child_process');
8
- const { getPlanningRoot, PROJECTS_DIR } = require('./paths.cjs');
8
+ const { getPlanningRoot, isV2Install, PROJECTS_DIR } = require('./paths.cjs');
9
9
 
10
10
  // ─── Path helpers ────────────────────────────────────────────────────────────
11
11
 
@@ -102,17 +102,7 @@ function loadConfig(cwd) {
102
102
  }
103
103
  } catch { /* ignore */ }
104
104
 
105
- // Fall back to legacy dgs.config.json if neither new file exists
106
- let hasNewConfig = Object.keys(shared).length > 0 || Object.keys(local).length > 0;
107
- if (!hasNewConfig) {
108
- const legacyPath = path.join(root, 'dgs.config.json');
109
- try {
110
- if (fs.existsSync(legacyPath)) {
111
- shared = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
112
- hasNewConfig = true;
113
- }
114
- } catch { /* ignore */ }
115
- }
105
+ const hasNewConfig = Object.keys(shared).length > 0 || Object.keys(local).length > 0;
116
106
 
117
107
  if (!hasNewConfig) {
118
108
  return { ...defaults, current_project: null, v2_hint_shown: false, sync_hint_shown: false };
@@ -182,8 +172,7 @@ function isGitIgnored(cwd, targetPath) {
182
172
  try {
183
173
  // --no-index checks .gitignore rules regardless of whether the file is tracked.
184
174
  // Without it, git check-ignore returns "not ignored" for tracked files even when
185
- // .gitignore explicitly lists them — a common source of confusion when .planning/
186
- // was committed before being added to .gitignore.
175
+ // .gitignore explicitly lists them — ensure planning files are not accidentally ignored.
187
176
  execSync('git check-ignore -q --no-index -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), {
188
177
  cwd,
189
178
  stdio: 'pipe',
@@ -308,12 +297,12 @@ function findPhaseInternal(cwd, phase) {
308
297
 
309
298
  const normalized = normalizePhaseName(phase);
310
299
 
311
- // Resolve phases directory: v2 uses .planning/<project>/phases/, v1 uses .planning/phases/
300
+ // Resolve phases directory: project root or '.' fallback
312
301
  let projectRoot;
313
302
  try {
314
303
  projectRoot = getProjectRoot(cwd);
315
304
  } catch {
316
- projectRoot = '.planning';
305
+ projectRoot = '.';
317
306
  }
318
307
  const phasesRel = path.join(projectRoot, 'phases');
319
308
  const phasesDir = path.join(cwd, phasesRel);
@@ -393,12 +382,12 @@ function getArchivedPhaseDirs(cwd) {
393
382
 
394
383
  function getRoadmapPhaseInternal(cwd, phaseNum) {
395
384
  if (!phaseNum) return null;
396
- // v2: look in project root (.planning/<project>/), v1: .planning/
385
+ // Look in project root (projects/<slug>/)
397
386
  let projectRoot;
398
387
  try {
399
388
  projectRoot = getProjectRoot(cwd);
400
389
  } catch {
401
- projectRoot = path.relative(cwd, getPlanningRoot(cwd)) || '.planning';
390
+ projectRoot = path.relative(cwd, getPlanningRoot(cwd)) || '.';
402
391
  }
403
392
  const roadmapPath = path.join(cwd, projectRoot, 'ROADMAP.md');
404
393
  if (!fs.existsSync(roadmapPath)) return null;
@@ -554,8 +543,8 @@ function getMilestonePhaseFilter(cwd) {
554
543
  // ─── v2 Path Resolution ──────────────────────────────────────────────────────
555
544
 
556
545
  /**
557
- * System directories that live under .planning/ but are NOT project folders.
558
- * Kept as a safety net — getProjectFolders() now scans .planning/projects/ directly
546
+ * System directories that are NOT project folders.
547
+ * Kept as a safety net — getProjectFolders() scans projects/ directly
559
548
  * and no longer relies on this set for filtering. The 'projects' entry prevents
560
549
  * the projects container directory itself from being mistaken for a project.
561
550
  */
@@ -564,32 +553,6 @@ const SYSTEM_DIRS = new Set([
564
553
  'debug', 'milestones', 'research', PROJECTS_DIR,
565
554
  ]);
566
555
 
567
- /**
568
- * Detect if this is a v2 (multi-project) installation.
569
- * Requires both file existence AND valid DGS-generated content.
570
- * PROJECTS.md must start with "# Projects", REPOS.md with "# Repos".
571
- *
572
- * @param {string} cwd - Working directory
573
- * @returns {boolean}
574
- */
575
- function isV2Install(cwd) {
576
- const checks = [
577
- { file: 'PROJECTS.md', header: '# Projects' },
578
- { file: 'REPOS.md', header: '# Repos' },
579
- ];
580
- const planRoot = getPlanningRoot(cwd);
581
- for (const { file, header } of checks) {
582
- const filePath = path.join(planRoot, file);
583
- try {
584
- const content = fs.readFileSync(filePath, 'utf-8');
585
- if (content.startsWith(header)) return true;
586
- } catch {
587
- // File doesn't exist or unreadable — not a marker
588
- }
589
- }
590
- return false;
591
- }
592
-
593
556
  /**
594
557
  * Construct the absolute path to a project directory.
595
558
  *
@@ -606,9 +569,9 @@ function getProjectDir(cwd, slug) {
606
569
 
607
570
  /**
608
571
  * Get the project root directory (relative path).
609
- * - v2 with current_project set: returns .planning/projects/<project>
572
+ * - v2 with current_project set: returns projects/<project>
610
573
  * - v2 without current_project: throws Error('NO_CURRENT_PROJECT_V2')
611
- * - v1 (no v2 markers): returns '.planning'
574
+ * - No v2 markers: returns '.'
612
575
  *
613
576
  * @param {string} cwd - Working directory
614
577
  * @returns {string} Relative path to project root
@@ -618,10 +581,8 @@ function getProjectRoot(cwd) {
618
581
  const config = loadConfig(cwd);
619
582
  const currentProject = (config.current_project || '').trim();
620
583
 
621
- // Determine layout: root layout when planning root equals cwd
622
- const planRoot = getPlanningRoot(cwd);
623
- const isRootLayout = (planRoot === path.resolve(cwd));
624
- const planRootRel = isRootLayout ? '.' : '.planning';
584
+ // Root layout: planning root IS the repo root
585
+ const planRootRel = '.';
625
586
 
626
587
  if (currentProject) {
627
588
  // Validate project name — must be a simple slug, no path traversal
@@ -649,7 +610,7 @@ function getProjectRoot(cwd) {
649
610
  throw new Error('NO_CURRENT_PROJECT_V2');
650
611
  }
651
612
 
652
- // v1 fallback — no v2 markers, return planning root relative path
613
+ // No v2 markers return root
653
614
  return planRootRel;
654
615
  }
655
616
 
@@ -674,8 +635,7 @@ function requireProjectRoot(cwd) {
674
635
 
675
636
  /**
676
637
  * Resolve a project-scoped relative path.
677
- * e.g., resolveProjectPath(cwd, 'STATE.md') → '.planning/projects/auth-overhaul/STATE.md' (v2)
678
- * resolveProjectPath(cwd, 'STATE.md') → '.planning/STATE.md' (v1)
638
+ * e.g., resolveProjectPath(cwd, 'STATE.md') → 'projects/auth-overhaul/STATE.md'
679
639
  * resolveProjectPath(cwd) → project root
680
640
  *
681
641
  * @param {string} cwd - Working directory
@@ -692,7 +652,7 @@ function resolveProjectPath(cwd, relativePath) {
692
652
  *
693
653
  * This is intentionally lightweight — reads STATUS line from STATE.md using
694
654
  * safeReadFile (already in core.cjs) to avoid circular dependency on projects.cjs.
695
- * Reads from .planning/projects/<slug>/STATE.md.
655
+ * Reads from projects/<slug>/STATE.md.
696
656
  *
697
657
  * @param {string} cwd - Working directory
698
658
  * @param {string} slug - Project slug
@@ -711,7 +671,7 @@ function isProjectCompleted(cwd, slug) {
711
671
 
712
672
  /**
713
673
  * List project folders in a v2 install.
714
- * Scans .planning/projects/ directly and returns directories that:
674
+ * Scans projects/ directly and returns directories that:
715
675
  * - Don't start with '.'
716
676
  * - Contain a STATE.md file (qualifying marker for project folders)
717
677
  *