@ktpartners/dgs-platform 2.8.0 → 3.0.4

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 (94) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +41 -13
  3. package/agents/dgs-plan-checker.md +29 -3
  4. package/agents/dgs-planner.md +10 -0
  5. package/commands/dgs/abandon-quick.md +28 -0
  6. package/commands/dgs/add-tests.md +2 -2
  7. package/commands/dgs/audit-milestone.md +2 -2
  8. package/commands/dgs/capture-principle.md +11 -11
  9. package/commands/dgs/cleanup.md +2 -2
  10. package/commands/dgs/complete-milestone.md +11 -11
  11. package/commands/dgs/complete-quick.md +28 -0
  12. package/commands/dgs/create-milestone-job.md +2 -2
  13. package/commands/dgs/debug.md +3 -3
  14. package/commands/dgs/develop-idea.md +1 -1
  15. package/commands/dgs/fast.md +3 -1
  16. package/commands/dgs/health.md +1 -1
  17. package/commands/dgs/map-codebase.md +6 -6
  18. package/commands/dgs/new-milestone.md +5 -5
  19. package/commands/dgs/new-project.md +6 -6
  20. package/commands/dgs/plan-milestone-gaps.md +1 -1
  21. package/commands/dgs/progress.md +3 -3
  22. package/commands/dgs/quick-abandon.md +8 -0
  23. package/commands/dgs/quick-complete.md +8 -0
  24. package/commands/dgs/quick.md +10 -3
  25. package/commands/dgs/research-idea.md +2 -2
  26. package/commands/dgs/research-phase.md +3 -3
  27. package/commands/dgs/switch-project.md +1 -1
  28. package/commands/dgs/write-spec.md +3 -3
  29. package/deliver-great-systems/bin/dgs-tools.cjs +284 -30
  30. package/deliver-great-systems/bin/lib/commands.cjs +316 -31
  31. package/deliver-great-systems/bin/lib/commands.test.cjs +336 -0
  32. package/deliver-great-systems/bin/lib/config.cjs +39 -6
  33. package/deliver-great-systems/bin/lib/context.cjs +120 -0
  34. package/deliver-great-systems/bin/lib/core.cjs +28 -11
  35. package/deliver-great-systems/bin/lib/execution.cjs +49 -17
  36. package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
  37. package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
  38. package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
  39. package/deliver-great-systems/bin/lib/init.cjs +306 -39
  40. package/deliver-great-systems/bin/lib/init.test.cjs +416 -6
  41. package/deliver-great-systems/bin/lib/jobs.cjs +124 -21
  42. package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
  43. package/deliver-great-systems/bin/lib/migration.cjs +409 -1
  44. package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
  45. package/deliver-great-systems/bin/lib/milestone.cjs +54 -29
  46. package/deliver-great-systems/bin/lib/phase.cjs +128 -2
  47. package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
  48. package/deliver-great-systems/bin/lib/projects.cjs +28 -8
  49. package/deliver-great-systems/bin/lib/projects.test.cjs +86 -0
  50. package/deliver-great-systems/bin/lib/quick.cjs +584 -0
  51. package/deliver-great-systems/bin/lib/quick.test.cjs +596 -0
  52. package/deliver-great-systems/bin/lib/repos.cjs +25 -1
  53. package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
  54. package/deliver-great-systems/bin/lib/specs.cjs +3 -81
  55. package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
  56. package/deliver-great-systems/bin/lib/state.cjs +142 -54
  57. package/deliver-great-systems/bin/lib/sync.cjs +75 -0
  58. package/deliver-great-systems/bin/lib/verify.cjs +80 -1
  59. package/deliver-great-systems/bin/lib/worktrees.cjs +764 -0
  60. package/deliver-great-systems/bin/lib/worktrees.test.cjs +887 -0
  61. package/deliver-great-systems/templates/claude-md.md +16 -0
  62. package/deliver-great-systems/workflows/abandon-quick.md +89 -0
  63. package/deliver-great-systems/workflows/add-idea.md +3 -3
  64. package/deliver-great-systems/workflows/add-tests.md +14 -0
  65. package/deliver-great-systems/workflows/add-todo.md +1 -0
  66. package/deliver-great-systems/workflows/approve-spec.md +25 -4
  67. package/deliver-great-systems/workflows/audit-phase.md +15 -5
  68. package/deliver-great-systems/workflows/cancel-job.md +1 -1
  69. package/deliver-great-systems/workflows/check-todos.md +2 -3
  70. package/deliver-great-systems/workflows/complete-milestone.md +197 -22
  71. package/deliver-great-systems/workflows/complete-quick.md +68 -0
  72. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  73. package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
  74. package/deliver-great-systems/workflows/develop-idea.md +11 -11
  75. package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
  76. package/deliver-great-systems/workflows/discuss-idea.md +1 -1
  77. package/deliver-great-systems/workflows/execute-phase.md +121 -32
  78. package/deliver-great-systems/workflows/execute-plan.md +12 -21
  79. package/deliver-great-systems/workflows/help.md +33 -29
  80. package/deliver-great-systems/workflows/init-product.md +2 -18
  81. package/deliver-great-systems/workflows/new-milestone.md +40 -24
  82. package/deliver-great-systems/workflows/new-project.md +22 -680
  83. package/deliver-great-systems/workflows/progress-all.md +133 -0
  84. package/deliver-great-systems/workflows/quick-abandon.md +89 -0
  85. package/deliver-great-systems/workflows/quick-complete.md +68 -0
  86. package/deliver-great-systems/workflows/quick.md +152 -23
  87. package/deliver-great-systems/workflows/refine-spec.md +1 -1
  88. package/deliver-great-systems/workflows/research-idea.md +8 -8
  89. package/deliver-great-systems/workflows/resume-project.md +2 -2
  90. package/deliver-great-systems/workflows/run-job.md +8 -8
  91. package/deliver-great-systems/workflows/validate-phase.md +39 -1
  92. package/deliver-great-systems/workflows/verify-work.md +14 -0
  93. package/deliver-great-systems/workflows/write-spec.md +2 -2
  94. package/package.json +1 -1
@@ -663,85 +663,94 @@ describe('jobs', () => {
663
663
 
664
664
  describe('generateMilestoneSteps', () => {
665
665
 
666
- it('produces 4 steps for unplanned phase (no_directory)', () => {
666
+ it('produces 5 steps for unplanned phase (no_directory)', () => {
667
667
  const phases = [
668
668
  { number: '50', name: 'Job Creation', disk_status: 'no_directory', roadmap_complete: false },
669
669
  ];
670
670
  const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
671
671
 
672
- assert.equal(steps.length, 4);
672
+ assert.equal(steps.length, 5);
673
673
  assert.equal(steps[0].command, 'map-codebase');
674
674
  assert.equal(steps[0].args, '50 --auto');
675
675
  assert.equal(steps[1].command, 'plan-phase');
676
676
  assert.equal(steps[1].args, '50 --non-interactive');
677
677
  assert.equal(steps[2].command, 'execute-phase');
678
678
  assert.equal(steps[2].args, '50 --non-interactive');
679
- assert.equal(steps[3].command, 'audit-phase');
680
- assert.equal(steps[3].args, '50');
679
+ assert.equal(steps[3].command, 'validate-phase');
680
+ assert.equal(steps[3].args, '50 --scaffold');
681
+ assert.equal(steps[4].command, 'audit-phase');
682
+ assert.equal(steps[4].args, '50');
681
683
  });
682
684
 
683
- it('produces 4 steps for unplanned phase (empty)', () => {
685
+ it('produces 5 steps for unplanned phase (empty)', () => {
684
686
  const phases = [
685
687
  { number: '51', name: 'Job Execution', disk_status: 'empty', roadmap_complete: false },
686
688
  ];
687
689
  const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
688
690
 
689
- assert.equal(steps.length, 4);
691
+ assert.equal(steps.length, 5);
690
692
  assert.equal(steps[0].command, 'map-codebase');
691
693
  assert.equal(steps[1].command, 'plan-phase');
692
694
  assert.equal(steps[2].command, 'execute-phase');
693
- assert.equal(steps[3].command, 'audit-phase');
695
+ assert.equal(steps[3].command, 'validate-phase');
696
+ assert.equal(steps[4].command, 'audit-phase');
694
697
  });
695
698
 
696
- it('produces 4 steps for discussed phase (context but no plan)', () => {
699
+ it('produces 5 steps for discussed phase (context but no plan)', () => {
697
700
  const phases = [
698
701
  { number: '52', name: 'Silent Mode', disk_status: 'discussed', roadmap_complete: false },
699
702
  ];
700
703
  const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
701
704
 
702
- assert.equal(steps.length, 4);
705
+ assert.equal(steps.length, 5);
703
706
  assert.equal(steps[0].command, 'map-codebase');
704
707
  assert.equal(steps[1].command, 'plan-phase');
705
708
  assert.equal(steps[2].command, 'execute-phase');
706
- assert.equal(steps[3].command, 'audit-phase');
709
+ assert.equal(steps[3].command, 'validate-phase');
710
+ assert.equal(steps[4].command, 'audit-phase');
707
711
  });
708
712
 
709
- it('produces 4 steps for researched phase', () => {
713
+ it('produces 5 steps for researched phase', () => {
710
714
  const phases = [
711
715
  { number: '53', name: 'Audit Integration', disk_status: 'researched', roadmap_complete: false },
712
716
  ];
713
717
  const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
714
718
 
715
- assert.equal(steps.length, 4);
719
+ assert.equal(steps.length, 5);
716
720
  assert.equal(steps[0].command, 'map-codebase');
717
721
  assert.equal(steps[1].command, 'plan-phase');
718
722
  assert.equal(steps[2].command, 'execute-phase');
719
- assert.equal(steps[3].command, 'audit-phase');
723
+ assert.equal(steps[3].command, 'validate-phase');
724
+ assert.equal(steps[4].command, 'audit-phase');
720
725
  });
721
726
 
722
- it('produces 2 steps for planned phase', () => {
727
+ it('produces 3 steps for planned phase', () => {
723
728
  const phases = [
724
729
  { number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
725
730
  ];
726
731
  const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
727
732
 
728
- assert.equal(steps.length, 2);
733
+ assert.equal(steps.length, 3);
729
734
  assert.equal(steps[0].command, 'execute-phase');
730
735
  assert.equal(steps[0].args, '50 --non-interactive');
731
- assert.equal(steps[1].command, 'audit-phase');
732
- assert.equal(steps[1].args, '50');
736
+ assert.equal(steps[1].command, 'validate-phase');
737
+ assert.equal(steps[1].args, '50 --scaffold');
738
+ assert.equal(steps[2].command, 'audit-phase');
739
+ assert.equal(steps[2].args, '50');
733
740
  });
734
741
 
735
- it('produces 2 steps for partially executed phase', () => {
742
+ it('produces 3 steps for partially executed phase', () => {
736
743
  const phases = [
737
744
  { number: '50', name: 'Job Creation', disk_status: 'partial', roadmap_complete: false },
738
745
  ];
739
746
  const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
740
747
 
741
- assert.equal(steps.length, 2);
748
+ assert.equal(steps.length, 3);
742
749
  assert.equal(steps[0].command, 'execute-phase');
743
- assert.equal(steps[1].command, 'audit-phase');
744
- assert.equal(steps[1].args, '50');
750
+ assert.equal(steps[1].command, 'validate-phase');
751
+ assert.equal(steps[1].args, '50 --scaffold');
752
+ assert.equal(steps[2].command, 'audit-phase');
753
+ assert.equal(steps[2].args, '50');
745
754
  });
746
755
 
747
756
  it('produces 0 steps for completed phase (disk_status complete)', () => {
@@ -771,34 +780,38 @@ describe('jobs', () => {
771
780
  const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
772
781
 
773
782
  // Phase 49: 0 steps (complete)
774
- // Phase 50: 2 steps (planned -> execute + verify)
775
- // Phase 51: 4 steps (unplanned -> map + plan + execute + verify)
776
- assert.equal(steps.length, 6);
783
+ // Phase 50: 3 steps (planned -> execute + validate + audit)
784
+ // Phase 51: 5 steps (unplanned -> map + plan + execute + validate + audit)
785
+ assert.equal(steps.length, 8);
777
786
  assert.equal(steps[0].command, 'execute-phase');
778
787
  assert.equal(steps[0].args, '50 --non-interactive');
779
- assert.equal(steps[1].command, 'audit-phase');
780
- assert.equal(steps[1].args, '50');
781
- assert.equal(steps[2].command, 'map-codebase');
782
- assert.equal(steps[2].args, '51 --auto');
783
- assert.equal(steps[3].command, 'plan-phase');
784
- assert.equal(steps[3].args, '51 --non-interactive');
785
- assert.equal(steps[4].command, 'execute-phase');
788
+ assert.equal(steps[1].command, 'validate-phase');
789
+ assert.equal(steps[1].args, '50 --scaffold');
790
+ assert.equal(steps[2].command, 'audit-phase');
791
+ assert.equal(steps[2].args, '50');
792
+ assert.equal(steps[3].command, 'map-codebase');
793
+ assert.equal(steps[3].args, '51 --auto');
794
+ assert.equal(steps[4].command, 'plan-phase');
786
795
  assert.equal(steps[4].args, '51 --non-interactive');
787
- assert.equal(steps[5].command, 'audit-phase');
788
- assert.equal(steps[5].args, '51');
796
+ assert.equal(steps[5].command, 'execute-phase');
797
+ assert.equal(steps[5].args, '51 --non-interactive');
798
+ assert.equal(steps[6].command, 'validate-phase');
799
+ assert.equal(steps[6].args, '51 --scaffold');
800
+ assert.equal(steps[7].command, 'audit-phase');
801
+ assert.equal(steps[7].args, '51');
789
802
  });
790
803
 
791
- it('appends audit and complete steps when check=true', () => {
804
+ it('appends audit step when check=true', () => {
792
805
  const phases = [
793
806
  { number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
794
807
  ];
795
808
  const steps = generateMilestoneSteps(phases, { check: true, version: 'v6.0' });
796
809
 
797
- // 2 phase steps + 2 check steps = 4
810
+ // 3 phase steps + 1 audit step = 4
798
811
  assert.equal(steps.length, 4);
799
- assert.equal(steps[2].command, 'audit-milestone');
800
- assert.equal(steps[2].args, 'v6.0');
801
- assert.equal(steps[3].command, 'complete-milestone');
812
+ assert.equal(steps[1].command, 'validate-phase');
813
+ assert.equal(steps[2].command, 'audit-phase');
814
+ assert.equal(steps[3].command, 'audit-milestone');
802
815
  assert.equal(steps[3].args, 'v6.0');
803
816
  });
804
817
 
@@ -808,22 +821,21 @@ describe('jobs', () => {
808
821
  ];
809
822
  const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
810
823
 
811
- assert.equal(steps.length, 2);
812
- // No audit/complete steps
824
+ assert.equal(steps.length, 3);
825
+ // No audit-milestone/complete-milestone steps
813
826
  assert.ok(!steps.some(s => s.command === 'audit-milestone'));
814
827
  assert.ok(!steps.some(s => s.command === 'complete-milestone'));
815
828
  });
816
829
 
817
- it('returns only audit+complete when all phases complete and check=true', () => {
830
+ it('returns only audit when all phases complete and check=true', () => {
818
831
  const phases = [
819
832
  { number: '49', name: 'Job File Format', disk_status: 'complete', roadmap_complete: true },
820
833
  { number: '50', name: 'Job Creation', disk_status: 'complete', roadmap_complete: true },
821
834
  ];
822
835
  const steps = generateMilestoneSteps(phases, { check: true, version: 'v6.0' });
823
836
 
824
- assert.equal(steps.length, 2);
837
+ assert.equal(steps.length, 1);
825
838
  assert.equal(steps[0].command, 'audit-milestone');
826
- assert.equal(steps[1].command, 'complete-milestone');
827
839
  });
828
840
 
829
841
  it('returns empty array when all phases complete and check=false', () => {
@@ -844,15 +856,15 @@ describe('jobs', () => {
844
856
  ];
845
857
  const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
846
858
 
847
- // Phase 50: 2 steps (execute + verify), Phase 50.1: 4 steps (map + plan + execute + verify), Phase 51: 2 steps (execute + verify) = 8 total
848
- assert.equal(steps.length, 8);
859
+ // Phase 50: 3 steps (execute + validate + audit), Phase 50.1: 5 steps (map + plan + execute + validate + audit), Phase 51: 3 steps (execute + validate + audit) = 11 total
860
+ assert.equal(steps.length, 11);
849
861
  // First phase should be 50 (execute-phase)
850
862
  assert.equal(steps[0].args, '50 --non-interactive');
851
- // Then 50.1 (map-codebase at index 2)
852
- assert.equal(steps[2].command, 'map-codebase');
853
- assert.equal(steps[2].args, '50.1 --auto');
854
- // Then 51 (execute-phase at index 6)
855
- assert.equal(steps[6].args, '51 --non-interactive');
863
+ // Then 50.1 (map-codebase at index 3)
864
+ assert.equal(steps[3].command, 'map-codebase');
865
+ assert.equal(steps[3].args, '50.1 --auto');
866
+ // Then 51 (execute-phase at index 8)
867
+ assert.equal(steps[8].args, '51 --non-interactive');
856
868
  });
857
869
 
858
870
  it('includes correct flags on plan-phase (--non-interactive), execute-phase (--non-interactive), map-codebase (--auto), and audit-phase (phase number only) steps', () => {
@@ -864,11 +876,14 @@ describe('jobs', () => {
864
876
  const mapStep = steps.find(s => s.command === 'map-codebase');
865
877
  const planStep = steps.find(s => s.command === 'plan-phase');
866
878
  const executeStep = steps.find(s => s.command === 'execute-phase');
879
+ const validateStep = steps.find(s => s.command === 'validate-phase');
867
880
  const auditStep = steps.find(s => s.command === 'audit-phase');
868
881
 
869
882
  assert.ok(mapStep.args.includes('--auto'), 'map-codebase should have --auto');
870
883
  assert.ok(planStep.args.includes('--non-interactive'), 'plan-phase should have --non-interactive');
871
884
  assert.ok(executeStep.args.includes('--non-interactive'), 'execute-phase should have --non-interactive');
885
+ assert.ok(validateStep.args.includes('--scaffold'), 'validate-phase should have --scaffold');
886
+ assert.ok(!validateStep.args.includes('--non-interactive'), 'validate-phase should NOT have --non-interactive');
872
887
  assert.strictEqual(auditStep.args, '50', 'audit-phase should have just the phase number');
873
888
  });
874
889
 
@@ -880,12 +895,14 @@ describe('jobs', () => {
880
895
  ];
881
896
  const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
882
897
 
883
- // Phase 49 complete (0 steps), Phase 50 planned (2 steps), Phase 51 planned (2 steps) = 4
884
- assert.equal(steps.length, 4);
898
+ // Phase 49 complete (0 steps), Phase 50 planned (3 steps), Phase 51 planned (3 steps) = 6
899
+ assert.equal(steps.length, 6);
885
900
  assert.equal(steps[0].args, '50 --non-interactive');
886
- assert.equal(steps[1].args, '50');
887
- assert.equal(steps[2].args, '51 --non-interactive');
888
- assert.equal(steps[3].args, '51');
901
+ assert.equal(steps[1].args, '50 --scaffold');
902
+ assert.equal(steps[2].args, '50');
903
+ assert.equal(steps[3].args, '51 --non-interactive');
904
+ assert.equal(steps[4].args, '51 --scaffold');
905
+ assert.equal(steps[5].args, '51');
889
906
  });
890
907
 
891
908
  it('returns steps as objects with command and args properties', () => {
@@ -929,6 +946,21 @@ describe('jobs', () => {
929
946
  }
930
947
  });
931
948
 
949
+ it('validate-phase steps use --scaffold flag (not --non-interactive or --auto)', () => {
950
+ const phases = [
951
+ { number: '50', name: 'Test', disk_status: 'no_directory', roadmap_complete: false },
952
+ { number: '51', name: 'Test2', disk_status: 'planned', roadmap_complete: false },
953
+ ];
954
+ const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
955
+ const validateSteps = steps.filter(s => s.command === 'validate-phase');
956
+ assert.equal(validateSteps.length, 2, 'Should have 2 validate-phase steps');
957
+ for (const step of validateSteps) {
958
+ assert.ok(step.args.includes('--scaffold'), `validate-phase step for ${step.args} should have --scaffold`);
959
+ assert.ok(!step.args.includes('--auto'), 'validate-phase should NOT have --auto');
960
+ assert.ok(!step.args.includes('--non-interactive'), 'validate-phase should NOT have --non-interactive');
961
+ }
962
+ });
963
+
932
964
  it('no map-codebase for NEEDS_EXECUTION phases', () => {
933
965
  const phases = [
934
966
  { number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
@@ -1085,7 +1117,7 @@ Plans:
1085
1117
  assert.equal(result.created, true);
1086
1118
  });
1087
1119
 
1088
- it('writes job file to jobs/pending/', () => {
1120
+ it('writes job file to flat jobs/ directory', () => {
1089
1121
  fixture = createFixture({
1090
1122
  'ROADMAP.md': FIXTURE_ROADMAP,
1091
1123
  'phases/49-job-file-format/49-01-PLAN.md': '',
@@ -1097,14 +1129,14 @@ Plans:
1097
1129
  });
1098
1130
 
1099
1131
  const result = cmdJobsCreateMilestone(fixture.cwd, 'v6.0', true, false);
1100
- assert.ok(result.file.includes('pending'));
1101
- assert.ok(result.file.includes('milestone-v6.0.md'));
1132
+ assert.ok(result.file.includes('jobs/milestone-v6.0.md'));
1133
+ assert.ok(!result.file.includes('pending'), 'should write to flat jobs/ not jobs/pending/');
1102
1134
 
1103
1135
  const jobFilePath = path.join(fixture.cwd, result.file);
1104
1136
  assert.ok(fs.existsSync(jobFilePath), 'Job file should exist on disk');
1105
1137
  });
1106
1138
 
1107
- it('auto-creates jobs/pending/ directory', () => {
1139
+ it('auto-creates jobs/ directory', () => {
1108
1140
  fixture = createFixture({
1109
1141
  'ROADMAP.md': FIXTURE_ROADMAP,
1110
1142
  'phases/49-job-file-format/49-01-PLAN.md': '',
@@ -1115,12 +1147,12 @@ Plans:
1115
1147
  'phases/50-job-creation/50-02-PLAN.md': '',
1116
1148
  });
1117
1149
 
1118
- // Ensure pending dir does not exist yet
1119
- const pendingDir = path.join(fixture.cwd, 'jobs', 'pending');
1120
- assert.ok(!fs.existsSync(pendingDir), 'pending dir should not exist before call');
1150
+ // Ensure jobs dir does not exist yet
1151
+ const jobsDir = path.join(fixture.cwd, 'jobs');
1152
+ assert.ok(!fs.existsSync(jobsDir), 'jobs dir should not exist before call');
1121
1153
 
1122
1154
  cmdJobsCreateMilestone(fixture.cwd, 'v6.0', true, false);
1123
- assert.ok(fs.existsSync(pendingDir), 'pending dir should be auto-created');
1155
+ assert.ok(fs.existsSync(jobsDir), 'jobs dir should be auto-created');
1124
1156
  });
1125
1157
 
1126
1158
  it('returns JSON with expected fields', () => {
@@ -2019,10 +2051,9 @@ Plans:
2019
2051
  if (fixture) fixture.cleanup();
2020
2052
  });
2021
2053
 
2022
- it('cancels in-progress job: resets [>] steps to [ ], updates Status, moves to pending/', () => {
2054
+ it('cancels in-progress job: resets [>] steps to [ ], updates Status via frontmatter', () => {
2023
2055
  fixture = createFixture({
2024
2056
  'jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
2025
- 'jobs/pending/': null,
2026
2057
  });
2027
2058
  const result = cancelJob(fixture.cwd, 'v6.0');
2028
2059
 
@@ -2031,15 +2062,14 @@ Plans:
2031
2062
  assert.ok(result.path);
2032
2063
  assert.equal(result.steps_reset, 1); // one [>] step
2033
2064
 
2034
- // Verify file moved to pending
2035
- assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending', 'milestone-v6.0.md')));
2036
- assert.ok(!fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress', 'milestone-v6.0.md')));
2065
+ // File stays in place — status updated via frontmatter, no file move
2066
+ assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress', 'milestone-v6.0.md')));
2037
2067
 
2038
- // Verify content: [>] reset to [ ], [x] preserved
2039
- const content = fs.readFileSync(path.join(fixture.cwd, 'jobs', 'pending', 'milestone-v6.0.md'), 'utf-8');
2040
- assert.ok(content.includes('**Status:** pending'), 'Status should be pending');
2068
+ // Verify content: [>] reset to [ ], [x] preserved, Status header updated
2069
+ const content = fs.readFileSync(path.join(fixture.cwd, 'jobs', 'in-progress', 'milestone-v6.0.md'), 'utf-8');
2070
+ assert.ok(content.includes('**Status:** pending'), 'Status header should be pending');
2071
+ assert.ok(content.includes('status: pending'), 'Frontmatter status should be pending');
2041
2072
  assert.ok(!content.includes('[>]'), 'No in-progress markers should remain');
2042
- // completed steps preserved
2043
2073
  assert.ok(content.includes('[x]'), 'Completed steps should be preserved');
2044
2074
  });
2045
2075
 
@@ -2071,11 +2101,11 @@ Plans:
2071
2101
  it('keeps completed [x] steps marked done', () => {
2072
2102
  fixture = createFixture({
2073
2103
  'jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
2074
- 'jobs/pending/': null,
2075
2104
  });
2076
2105
  cancelJob(fixture.cwd, 'v6.0');
2077
2106
 
2078
- const content = fs.readFileSync(path.join(fixture.cwd, 'jobs', 'pending', 'milestone-v6.0.md'), 'utf-8');
2107
+ // File stays in place read from in-progress/
2108
+ const content = fs.readFileSync(path.join(fixture.cwd, 'jobs', 'in-progress', 'milestone-v6.0.md'), 'utf-8');
2079
2109
  const stepLines = content.split('\n').filter(l => l.trim().startsWith('- ['));
2080
2110
  const completedSteps = stepLines.filter(l => l.includes('[x]'));
2081
2111
  assert.ok(completedSteps.length > 0, 'Completed steps should be preserved');
@@ -2618,3 +2648,92 @@ describe('rollbackJob', () => {
2618
2648
  assert.equal(result.reason, 'no_start_shas');
2619
2649
  });
2620
2650
  });
2651
+
2652
+ // ─── JOB_STATUSES constant ──────────────────────────────────────────────────
2653
+
2654
+ describe('JOB_STATUSES constant', () => {
2655
+ it('exports correct values', () => {
2656
+ const { JOB_STATUSES } = require('./jobs.cjs');
2657
+ assert.deepStrictEqual(JOB_STATUSES, ['pending', 'in-progress', 'completed', 'failed', 'rolled_back']);
2658
+ });
2659
+ });
2660
+
2661
+ // ─── setJobStatus ───────────────────────────────────────────────────────────
2662
+
2663
+ describe('setJobStatus', () => {
2664
+ it('throws on invalid status', () => {
2665
+ const { setJobStatus } = require('./jobs.cjs');
2666
+ assert.throws(() => {
2667
+ setJobStatus('/nonexistent', 'v99.0', 'invalid-status');
2668
+ }, (err) => {
2669
+ assert.ok(err.message.includes('Invalid status'));
2670
+ assert.ok(err.message.includes('pending'));
2671
+ assert.ok(err.message.includes('in-progress'));
2672
+ return true;
2673
+ });
2674
+ });
2675
+ });
2676
+
2677
+ // ─── findJobFile flat-first scanning ──────────────────────────────────────
2678
+
2679
+ describe('findJobFile flat-first scanning', () => {
2680
+ let fixture;
2681
+ afterEach(() => { if (fixture) fixture.cleanup(); });
2682
+
2683
+ it('finds job in flat jobs/ directory', () => {
2684
+ const jobContent = '---\nstatus: pending\n---\n# Milestone Job: v20.0\n\n**Version:** v20.0\n**Created:** 2026-04-06T00:00:00Z\n**Status:** pending\n**Check:** true\n\n## Steps\n\n- [ ] `/dgs:plan-phase 132`\n';
2685
+ fixture = createFixture({
2686
+ 'config.json': '{}',
2687
+ 'jobs/milestone-v20.0.md': jobContent,
2688
+ });
2689
+ const result = findJobFile(fixture.cwd, 'v20.0');
2690
+ assert.equal(result.found, true);
2691
+ assert.equal(result.status, 'pending');
2692
+ assert.equal(result.directory, null, 'Flat files should have null directory');
2693
+ assert.ok(result.path.endsWith('jobs/milestone-v20.0.md'));
2694
+ });
2695
+
2696
+ it('reads status from bold-key header when no YAML frontmatter', () => {
2697
+ const jobContent = '# Milestone Job: v19.0\n\n**Version:** v19.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** completed\n**Check:** false\n\n## Steps\n\n- [x] `/dgs:plan-phase 100` \u2014 completed\n';
2698
+ fixture = createFixture({
2699
+ 'config.json': '{}',
2700
+ 'jobs/milestone-v19.0.md': jobContent,
2701
+ });
2702
+ const result = findJobFile(fixture.cwd, 'v19.0');
2703
+ assert.equal(result.found, true);
2704
+ assert.equal(result.status, 'completed');
2705
+ assert.equal(result.directory, null);
2706
+ });
2707
+
2708
+ it('falls back to legacy subdirectory with warning', () => {
2709
+ const jobContent = '# Milestone Job: v18.0\n\n**Version:** v18.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** in-progress\n**Check:** true\n\n## Steps\n\n- [>] `/dgs:plan-phase 90`\n';
2710
+ fixture = createFixture({
2711
+ 'config.json': '{}',
2712
+ 'jobs/in-progress/milestone-v18.0.md': jobContent,
2713
+ });
2714
+ const origWrite = process.stderr.write;
2715
+ let stderrOutput = '';
2716
+ process.stderr.write = (msg) => { stderrOutput += msg; };
2717
+ try {
2718
+ const result = findJobFile(fixture.cwd, 'v18.0');
2719
+ assert.equal(result.found, true);
2720
+ assert.ok(stderrOutput.includes('[DGS] Warning:'), 'Should emit legacy warning');
2721
+ assert.ok(stderrOutput.includes('legacy'), 'Warning should mention legacy');
2722
+ assert.equal(result.directory, 'in-progress');
2723
+ } finally {
2724
+ process.stderr.write = origWrite;
2725
+ }
2726
+ });
2727
+
2728
+ it('prefers flat over legacy', () => {
2729
+ const flatJob = '---\nstatus: pending\n---\n# Milestone Job: v17.0\n\n**Version:** v17.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** pending\n**Check:** true\n\n## Steps\n\n- [ ] `/dgs:plan-phase 80`\n';
2730
+ const legacyJob = '# Milestone Job: v17.0\n\n**Version:** v17.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** pending\n**Check:** true\n\n## Steps\n\n- [ ] `/dgs:plan-phase 80`\n';
2731
+ fixture = createFixture({
2732
+ 'config.json': '{}',
2733
+ 'jobs/milestone-v17.0.md': flatJob,
2734
+ 'jobs/pending/milestone-v17.0.md': legacyJob,
2735
+ });
2736
+ const result = findJobFile(fixture.cwd, 'v17.0');
2737
+ assert.equal(result.directory, null, 'Flat should win over legacy');
2738
+ });
2739
+ });