@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.
- package/agents/dgs-executor.md +51 -0
- package/commands/dgs/sync.md +70 -0
- package/deliver-great-systems/bin/dgs-tools.cjs +290 -4
- package/deliver-great-systems/bin/lib/config.cjs +259 -67
- package/deliver-great-systems/bin/lib/core.cjs +49 -8
- package/deliver-great-systems/bin/lib/core.test.cjs +35 -14
- package/deliver-great-systems/bin/lib/init.cjs +61 -6
- package/deliver-great-systems/bin/lib/init.test.cjs +9 -9
- package/deliver-great-systems/bin/lib/migration.cjs +1 -1
- package/deliver-great-systems/bin/lib/migration.test.cjs +7 -9
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +1 -1
- package/deliver-great-systems/bin/lib/paths.cjs +32 -22
- package/deliver-great-systems/bin/lib/paths.test.cjs +16 -6
- package/deliver-great-systems/bin/lib/projects.cjs +1 -1
- package/deliver-great-systems/bin/lib/projects.test.cjs +1 -1
- package/deliver-great-systems/bin/lib/repos.cjs +29 -10
- package/deliver-great-systems/bin/lib/state.cjs +2 -2
- package/deliver-great-systems/bin/lib/sync.cjs +878 -0
- package/deliver-great-systems/bin/lib/test-helpers.cjs +44 -12
- package/deliver-great-systems/references/git-integration.md +81 -0
- package/deliver-great-systems/references/planning-config.md +154 -31
- package/deliver-great-systems/references/sync-cadence.md +191 -0
- package/deliver-great-systems/references/sync-hooks.md +96 -0
- package/deliver-great-systems/test/cadence.test.cjs +160 -0
- package/deliver-great-systems/test/sync-workflow.test.cjs +562 -0
- package/deliver-great-systems/workflows/execute-phase.md +111 -4
- package/deliver-great-systems/workflows/init-product.md +6 -2
- package/deliver-great-systems/workflows/run-job.md +77 -2
- package/deliver-great-systems/workflows/settings.md +82 -1
- 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
|
|
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
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
|
755
|
-
// Root layout with
|
|
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
|
|
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
|
|
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
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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/
|
|
168
|
-
assert.equal(result.config_path, '.planning/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
509
|
-
assert.equal(result.pending_dir, path.join('.planning', '
|
|
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
|
|
513
|
-
assert.equal(result.completed_dir, path.join('.planning', '
|
|
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', '
|
|
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
|
|
301
|
-
|
|
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('
|
|
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', '
|
|
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|
|
|
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
|
|
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)
|
|
10
|
-
* getPaths(cwd)
|
|
11
|
-
* initPaths(cwd)
|
|
12
|
-
* resetPaths()
|
|
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/
|
|
16
|
-
* 2.
|
|
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
|
|
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
|
|
45
|
+
// Step 1: .planning/ layout -- config files inside .planning/
|
|
45
46
|
if (
|
|
46
|
-
fs.existsSync(path.join(dotPlanning, '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, '
|
|
105
|
-
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
244
|
+
assert.ok(paths.CONFIG.endsWith('config.json'));
|
|
245
|
+
assert.ok(!paths.CONFIG.endsWith('dgs.config.json'));
|
|
245
246
|
});
|
|
246
247
|
|
|
247
|
-
it('
|
|
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.
|
|
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', '
|
|
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', '
|
|
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)),
|