@ktpartners/dgs-platform 2.6.3 → 2.7.1

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 (30) hide show
  1. package/agents/dgs-executor.md +51 -0
  2. package/commands/dgs/sync.md +70 -0
  3. package/deliver-great-systems/bin/dgs-tools.cjs +290 -4
  4. package/deliver-great-systems/bin/lib/config.cjs +259 -67
  5. package/deliver-great-systems/bin/lib/core.cjs +49 -8
  6. package/deliver-great-systems/bin/lib/core.test.cjs +35 -14
  7. package/deliver-great-systems/bin/lib/init.cjs +61 -6
  8. package/deliver-great-systems/bin/lib/init.test.cjs +9 -9
  9. package/deliver-great-systems/bin/lib/migration.cjs +1 -1
  10. package/deliver-great-systems/bin/lib/migration.test.cjs +7 -9
  11. package/deliver-great-systems/bin/lib/path-audit.test.cjs +1 -1
  12. package/deliver-great-systems/bin/lib/paths.cjs +32 -22
  13. package/deliver-great-systems/bin/lib/paths.test.cjs +16 -6
  14. package/deliver-great-systems/bin/lib/projects.cjs +1 -1
  15. package/deliver-great-systems/bin/lib/projects.test.cjs +1 -1
  16. package/deliver-great-systems/bin/lib/repos.cjs +29 -10
  17. package/deliver-great-systems/bin/lib/state.cjs +2 -2
  18. package/deliver-great-systems/bin/lib/sync.cjs +878 -0
  19. package/deliver-great-systems/bin/lib/test-helpers.cjs +44 -12
  20. package/deliver-great-systems/references/git-integration.md +81 -0
  21. package/deliver-great-systems/references/planning-config.md +154 -31
  22. package/deliver-great-systems/references/sync-cadence.md +191 -0
  23. package/deliver-great-systems/references/sync-hooks.md +96 -0
  24. package/deliver-great-systems/test/cadence.test.cjs +160 -0
  25. package/deliver-great-systems/test/sync-workflow.test.cjs +562 -0
  26. package/deliver-great-systems/workflows/execute-phase.md +111 -4
  27. package/deliver-great-systems/workflows/init-product.md +6 -2
  28. package/deliver-great-systems/workflows/run-job.md +77 -2
  29. package/deliver-great-systems/workflows/settings.md +82 -1
  30. package/package.json +1 -1
@@ -626,9 +626,9 @@ describe('requireProjectRoot', () => {
626
626
  it('throws PROJECT_NOT_FOUND when v2 install has current_project pointing to nonexistent directory', () => {
627
627
  const fixture = createTempProject({ version: 2, project: 'real-project' });
628
628
 
629
- // Manually update config to point at a nonexistent project
629
+ // Manually update local config to point at a nonexistent project
630
630
  fs.writeFileSync(
631
- path.join(fixture.cwd, '.planning', 'config.json'),
631
+ path.join(fixture.cwd, '.planning', 'config.local.json'),
632
632
  JSON.stringify({ current_project: 'ghost-project' })
633
633
  );
634
634
 
@@ -646,7 +646,7 @@ describe('requireProjectRoot', () => {
646
646
  const fixture = createTempProject({ version: 2, project: 'real-project' });
647
647
 
648
648
  fs.writeFileSync(
649
- path.join(fixture.cwd, '.planning', 'config.json'),
649
+ path.join(fixture.cwd, '.planning', 'config.local.json'),
650
650
  JSON.stringify({ current_project: '../etc' })
651
651
  );
652
652
 
@@ -698,13 +698,13 @@ describe('root layout', () => {
698
698
  fixture = null;
699
699
  });
700
700
 
701
- it('loadConfig reads from root-layout dgs.config.json', () => {
701
+ it('loadConfig reads from root-layout config.json', () => {
702
702
  fixture = createTempProject({ layout: 'root', withConfig: { model_profile: 'quality' } });
703
703
  const config = loadConfig(fixture.cwd);
704
704
  assert.equal(config.model_profile, 'quality');
705
705
  });
706
706
 
707
- it('loadConfig returns defaults when root-layout dgs.config.json has no model_profile', () => {
707
+ it('loadConfig returns defaults when root-layout config.json has no model_profile', () => {
708
708
  fixture = createTempProject({ layout: 'root' });
709
709
  const config = loadConfig(fixture.cwd);
710
710
  assert.equal(config.model_profile, 'balanced');
@@ -713,7 +713,8 @@ describe('root layout', () => {
713
713
  it('getProjectRoot returns . for v1 root-layout', () => {
714
714
  // Create a root-layout v1 fixture (no v2 markers with valid headers)
715
715
  fixture = createFixture({
716
- 'dgs.config.json': JSON.stringify({ planningRoot: '.' }),
716
+ 'config.local.json': JSON.stringify({ planningRoot: '.' }),
717
+ 'config.json': JSON.stringify({}),
717
718
  'PROJECT.md': '# Project\n',
718
719
  'STATE.md': '# State\n',
719
720
  'ROADMAP.md': '# Roadmap\n',
@@ -731,7 +732,8 @@ describe('root layout', () => {
731
732
 
732
733
  it('isV2Install returns false for root-layout v1', () => {
733
734
  fixture = createFixture({
734
- 'dgs.config.json': JSON.stringify({ planningRoot: '.' }),
735
+ 'config.local.json': JSON.stringify({ planningRoot: '.' }),
736
+ 'config.json': JSON.stringify({}),
735
737
  'PROJECT.md': '# Project\n',
736
738
  'STATE.md': '# State\n',
737
739
  });
@@ -741,7 +743,8 @@ describe('root layout', () => {
741
743
 
742
744
  it('getProjectFolders works in root-layout v2', () => {
743
745
  fixture = createFixture({
744
- 'dgs.config.json': JSON.stringify({ planningRoot: '.', current_project: 'proj-a' }),
746
+ 'config.local.json': JSON.stringify({ planningRoot: '.', current_project: 'proj-a' }),
747
+ 'config.json': JSON.stringify({}),
745
748
  'PROJECTS.md': '# Projects\n',
746
749
  'REPOS.md': '# Repos\n',
747
750
  'projects/proj-a/STATE.md': '# State\n',
@@ -751,8 +754,8 @@ describe('root layout', () => {
751
754
  assert.ok(result.includes('proj-a'));
752
755
  });
753
756
 
754
- it('loadConfig reads legacy config.json fallback in root-layout', () => {
755
- // Root layout with only config.json (no dgs.config.json) + PROJECT.md auto-detect
757
+ it('loadConfig reads config.json in root-layout auto-detect', () => {
758
+ // Root layout with config.json + PROJECT.md auto-detect (no config.local.json needed)
756
759
  fixture = createFixture({
757
760
  'PROJECT.md': '# Project\n',
758
761
  'config.json': JSON.stringify({ model_profile: 'budget' }),
@@ -795,7 +798,7 @@ describe('getProjectDir', () => {
795
798
 
796
799
  // ─── Config Dual-Read Tests ─────────────────────────────────────────────────
797
800
 
798
- describe('config dual-read', () => {
801
+ describe('config two-file merge', () => {
799
802
  let fixture;
800
803
 
801
804
  afterEach(() => {
@@ -803,20 +806,38 @@ describe('config dual-read', () => {
803
806
  fixture = null;
804
807
  });
805
808
 
806
- it('loadConfig reads dgs.config.json when both exist in .planning/', () => {
809
+ it('loadConfig merges config.json and config.local.json', () => {
810
+ fixture = createFixture({
811
+ '.planning/config.json': JSON.stringify({ model_profile: 'quality' }),
812
+ '.planning/config.local.json': JSON.stringify({ current_project: 'my-app' }),
813
+ });
814
+ const config = loadConfig(fixture.cwd);
815
+ assert.equal(config.model_profile, 'quality');
816
+ assert.equal(config.current_project, 'my-app');
817
+ });
818
+
819
+ it('loadConfig local overrides shared for overlapping keys', () => {
807
820
  fixture = createFixture({
808
- '.planning/dgs.config.json': JSON.stringify({ model_profile: 'quality' }),
809
821
  '.planning/config.json': JSON.stringify({ model_profile: 'speed' }),
822
+ '.planning/config.local.json': JSON.stringify({ model_profile: 'quality' }),
810
823
  });
811
824
  const config = loadConfig(fixture.cwd);
812
825
  assert.equal(config.model_profile, 'quality');
813
826
  });
814
827
 
815
- it('loadConfig falls back to config.json when no dgs.config.json in .planning/', () => {
828
+ it('loadConfig reads config.json when no config.local.json exists', () => {
816
829
  fixture = createFixture({
817
830
  '.planning/config.json': JSON.stringify({ model_profile: 'speed' }),
818
831
  });
819
832
  const config = loadConfig(fixture.cwd);
820
833
  assert.equal(config.model_profile, 'speed');
821
834
  });
835
+
836
+ it('loadConfig falls back to legacy dgs.config.json', () => {
837
+ fixture = createFixture({
838
+ '.planning/dgs.config.json': JSON.stringify({ model_profile: 'quality' }),
839
+ });
840
+ const config = loadConfig(fixture.cwd);
841
+ assert.equal(config.model_profile, 'quality');
842
+ });
822
843
  });
@@ -9,6 +9,7 @@ const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInte
9
9
  const { requireGitIdentity, formatAuthorString } = require('./identity.cjs');
10
10
  const { getPlanningRoot, PROJECTS_DIR } = require('./paths.cjs');
11
11
  const { parseReposMd, validateReposMdEager } = require('./repos.cjs');
12
+ const { getCadence } = require('./sync.cjs');
12
13
 
13
14
  /**
14
15
  * Safely resolve the current git author string.
@@ -163,6 +164,10 @@ function cmdInitExecutePhase(cwd, phase, raw) {
163
164
  milestone_branch_template: config.milestone_branch_template,
164
165
  verifier_enabled: config.verifier,
165
166
  base_branch: config.base_branch,
167
+ sync_push: config.sync_push,
168
+ sync_pull: config.sync_pull,
169
+ cadence_pull: getCadence('execute-phase').pull,
170
+ cadence_push: getCadence('execute-phase').push,
166
171
 
167
172
  // Phase info
168
173
  phase_found: !!phaseInfo,
@@ -208,11 +213,11 @@ function cmdInitExecutePhase(cwd, phase, raw) {
208
213
  // File existence
209
214
  state_exists: ctx.root ? pathExistsInternal(cwd, path.join(ctx.root, 'STATE.md')) : false,
210
215
  roadmap_exists: ctx.root ? pathExistsInternal(cwd, path.join(ctx.root, 'ROADMAP.md')) : false,
211
- config_exists: pathExistsInternal(cwd, path.join(planRootRel, 'dgs.config.json')),
216
+ config_exists: pathExistsInternal(cwd, path.join(planRootRel, 'config.json')),
212
217
  // File paths (project-qualified)
213
218
  state_path: ctx.root ? path.join(ctx.root, 'STATE.md') : path.join(planRootRel, 'STATE.md'),
214
219
  roadmap_path: ctx.root ? path.join(ctx.root, 'ROADMAP.md') : path.join(planRootRel, 'ROADMAP.md'),
215
- config_path: path.join(planRootRel, 'dgs.config.json'),
220
+ config_path: path.join(planRootRel, 'config.json'),
216
221
  project_path: ctx.root ? path.join(ctx.root, 'PROJECT.md') : path.join(planRootRel, 'PROJECT.md'),
217
222
  debug_dir: ctx.root ? path.join(ctx.root, 'debug') : path.join(planRootRel, 'debug'),
218
223
 
@@ -258,6 +263,10 @@ function cmdInitPlanPhase(cwd, phase, raw) {
258
263
  plan_checker_enabled: config.plan_checker,
259
264
  nyquist_validation_enabled: config.nyquist_validation,
260
265
  commit_docs: config.commit_docs,
266
+ sync_push: config.sync_push,
267
+ sync_pull: config.sync_pull,
268
+ cadence_pull: getCadence('plan-phase').pull,
269
+ cadence_push: getCadence('plan-phase').push,
261
270
 
262
271
  // Phase info
263
272
  phase_found: !!phaseInfo,
@@ -357,6 +366,10 @@ function cmdInitNewProject(cwd, raw) {
357
366
 
358
367
  // Config
359
368
  commit_docs: config.commit_docs,
369
+ sync_push: config.sync_push,
370
+ sync_pull: config.sync_pull,
371
+ cadence_pull: getCadence('new-project').pull,
372
+ cadence_push: getCadence('new-project').push,
360
373
 
361
374
  // Existing state (project-qualified)
362
375
  project_exists: ctx.root ? pathExistsInternal(cwd, path.join(ctx.root, 'PROJECT.md')) : pathExistsInternal(cwd, path.join(planRootRel, 'PROJECT.md')),
@@ -411,6 +424,10 @@ function cmdInitNewMilestone(cwd, raw) {
411
424
  // Config
412
425
  commit_docs: config.commit_docs,
413
426
  research_enabled: config.research,
427
+ sync_push: config.sync_push,
428
+ sync_pull: config.sync_pull,
429
+ cadence_pull: getCadence('new-milestone').pull,
430
+ cadence_push: getCadence('new-milestone').push,
414
431
 
415
432
  // Current milestone
416
433
  current_milestone: milestone.version,
@@ -470,6 +487,10 @@ function cmdInitQuick(cwd, description, raw) {
470
487
 
471
488
  // Config
472
489
  commit_docs: config.commit_docs,
490
+ sync_push: config.sync_push,
491
+ sync_pull: config.sync_pull,
492
+ cadence_pull: getCadence('quick').pull,
493
+ cadence_push: getCadence('quick').push,
473
494
 
474
495
  // Quick task info
475
496
  quick_id: quickId,
@@ -535,6 +556,10 @@ function cmdInitResume(cwd, raw) {
535
556
 
536
557
  // Config
537
558
  commit_docs: config.commit_docs,
559
+ sync_push: config.sync_push,
560
+ sync_pull: config.sync_pull,
561
+ cadence_pull: getCadence('resume-work').pull,
562
+ cadence_push: getCadence('resume-work').push,
538
563
 
539
564
  // v2 context
540
565
  dgs_mode: ctx.dgs_mode,
@@ -563,6 +588,10 @@ function cmdInitVerifyWork(cwd, phase, raw) {
563
588
 
564
589
  // Config
565
590
  commit_docs: config.commit_docs,
591
+ sync_push: config.sync_push,
592
+ sync_pull: config.sync_pull,
593
+ cadence_pull: getCadence('verify-work').pull,
594
+ cadence_push: getCadence('verify-work').push,
566
595
 
567
596
  // Phase info
568
597
  phase_found: !!phaseInfo,
@@ -602,6 +631,10 @@ function cmdInitAuditPhase(cwd, phase, raw) {
602
631
 
603
632
  // Config
604
633
  commit_docs: config.commit_docs,
634
+ sync_push: config.sync_push,
635
+ sync_pull: config.sync_pull,
636
+ cadence_pull: getCadence('audit-phase').pull,
637
+ cadence_push: getCadence('audit-phase').push,
605
638
 
606
639
  // Phase info
607
640
  phase_found: !!phaseInfo,
@@ -625,11 +658,12 @@ function cmdInitAuditPhase(cwd, phase, raw) {
625
658
  output(result, raw);
626
659
  }
627
660
 
628
- function cmdInitPhaseOp(cwd, phase, raw) {
661
+ function cmdInitPhaseOp(cwd, phase, raw, workflow) {
629
662
  const config = loadConfig(cwd);
630
663
  const ctx = resolveProjectContext(cwd);
631
664
  const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
632
665
  let phaseInfo = findPhaseInternal(cwd, phase);
666
+ const cadence = getCadence(workflow || 'plan-phase');
633
667
 
634
668
  // Fallback to ROADMAP.md if no directory exists (e.g., Plans: TBD)
635
669
  if (!phaseInfo) {
@@ -656,6 +690,10 @@ function cmdInitPhaseOp(cwd, phase, raw) {
656
690
  // Config
657
691
  commit_docs: config.commit_docs,
658
692
  brave_search: config.brave_search,
693
+ sync_push: config.sync_push,
694
+ sync_pull: config.sync_pull,
695
+ cadence_pull: cadence.pull,
696
+ cadence_push: cadence.push,
659
697
 
660
698
  // Phase info
661
699
  phase_found: !!phaseInfo,
@@ -720,7 +758,7 @@ function cmdInitPhaseOp(cwd, phase, raw) {
720
758
  output(result, raw);
721
759
  }
722
760
 
723
- function cmdInitTodos(cwd, area, raw) {
761
+ function cmdInitTodos(cwd, area, raw, workflow) {
724
762
  const config = loadConfig(cwd);
725
763
  const ctx = resolveProjectContext(cwd);
726
764
  const now = new Date();
@@ -762,6 +800,10 @@ function cmdInitTodos(cwd, area, raw) {
762
800
  const result = {
763
801
  // Config
764
802
  commit_docs: config.commit_docs,
803
+ sync_push: config.sync_push,
804
+ sync_pull: config.sync_pull,
805
+ cadence_pull: getCadence(workflow || 'check-todos').pull,
806
+ cadence_push: getCadence(workflow || 'check-todos').push,
765
807
 
766
808
  // Timestamps
767
809
  date: now.toISOString().split('T')[0],
@@ -795,11 +837,12 @@ function cmdInitTodos(cwd, area, raw) {
795
837
  output(result, raw);
796
838
  }
797
839
 
798
- function cmdInitMilestoneOp(cwd, raw) {
840
+ function cmdInitMilestoneOp(cwd, raw, workflow) {
799
841
  const config = loadConfig(cwd);
800
842
  const ctx = resolveProjectContext(cwd);
801
843
  const milestone = getMilestoneInfo(cwd);
802
844
  const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
845
+ const cadence = getCadence(workflow || 'complete-milestone');
803
846
 
804
847
  // Count phases (project-qualified)
805
848
  let phaseCount = 0;
@@ -838,6 +881,10 @@ function cmdInitMilestoneOp(cwd, raw) {
838
881
  branching_strategy: config.branching_strategy,
839
882
  phase_branch_template: config.phase_branch_template,
840
883
  milestone_branch_template: config.milestone_branch_template,
884
+ sync_push: config.sync_push,
885
+ sync_pull: config.sync_pull,
886
+ cadence_pull: cadence.pull,
887
+ cadence_push: cadence.push,
841
888
 
842
889
  // Current milestone
843
890
  milestone_version: milestone.version,
@@ -943,6 +990,10 @@ function cmdInitMapCodebase(cwd, onlyRepo, raw) {
943
990
  commit_docs: config.commit_docs,
944
991
  search_gitignored: config.search_gitignored,
945
992
  parallelization: config.parallelization,
993
+ sync_push: config.sync_push,
994
+ sync_pull: config.sync_pull,
995
+ cadence_pull: getCadence('map-codebase').pull,
996
+ cadence_push: getCadence('map-codebase').push,
946
997
 
947
998
  // Paths — codebase stays product-level
948
999
  codebase_dir: path.join(planRootRel, 'codebase'),
@@ -1056,6 +1107,10 @@ function cmdInitProgress(cwd, raw) {
1056
1107
 
1057
1108
  // Config
1058
1109
  commit_docs: config.commit_docs,
1110
+ sync_push: config.sync_push,
1111
+ sync_pull: config.sync_pull,
1112
+ cadence_pull: getCadence('progress').pull,
1113
+ cadence_push: getCadence('progress').push,
1059
1114
 
1060
1115
  // Milestone
1061
1116
  milestone_version: milestone.version,
@@ -1081,7 +1136,7 @@ function cmdInitProgress(cwd, raw) {
1081
1136
  state_path: statePath,
1082
1137
  roadmap_path: ctx.root ? path.join(ctx.root, 'ROADMAP.md') : path.join(planRootRel, 'ROADMAP.md'),
1083
1138
  project_path: ctx.root ? path.join(ctx.root, 'PROJECT.md') : path.join(planRootRel, 'PROJECT.md'),
1084
- config_path: path.join(planRootRel, 'dgs.config.json'),
1139
+ config_path: path.join(planRootRel, 'config.json'),
1085
1140
 
1086
1141
  // Author
1087
1142
  author: resolveAuthorSafe(cwd),
@@ -164,8 +164,8 @@ describe('v1 mode: init execute-phase', () => {
164
164
  assert.equal(result.roadmap_path, '.planning/ROADMAP.md');
165
165
  });
166
166
 
167
- it('returns config_path as .planning/dgs.config.json (always product-level)', () => {
168
- assert.equal(result.config_path, '.planning/dgs.config.json');
167
+ it('returns config_path as .planning/config.json (always product-level)', () => {
168
+ assert.equal(result.config_path, '.planning/config.json');
169
169
  });
170
170
 
171
171
  it('returns project_path as .planning/PROJECT.md', () => {
@@ -248,7 +248,7 @@ describe('v1 mode: init progress', () => {
248
248
  assert.equal(result.state_path, '.planning/STATE.md');
249
249
  assert.equal(result.roadmap_path, '.planning/ROADMAP.md');
250
250
  assert.equal(result.project_path, '.planning/PROJECT.md');
251
- assert.equal(result.config_path, '.planning/dgs.config.json');
251
+ assert.equal(result.config_path, '.planning/config.json');
252
252
  });
253
253
 
254
254
  it('returns dgs_mode v1', () => {
@@ -404,7 +404,7 @@ describe('v2 mode with project: init execute-phase', () => {
404
404
  });
405
405
 
406
406
  it('config_path stays product-level', () => {
407
- assert.equal(result.config_path, '.planning/dgs.config.json');
407
+ assert.equal(result.config_path, '.planning/config.json');
408
408
  });
409
409
 
410
410
  it('returns project-qualified project_path', () => {
@@ -482,7 +482,7 @@ describe('v2 mode with project: init progress', () => {
482
482
  });
483
483
 
484
484
  it('config_path stays product-level', () => {
485
- assert.equal(result.config_path, '.planning/dgs.config.json');
485
+ assert.equal(result.config_path, '.planning/config.json');
486
486
  });
487
487
 
488
488
  it('phase directory is project-qualified', () => {
@@ -505,12 +505,12 @@ describe('v2 mode with project: init todos', () => {
505
505
  fixture.cleanup();
506
506
  });
507
507
 
508
- it('returns project-qualified pending_dir', () => {
509
- assert.equal(result.pending_dir, path.join('.planning', 'projects', 'test-project', 'todos', 'pending'));
508
+ it('returns product-level pending_dir', () => {
509
+ assert.equal(result.pending_dir, path.join('.planning', 'todos', 'pending'));
510
510
  });
511
511
 
512
- it('returns project-qualified completed_dir', () => {
513
- assert.equal(result.completed_dir, path.join('.planning', 'projects', 'test-project', 'todos', 'completed'));
512
+ it('returns product-level completed_dir', () => {
513
+ assert.equal(result.completed_dir, path.join('.planning', 'todos', 'completed'));
514
514
  });
515
515
 
516
516
  it('returns dgs_mode v2', () => {
@@ -138,7 +138,7 @@ function createBackupTag(cwd) {
138
138
  // ─── Migration Planning ─────────────────────────────────────────────────────
139
139
 
140
140
  // Directories to move (project-level, order: directories first)
141
- const DIRS_TO_MOVE = ['phases', 'research', 'todos', 'quick', 'debug'];
141
+ const DIRS_TO_MOVE = ['phases', 'research', 'quick', 'debug'];
142
142
 
143
143
  // Files to move (project-level)
144
144
  const FILES_TO_MOVE = ['PROJECT.md', 'REQUIREMENTS.md', 'ROADMAP.md', 'STATE.md'];
@@ -293,12 +293,13 @@ describe('migrateV1ToV2', () => {
293
293
  assert.ok(content.startsWith('# Projects'));
294
294
  });
295
295
 
296
- it('sets current_project in config.json', () => {
296
+ it('sets current_project in config.local.json', () => {
297
297
  createV1Install(tmpDir, 'Test App');
298
298
  migrateV1ToV2(tmpDir, 'test-app');
299
299
 
300
- const configPath = path.join(tmpDir, '.planning', 'dgs.config.json');
301
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
300
+ const localConfigPath = path.join(tmpDir, '.planning', 'config.local.json');
301
+ assert.ok(fs.existsSync(localConfigPath), 'config.local.json should exist');
302
+ const config = JSON.parse(fs.readFileSync(localConfigPath, 'utf-8'));
302
303
  assert.strictEqual(config.current_project, 'test-app');
303
304
  });
304
305
 
@@ -326,13 +327,13 @@ describe('migrateV1ToV2', () => {
326
327
  assert.ok(log.includes('rename'));
327
328
  });
328
329
 
329
- it('moves todos/ directory if it exists', () => {
330
+ it('preserves todos/ at product level (not moved to project)', () => {
330
331
  writeFile(tmpDir, '.planning/todos/pending/task1.md', '# Todo 1\n');
331
332
  createV1Install(tmpDir, 'Test App');
332
333
  migrateV1ToV2(tmpDir, 'test-app');
333
334
 
334
- assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'test-app', 'todos', 'pending', 'task1.md')));
335
- assert.ok(!fs.existsSync(path.join(tmpDir, '.planning', 'todos')));
335
+ assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'pending', 'task1.md')));
336
+ assert.ok(!fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'test-app', 'todos')));
336
337
  });
337
338
 
338
339
  it('moves quick/ directory if it exists', () => {
@@ -406,18 +407,15 @@ describe('collectMigrationMoves', () => {
406
407
  writeFile(tmpDir, '.planning/PROJECT.md', '# Project: Test\n');
407
408
  writeFile(tmpDir, '.planning/phases/01-setup/01-01-PLAN.md', '---\n---\n');
408
409
  writeFile(tmpDir, '.planning/research/notes.md', '# Notes\n');
409
- writeFile(tmpDir, '.planning/todos/pending/task.md', '# Todo\n');
410
410
 
411
411
  const { moves } = collectMigrationMoves(tmpDir, 'test');
412
412
 
413
413
  const phasesMove = moves.find(m => m.relSource.endsWith('phases'));
414
414
  const researchMove = moves.find(m => m.relSource.endsWith('research'));
415
- const todosMove = moves.find(m => m.relSource.endsWith('todos'));
416
415
  const projectMove = moves.find(m => m.relSource.endsWith('PROJECT.md'));
417
416
 
418
417
  assert.strictEqual(phasesMove.isDir, true);
419
418
  assert.strictEqual(researchMove.isDir, true);
420
- assert.strictEqual(todosMove.isDir, true);
421
419
  assert.strictEqual(projectMove.isDir, false);
422
420
  });
423
421
  });
@@ -41,7 +41,7 @@ const COMPREHENSIVE_PATTERN = /\.planning\//;
41
41
  * multi-project install because they resolve to the root instead of
42
42
  * .planning/<project>/<path>.
43
43
  */
44
- const PROJECT_SCOPED_PATTERN = /\.planning\/(?:STATE|ROADMAP|PROJECT(?!S\.md)|REQUIREMENTS|phases|archive|quick|todos|debug|research)/;
44
+ const PROJECT_SCOPED_PATTERN = /\.planning\/(?:STATE|ROADMAP|PROJECT(?!S\.md)|REQUIREMENTS|phases|archive|quick|debug|research)/;
45
45
 
46
46
  /**
47
47
  * Workflow files allowlisted from comprehensive .planning/ scanning.
@@ -1,19 +1,20 @@
1
1
  /**
2
- * paths.cjs Planning root detection and path constants
2
+ * paths.cjs -- Planning root detection and path constants
3
3
  *
4
4
  * LEAF MODULE: Zero DGS imports. Uses only Node.js built-in fs and path.
5
5
  * This module sits at the bottom of the dependency graph to prevent
6
6
  * circular dependencies with core.cjs and config.cjs.
7
7
  *
8
8
  * Exports:
9
- * getPlanningRoot(cwd) Returns absolute path to planning root
10
- * getPaths(cwd) Returns frozen PATHS object with all standard paths
11
- * initPaths(cwd) Alias for getPaths (explicit cache population)
12
- * resetPaths() Clears the cwd-keyed cache (for test isolation)
9
+ * getPlanningRoot(cwd) -- Returns absolute path to planning root
10
+ * getPaths(cwd) -- Returns frozen PATHS object with all standard paths
11
+ * initPaths(cwd) -- Alias for getPaths (explicit cache population)
12
+ * resetPaths() -- Clears the cwd-keyed cache (for test isolation)
13
13
  *
14
14
  * Detection cascade (getPlanningRoot):
15
- * 1. .planning/dgs.config.json or .planning/config.json exists -> .planning/ layout
16
- * 2. dgs.config.json at root with planningRoot: '.' -> root layout
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: '.')
17
18
  * 3. dgs.config.json at root without planningRoot: '.' -> .planning/ layout
18
19
  * 4. PROJECT.md at root AND no .planning/ directory -> root layout (auto-detect)
19
20
  * 5. No signals -> .planning/ layout (default)
@@ -26,7 +27,7 @@ const _cache = new Map();
26
27
 
27
28
  /**
28
29
  * The canonical directory name for the projects container.
29
- * Single source of truth all project path construction should reference
30
+ * Single source of truth -- all project path construction should reference
30
31
  * this constant instead of using the string literal 'projects'.
31
32
  */
32
33
  const PROJECTS_DIR = 'projects';
@@ -41,32 +42,40 @@ function getPlanningRoot(cwd) {
41
42
  const resolved = path.resolve(cwd || process.cwd());
42
43
  const dotPlanning = path.join(resolved, '.planning');
43
44
 
44
- // Step 1: .planning/ layout config files inside .planning/
45
+ // Step 1: .planning/ layout -- config files inside .planning/
45
46
  if (
46
- fs.existsSync(path.join(dotPlanning, 'dgs.config.json')) ||
47
- fs.existsSync(path.join(dotPlanning, 'config.json'))
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'))
48
50
  ) {
49
51
  return dotPlanning;
50
52
  }
51
53
 
52
- // Step 2: Root layout dgs.config.json at repo root
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
+ }
63
+ }
64
+
65
+ // Step 2b: Legacy fallback -- dgs.config.json at repo root
53
66
  const rootConfigPath = path.join(resolved, 'dgs.config.json');
54
67
  if (fs.existsSync(rootConfigPath)) {
55
68
  try {
56
69
  const config = JSON.parse(fs.readFileSync(rootConfigPath, 'utf-8'));
57
70
  if (config.planningRoot === '.') return resolved;
58
71
  } catch {
59
- // Malformed JSON fall through to next check
72
+ // Malformed JSON -- fall through to next check
60
73
  }
61
74
  // dgs.config.json exists but no planningRoot: '.' or malformed
62
- // If malformed, we already fell through the try/catch
63
- // If valid but no planningRoot: '.', return .planning/
64
- // We need to distinguish: if we got here via catch, we should fall through
65
- // If we got here via the normal flow (no planningRoot), return .planning/
66
75
  return dotPlanning;
67
76
  }
68
77
 
69
- // Step 3: Auto-detect root layout PROJECT.md at root, no .planning/ dir
78
+ // Step 3: Auto-detect root layout -- PROJECT.md at root, no .planning/ dir
70
79
  if (
71
80
  fs.existsSync(path.join(resolved, 'PROJECT.md')) &&
72
81
  !fs.existsSync(dotPlanning)
@@ -74,7 +83,7 @@ function getPlanningRoot(cwd) {
74
83
  return resolved;
75
84
  }
76
85
 
77
- // Step 4: Default .planning/
86
+ // Step 4: Default -- .planning/
78
87
  return dotPlanning;
79
88
  }
80
89
 
@@ -101,8 +110,9 @@ function getPaths(cwd) {
101
110
  DOCS: path.join(root, 'docs'),
102
111
  CODEBASE: path.join(root, 'codebase'),
103
112
  MILESTONES: path.join(root, 'milestones'),
104
- CONFIG: path.join(root, 'dgs.config.json'),
105
- CONFIG_LEGACY: path.join(root, 'config.json'),
113
+ CONFIG: path.join(root, 'config.json'),
114
+ CONFIG_LOCAL: path.join(root, 'config.local.json'),
115
+ CONFIG_LEGACY: path.join(root, 'dgs.config.json'),
106
116
  QUICK: path.join(root, 'quick'),
107
117
  TODOS: path.join(root, 'todos'),
108
118
  RESEARCH: path.join(root, 'research'),
@@ -118,7 +128,7 @@ function getPaths(cwd) {
118
128
 
119
129
  /**
120
130
  * Populate the cache for the given cwd and return the PATHS object.
121
- * Alias for getPaths provides explicit intent for test setup.
131
+ * Alias for getPaths -- provides explicit intent for test setup.
122
132
  *
123
133
  * @param {string} [cwd] - Working directory (defaults to process.cwd())
124
134
  * @returns {Readonly<Object>} Frozen PATHS object
@@ -185,11 +185,11 @@ describe('PATHS object shape', () => {
185
185
 
186
186
  const EXPECTED_KEYS = [
187
187
  'ROOT', 'PHASES', 'IDEAS', 'SPECS', 'JOBS', 'DOCS',
188
- 'CODEBASE', 'MILESTONES', 'CONFIG', 'CONFIG_LEGACY',
188
+ 'CODEBASE', 'MILESTONES', 'CONFIG', 'CONFIG_LOCAL', 'CONFIG_LEGACY',
189
189
  'QUICK', 'TODOS', 'RESEARCH', 'DEBUG', 'ARCHIVE', 'PROJECTS', 'LAYOUT',
190
190
  ];
191
191
 
192
- it('returns object with all 17 expected keys', () => {
192
+ it('returns object with all 18 expected keys', () => {
193
193
  cwd = makeTempDir();
194
194
  fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
195
195
  fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
@@ -235,22 +235,32 @@ describe('PATHS object shape', () => {
235
235
  }
236
236
  });
237
237
 
238
- it('CONFIG ends with dgs.config.json', () => {
238
+ it('CONFIG ends with config.json', () => {
239
239
  cwd = makeTempDir();
240
240
  fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
241
241
  fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
242
242
 
243
243
  const paths = getPaths(cwd);
244
- assert.ok(paths.CONFIG.endsWith('dgs.config.json'));
244
+ assert.ok(paths.CONFIG.endsWith('config.json'));
245
+ assert.ok(!paths.CONFIG.endsWith('dgs.config.json'));
245
246
  });
246
247
 
247
- it('CONFIG_LEGACY ends with config.json', () => {
248
+ it('CONFIG_LOCAL ends with config.local.json', () => {
248
249
  cwd = makeTempDir();
249
250
  fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
250
251
  fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
251
252
 
252
253
  const paths = getPaths(cwd);
253
- assert.ok(paths.CONFIG_LEGACY.endsWith('config.json'));
254
+ assert.ok(paths.CONFIG_LOCAL.endsWith('config.local.json'));
255
+ });
256
+
257
+ it('CONFIG_LEGACY ends with dgs.config.json', () => {
258
+ cwd = makeTempDir();
259
+ fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
260
+ fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
261
+
262
+ const paths = getPaths(cwd);
263
+ assert.ok(paths.CONFIG_LEGACY.endsWith('dgs.config.json'));
254
264
  });
255
265
 
256
266
  it('subdirectory paths are derived from ROOT', () => {
@@ -14,7 +14,7 @@ const { getPlanningRoot } = require('./paths.cjs');
14
14
 
15
15
  // ─── Constants ──────────────────────────────────────────────────────────────
16
16
 
17
- const STANDARD_DIRS = ['phases', 'research', 'todos', 'quick', 'debug'];
17
+ const STANDARD_DIRS = ['phases', 'research', 'quick', 'debug'];
18
18
 
19
19
  // ─── Project Subfolder Creation ─────────────────────────────────────────────
20
20
 
@@ -64,7 +64,7 @@ describe('createProjectSubfolder', () => {
64
64
 
65
65
  it('creates all required subdirectories', () => {
66
66
  createProjectSubfolder(tmpDir, 'auth-overhaul', 'Auth Overhaul');
67
- const expectedDirs = ['phases', 'research', 'todos', 'quick', 'debug'];
67
+ const expectedDirs = ['phases', 'research', 'quick', 'debug'];
68
68
  for (const dir of expectedDirs) {
69
69
  assert.ok(
70
70
  fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'auth-overhaul', dir)),