@ktpartners/dgs-platform 2.7.4 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/agents/dgs-executor.md +0 -52
  3. package/deliver-great-systems/bin/dgs-tools.cjs +66 -10
  4. package/deliver-great-systems/bin/lib/commands.cjs +1 -8
  5. package/deliver-great-systems/bin/lib/config.cjs +9 -90
  6. package/deliver-great-systems/bin/lib/context.cjs +2 -2
  7. package/deliver-great-systems/bin/lib/context.test.cjs +100 -100
  8. package/deliver-great-systems/bin/lib/core.cjs +17 -57
  9. package/deliver-great-systems/bin/lib/core.test.cjs +166 -170
  10. package/deliver-great-systems/bin/lib/docs.cjs +3 -3
  11. package/deliver-great-systems/bin/lib/docs.test.cjs +14 -7
  12. package/deliver-great-systems/bin/lib/execution.cjs +2 -2
  13. package/deliver-great-systems/bin/lib/execution.test.cjs +65 -67
  14. package/deliver-great-systems/bin/lib/ideas.cjs +4 -4
  15. package/deliver-great-systems/bin/lib/ideas.test.cjs +45 -44
  16. package/deliver-great-systems/bin/lib/init.cjs +9 -4
  17. package/deliver-great-systems/bin/lib/init.test.cjs +242 -175
  18. package/deliver-great-systems/bin/lib/jobs.cjs +1 -1
  19. package/deliver-great-systems/bin/lib/jobs.test.cjs +203 -202
  20. package/deliver-great-systems/bin/lib/migration.cjs +256 -281
  21. package/deliver-great-systems/bin/lib/migration.test.cjs +385 -440
  22. package/deliver-great-systems/bin/lib/milestone.cjs +1 -1
  23. package/deliver-great-systems/bin/lib/overlap.cjs +4 -4
  24. package/deliver-great-systems/bin/lib/overlap.test.cjs +45 -44
  25. package/deliver-great-systems/bin/lib/path-audit.test.cjs +16 -22
  26. package/deliver-great-systems/bin/lib/paths.cjs +60 -59
  27. package/deliver-great-systems/bin/lib/paths.test.cjs +192 -225
  28. package/deliver-great-systems/bin/lib/phase.cjs +5 -4
  29. package/deliver-great-systems/bin/lib/projects.cjs +8 -8
  30. package/deliver-great-systems/bin/lib/projects.test.cjs +75 -74
  31. package/deliver-great-systems/bin/lib/repos.cjs +94 -230
  32. package/deliver-great-systems/bin/lib/repos.test.cjs +84 -75
  33. package/deliver-great-systems/bin/lib/search.cjs +4 -4
  34. package/deliver-great-systems/bin/lib/specs.cjs +2 -2
  35. package/deliver-great-systems/bin/lib/sync.cjs +1 -1
  36. package/deliver-great-systems/bin/lib/template.cjs +3 -3
  37. package/deliver-great-systems/bin/lib/test-helpers.cjs +59 -162
  38. package/deliver-great-systems/bin/lib/verify.cjs +3 -3
  39. package/deliver-great-systems/references/planning-config.md +7 -8
  40. package/deliver-great-systems/workflows/add-tests.md +1 -1
  41. package/deliver-great-systems/workflows/approve-spec.md +1 -11
  42. package/deliver-great-systems/workflows/complete-milestone.md +2 -2
  43. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  44. package/deliver-great-systems/workflows/create-milestone-job.md +2 -2
  45. package/deliver-great-systems/workflows/discuss-phase.md +2 -2
  46. package/deliver-great-systems/workflows/execute-phase.md +63 -4
  47. package/deliver-great-systems/workflows/execute-plan.md +0 -51
  48. package/deliver-great-systems/workflows/find-related-ideas.md +1 -1
  49. package/deliver-great-systems/workflows/help.md +25 -58
  50. package/deliver-great-systems/workflows/init-product.md +14 -451
  51. package/deliver-great-systems/workflows/map-codebase.md +109 -0
  52. package/deliver-great-systems/workflows/new-project.md +0 -1
  53. package/deliver-great-systems/workflows/quick.md +6 -7
  54. package/deliver-great-systems/workflows/run-job.md +56 -0
  55. package/deliver-great-systems/workflows/settings.md +30 -0
  56. package/package.json +5 -1
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tests for core.cjs v2 path resolution functions
2
+ * Tests for core.cjs path resolution functions
3
3
  *
4
4
  * Uses Node.js built-in test runner (node:test) and assert (node:assert).
5
5
  * Each test creates an isolated temp directory fixture and cleans up after.
@@ -26,17 +26,17 @@ const {
26
26
  getProjectDir,
27
27
  } = require('./core.cjs');
28
28
 
29
- // ─── v1 Mode Tests ────────────────────────────────────────────────────────────
29
+ // ─── Root layout (no v2 markers) Tests ───────────────────────────────────────
30
30
 
31
- describe('v1 mode (no v2 markers)', () => {
31
+ describe('root layout (no v2 markers)', () => {
32
32
  let fixture;
33
33
 
34
34
  beforeEach(() => {
35
35
  fixture = createFixture({
36
- '.planning/config.json': JSON.stringify({}),
37
- '.planning/STATE.md': '# State',
38
- '.planning/ROADMAP.md': '# Roadmap',
39
- '.planning/phases/': null,
36
+ 'config.json': JSON.stringify({}),
37
+ 'STATE.md': '# State',
38
+ 'ROADMAP.md': '# Roadmap',
39
+ 'phases/': null,
40
40
  });
41
41
  });
42
42
 
@@ -44,24 +44,24 @@ describe('v1 mode (no v2 markers)', () => {
44
44
  fixture.cleanup();
45
45
  });
46
46
 
47
- it('getProjectRoot returns .planning', () => {
47
+ it('getProjectRoot returns .', () => {
48
48
  const result = getProjectRoot(fixture.cwd);
49
- assert.equal(result, '.planning');
49
+ assert.equal(result, '.');
50
50
  });
51
51
 
52
- it('resolveProjectPath with relative path returns .planning/<path>', () => {
52
+ it('resolveProjectPath with relative path returns relative path', () => {
53
53
  const result = resolveProjectPath(fixture.cwd, 'STATE.md');
54
- assert.equal(result, path.join('.planning', 'STATE.md'));
54
+ assert.equal(result, 'STATE.md');
55
55
  });
56
56
 
57
57
  it('resolveProjectPath with nested path returns correct path', () => {
58
58
  const result = resolveProjectPath(fixture.cwd, 'phases/01-foo');
59
- assert.equal(result, path.join('.planning', 'phases', '01-foo'));
59
+ assert.equal(result, path.join('phases', '01-foo'));
60
60
  });
61
61
 
62
- it('resolveProjectPath without second arg returns .planning', () => {
62
+ it('resolveProjectPath without second arg returns .', () => {
63
63
  const result = resolveProjectPath(fixture.cwd);
64
- assert.equal(result, '.planning');
64
+ assert.equal(result, '.');
65
65
  });
66
66
 
67
67
  it('isV2Install returns false', () => {
@@ -77,12 +77,13 @@ describe('v2 mode with current_project', () => {
77
77
 
78
78
  beforeEach(() => {
79
79
  fixture = createFixture({
80
- '.planning/config.json': JSON.stringify({ current_project: 'auth-overhaul' }),
81
- '.planning/PROJECTS.md': '# Projects\n\n| Project | Status |\n',
82
- '.planning/REPOS.md': '# Repos\n\n| Name | Path |\n',
83
- '.planning/projects/auth-overhaul/STATE.md': '# State',
84
- '.planning/projects/auth-overhaul/ROADMAP.md': '# Roadmap',
85
- '.planning/projects/auth-overhaul/phases/': null,
80
+ 'config.json': JSON.stringify({}),
81
+ 'config.local.json': JSON.stringify({ current_project: 'auth-overhaul' }),
82
+ 'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
83
+ 'REPOS.md': '# Repos\n\n| Name | Path |\n',
84
+ 'projects/auth-overhaul/STATE.md': '# State',
85
+ 'projects/auth-overhaul/ROADMAP.md': '# Roadmap',
86
+ 'projects/auth-overhaul/phases/': null,
86
87
  });
87
88
  });
88
89
 
@@ -90,19 +91,19 @@ describe('v2 mode with current_project', () => {
90
91
  fixture.cleanup();
91
92
  });
92
93
 
93
- it('getProjectRoot returns .planning/projects/<project>', () => {
94
+ it('getProjectRoot returns projects/<project>', () => {
94
95
  const result = getProjectRoot(fixture.cwd);
95
- assert.equal(result, path.join('.planning', 'projects', 'auth-overhaul'));
96
+ assert.equal(result, path.join('projects', 'auth-overhaul'));
96
97
  });
97
98
 
98
99
  it('resolveProjectPath with STATE.md returns project-qualified path', () => {
99
100
  const result = resolveProjectPath(fixture.cwd, 'STATE.md');
100
- assert.equal(result, path.join('.planning', 'projects', 'auth-overhaul', 'STATE.md'));
101
+ assert.equal(result, path.join('projects', 'auth-overhaul', 'STATE.md'));
101
102
  });
102
103
 
103
104
  it('resolveProjectPath with nested path returns project-qualified path', () => {
104
105
  const result = resolveProjectPath(fixture.cwd, 'phases/01-foo');
105
- assert.equal(result, path.join('.planning', 'projects', 'auth-overhaul', 'phases', '01-foo'));
106
+ assert.equal(result, path.join('projects', 'auth-overhaul', 'phases', '01-foo'));
106
107
  });
107
108
  });
108
109
 
@@ -111,8 +112,8 @@ describe('v2 mode with current_project', () => {
111
112
  describe('v2 mode without current_project (guard trigger)', () => {
112
113
  it('throws NO_CURRENT_PROJECT_V2 when PROJECTS.md exists with valid header', () => {
113
114
  const fixture = createFixture({
114
- '.planning/config.json': JSON.stringify({}),
115
- '.planning/PROJECTS.md': '# Projects\n\n| Project | Status |\n',
115
+ 'config.json': JSON.stringify({}),
116
+ 'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
116
117
  });
117
118
 
118
119
  try {
@@ -127,8 +128,8 @@ describe('v2 mode without current_project (guard trigger)', () => {
127
128
 
128
129
  it('throws NO_CURRENT_PROJECT_V2 when REPOS.md exists with valid header', () => {
129
130
  const fixture = createFixture({
130
- '.planning/config.json': JSON.stringify({}),
131
- '.planning/REPOS.md': '# Repos\n\n| Name | Path |\n',
131
+ 'config.json': JSON.stringify({}),
132
+ 'REPOS.md': '# Repos\n\n| Name | Path |\n',
132
133
  });
133
134
 
134
135
  try {
@@ -147,8 +148,8 @@ describe('v2 mode without current_project (guard trigger)', () => {
147
148
  describe('strict v2 marker validation', () => {
148
149
  it('isV2Install returns false when PROJECTS.md has non-DGS content', () => {
149
150
  const fixture = createFixture({
150
- '.planning/config.json': JSON.stringify({}),
151
- '.planning/PROJECTS.md': 'Shopping List\n- apples\n- bananas',
151
+ 'config.json': JSON.stringify({}),
152
+ 'PROJECTS.md': 'Shopping List\n- apples\n- bananas',
152
153
  });
153
154
 
154
155
  try {
@@ -160,8 +161,8 @@ describe('strict v2 marker validation', () => {
160
161
 
161
162
  it('isV2Install returns true when PROJECTS.md starts with # Projects', () => {
162
163
  const fixture = createFixture({
163
- '.planning/config.json': JSON.stringify({}),
164
- '.planning/PROJECTS.md': '# Projects\n\n## Active\n',
164
+ 'config.json': JSON.stringify({}),
165
+ 'PROJECTS.md': '# Projects\n\n## Active\n',
165
166
  });
166
167
 
167
168
  try {
@@ -173,8 +174,8 @@ describe('strict v2 marker validation', () => {
173
174
 
174
175
  it('isV2Install returns true when REPOS.md starts with # Repos', () => {
175
176
  const fixture = createFixture({
176
- '.planning/config.json': JSON.stringify({}),
177
- '.planning/REPOS.md': '# Repos\n\n| Name | Path |\n',
177
+ 'config.json': JSON.stringify({}),
178
+ 'REPOS.md': '# Repos\n\n| Name | Path |\n',
178
179
  });
179
180
 
180
181
  try {
@@ -186,7 +187,7 @@ describe('strict v2 marker validation', () => {
186
187
 
187
188
  it('isV2Install returns false when neither file exists', () => {
188
189
  const fixture = createFixture({
189
- '.planning/config.json': JSON.stringify({}),
190
+ 'config.json': JSON.stringify({}),
190
191
  });
191
192
 
192
193
  try {
@@ -202,8 +203,9 @@ describe('strict v2 marker validation', () => {
202
203
  describe('project directory validation', () => {
203
204
  it('throws PROJECT_NOT_FOUND when current_project dir does not exist', () => {
204
205
  const fixture = createFixture({
205
- '.planning/config.json': JSON.stringify({ current_project: 'ghost-project' }),
206
- '.planning/PROJECTS.md': '# Projects\n',
206
+ 'config.json': JSON.stringify({}),
207
+ 'config.local.json': JSON.stringify({ current_project: 'ghost-project' }),
208
+ 'PROJECTS.md': '# Projects\n',
207
209
  });
208
210
 
209
211
  try {
@@ -222,9 +224,10 @@ describe('project directory validation', () => {
222
224
  describe('completed project guards', () => {
223
225
  it('getProjectRoot throws PROJECT_COMPLETED when project STATUS is completed', () => {
224
226
  const fixture = createFixture({
225
- '.planning/config.json': JSON.stringify({ current_project: 'done-proj' }),
226
- '.planning/PROJECTS.md': '# Projects\n',
227
- '.planning/projects/done-proj/STATE.md': '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n',
227
+ 'config.json': JSON.stringify({}),
228
+ 'config.local.json': JSON.stringify({ current_project: 'done-proj' }),
229
+ 'PROJECTS.md': '# Projects\n',
230
+ 'projects/done-proj/STATE.md': '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n',
228
231
  });
229
232
 
230
233
  try {
@@ -239,9 +242,10 @@ describe('completed project guards', () => {
239
242
 
240
243
  it('getProjectRoot throws PROJECT_COMPLETED for case-insensitive status (Completed)', () => {
241
244
  const fixture = createFixture({
242
- '.planning/config.json': JSON.stringify({ current_project: 'done-proj' }),
243
- '.planning/PROJECTS.md': '# Projects\n',
244
- '.planning/projects/done-proj/STATE.md': '# Project State\n\nStatus: Completed\n',
245
+ 'config.json': JSON.stringify({}),
246
+ 'config.local.json': JSON.stringify({ current_project: 'done-proj' }),
247
+ 'PROJECTS.md': '# Projects\n',
248
+ 'projects/done-proj/STATE.md': '# Project State\n\nStatus: Completed\n',
245
249
  });
246
250
 
247
251
  try {
@@ -256,14 +260,15 @@ describe('completed project guards', () => {
256
260
 
257
261
  it('getProjectRoot does NOT throw for active project', () => {
258
262
  const fixture = createFixture({
259
- '.planning/config.json': JSON.stringify({ current_project: 'active-proj' }),
260
- '.planning/PROJECTS.md': '# Projects\n',
261
- '.planning/projects/active-proj/STATE.md': '# Project State\n\nStatus: Active\n',
263
+ 'config.json': JSON.stringify({}),
264
+ 'config.local.json': JSON.stringify({ current_project: 'active-proj' }),
265
+ 'PROJECTS.md': '# Projects\n',
266
+ 'projects/active-proj/STATE.md': '# Project State\n\nStatus: Active\n',
262
267
  });
263
268
 
264
269
  try {
265
270
  const result = getProjectRoot(fixture.cwd);
266
- assert.equal(result, path.join('.planning', 'projects', 'active-proj'));
271
+ assert.equal(result, path.join('projects', 'active-proj'));
267
272
  } finally {
268
273
  fixture.cleanup();
269
274
  }
@@ -271,14 +276,15 @@ describe('completed project guards', () => {
271
276
 
272
277
  it('getProjectRoot does NOT throw when STATE.md is missing', () => {
273
278
  const fixture = createFixture({
274
- '.planning/config.json': JSON.stringify({ current_project: 'no-state-proj' }),
275
- '.planning/PROJECTS.md': '# Projects\n',
276
- '.planning/projects/no-state-proj/': null,
279
+ 'config.json': JSON.stringify({}),
280
+ 'config.local.json': JSON.stringify({ current_project: 'no-state-proj' }),
281
+ 'PROJECTS.md': '# Projects\n',
282
+ 'projects/no-state-proj/': null,
277
283
  });
278
284
 
279
285
  try {
280
286
  const result = getProjectRoot(fixture.cwd);
281
- assert.equal(result, path.join('.planning', 'projects', 'no-state-proj'));
287
+ assert.equal(result, path.join('projects', 'no-state-proj'));
282
288
  } finally {
283
289
  fixture.cleanup();
284
290
  }
@@ -286,9 +292,10 @@ describe('completed project guards', () => {
286
292
 
287
293
  it('requireProjectRoot propagates PROJECT_COMPLETED', () => {
288
294
  const fixture = createFixture({
289
- '.planning/config.json': JSON.stringify({ current_project: 'done-proj' }),
290
- '.planning/PROJECTS.md': '# Projects\n',
291
- '.planning/projects/done-proj/STATE.md': '# Project State\n\nStatus: completed\n',
295
+ 'config.json': JSON.stringify({}),
296
+ 'config.local.json': JSON.stringify({ current_project: 'done-proj' }),
297
+ 'PROJECTS.md': '# Projects\n',
298
+ 'projects/done-proj/STATE.md': '# Project State\n\nStatus: completed\n',
292
299
  });
293
300
 
294
301
  try {
@@ -303,9 +310,10 @@ describe('completed project guards', () => {
303
310
 
304
311
  it('resolveProjectPath throws PROJECT_COMPLETED for completed project', () => {
305
312
  const fixture = createFixture({
306
- '.planning/config.json': JSON.stringify({ current_project: 'done-proj' }),
307
- '.planning/PROJECTS.md': '# Projects\n',
308
- '.planning/projects/done-proj/STATE.md': '# Project State\n\nStatus: completed\n',
313
+ 'config.json': JSON.stringify({}),
314
+ 'config.local.json': JSON.stringify({ current_project: 'done-proj' }),
315
+ 'PROJECTS.md': '# Projects\n',
316
+ 'projects/done-proj/STATE.md': '# Project State\n\nStatus: completed\n',
309
317
  });
310
318
 
311
319
  try {
@@ -320,8 +328,8 @@ describe('completed project guards', () => {
320
328
 
321
329
  it('isProjectCompleted returns true for completed project', () => {
322
330
  const fixture = createFixture({
323
- '.planning/config.json': JSON.stringify({}),
324
- '.planning/projects/done-proj/STATE.md': '# Project State\n\nStatus: completed\n',
331
+ 'config.json': JSON.stringify({}),
332
+ 'projects/done-proj/STATE.md': '# Project State\n\nStatus: completed\n',
325
333
  });
326
334
 
327
335
  try {
@@ -333,8 +341,8 @@ describe('completed project guards', () => {
333
341
 
334
342
  it('isProjectCompleted returns false for active project', () => {
335
343
  const fixture = createFixture({
336
- '.planning/config.json': JSON.stringify({}),
337
- '.planning/projects/active-proj/STATE.md': '# Project State\n\nStatus: Active\n',
344
+ 'config.json': JSON.stringify({}),
345
+ 'projects/active-proj/STATE.md': '# Project State\n\nStatus: Active\n',
338
346
  });
339
347
 
340
348
  try {
@@ -346,8 +354,8 @@ describe('completed project guards', () => {
346
354
 
347
355
  it('isProjectCompleted returns false when STATE.md missing', () => {
348
356
  const fixture = createFixture({
349
- '.planning/config.json': JSON.stringify({}),
350
- '.planning/projects/no-state-proj/': null,
357
+ 'config.json': JSON.stringify({}),
358
+ 'projects/no-state-proj/': null,
351
359
  });
352
360
 
353
361
  try {
@@ -361,11 +369,11 @@ describe('completed project guards', () => {
361
369
  // ─── getProjectFolders Tests ──────────────────────────────────────────────────
362
370
 
363
371
  describe('getProjectFolders', () => {
364
- it('returns empty array for v1 install', () => {
372
+ it('returns empty array when no v2 markers exist', () => {
365
373
  const fixture = createFixture({
366
- '.planning/config.json': JSON.stringify({}),
367
- '.planning/STATE.md': '# State',
368
- '.planning/phases/': null,
374
+ 'config.json': JSON.stringify({}),
375
+ 'STATE.md': '# State',
376
+ 'phases/': null,
369
377
  });
370
378
 
371
379
  try {
@@ -378,14 +386,14 @@ describe('getProjectFolders', () => {
378
386
 
379
387
  it('returns project folder names from projects/ directory', () => {
380
388
  const fixture = createFixture({
381
- '.planning/config.json': JSON.stringify({}),
382
- '.planning/PROJECTS.md': '# Projects\n',
383
- '.planning/projects/auth-overhaul/STATE.md': '# State',
384
- '.planning/projects/dashboard-v2/STATE.md': '# State',
385
- '.planning/phases/': null,
386
- '.planning/codebase/': null,
387
- '.planning/archive/': null,
388
- '.planning/todos/': null,
389
+ 'config.json': JSON.stringify({}),
390
+ 'PROJECTS.md': '# Projects\n',
391
+ 'projects/auth-overhaul/STATE.md': '# State',
392
+ 'projects/dashboard-v2/STATE.md': '# State',
393
+ 'phases/': null,
394
+ 'codebase/': null,
395
+ 'archive/': null,
396
+ 'todos/': null,
389
397
  });
390
398
 
391
399
  try {
@@ -398,10 +406,10 @@ describe('getProjectFolders', () => {
398
406
 
399
407
  it('only includes directories containing STATE.md', () => {
400
408
  const fixture = createFixture({
401
- '.planning/config.json': JSON.stringify({}),
402
- '.planning/PROJECTS.md': '# Projects\n',
403
- '.planning/projects/valid-project/STATE.md': '# State',
404
- '.planning/projects/empty-folder/': null,
409
+ 'config.json': JSON.stringify({}),
410
+ 'PROJECTS.md': '# Projects\n',
411
+ 'projects/valid-project/STATE.md': '# State',
412
+ 'projects/empty-folder/': null,
405
413
  });
406
414
 
407
415
  try {
@@ -414,9 +422,9 @@ describe('getProjectFolders', () => {
414
422
 
415
423
  it('excludes dot-directories', () => {
416
424
  const fixture = createFixture({
417
- '.planning/config.json': JSON.stringify({}),
418
- '.planning/projects/.hidden/STATE.md': '# State',
419
- '.planning/projects/real-project/STATE.md': '# State',
425
+ 'config.json': JSON.stringify({}),
426
+ 'projects/.hidden/STATE.md': '# State',
427
+ 'projects/real-project/STATE.md': '# State',
420
428
  });
421
429
 
422
430
  try {
@@ -427,21 +435,11 @@ describe('getProjectFolders', () => {
427
435
  }
428
436
  });
429
437
 
430
- it('returns empty array when .planning does not exist', () => {
431
- const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'dgs-test-'));
432
- try {
433
- const result = getProjectFolders(cwd);
434
- assert.deepEqual(result, []);
435
- } finally {
436
- fs.rmSync(cwd, { recursive: true, force: true });
437
- }
438
- });
439
-
440
438
  it('returns empty array when projects/ directory does not exist', () => {
441
439
  const fixture = createFixture({
442
- '.planning/config.json': JSON.stringify({}),
443
- '.planning/PROJECTS.md': '# Projects\n',
444
- '.planning/phases/': null,
440
+ 'config.json': JSON.stringify({}),
441
+ 'PROJECTS.md': '# Projects\n',
442
+ 'phases/': null,
445
443
  });
446
444
 
447
445
  try {
@@ -476,37 +474,39 @@ describe('getV2Hint', () => {
476
474
  describe('edge cases', () => {
477
475
  it('resolveProjectPath with empty string returns project root', () => {
478
476
  const fixture = createFixture({
479
- '.planning/config.json': JSON.stringify({}),
477
+ 'config.json': JSON.stringify({}),
480
478
  });
481
479
 
482
480
  try {
483
481
  const result = resolveProjectPath(fixture.cwd, '');
484
- assert.equal(result, '.planning');
482
+ assert.equal(result, '.');
485
483
  } finally {
486
484
  fixture.cleanup();
487
485
  }
488
486
  });
489
487
 
490
- it('getProjectRoot handles missing .planning gracefully', () => {
491
- const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'dgs-test-'));
488
+ it('getProjectRoot returns . when no markers exist', () => {
489
+ const fixture = createFixture({
490
+ 'config.json': JSON.stringify({}),
491
+ });
492
+
492
493
  try {
493
- // Should not crash — returns .planning (v1 fallback since no markers)
494
- const result = getProjectRoot(cwd);
495
- assert.equal(result, '.planning');
494
+ const result = getProjectRoot(fixture.cwd);
495
+ assert.equal(result, '.');
496
496
  } finally {
497
- fs.rmSync(cwd, { recursive: true, force: true });
497
+ fixture.cleanup();
498
498
  }
499
499
  });
500
500
 
501
501
  it('treats empty string current_project as unset', () => {
502
502
  const fixture = createFixture({
503
- '.planning/config.json': JSON.stringify({ current_project: '' }),
503
+ 'config.json': JSON.stringify({}),
504
+ 'config.local.json': JSON.stringify({ current_project: '' }),
504
505
  });
505
506
 
506
507
  try {
507
- // Should return .planning (v1 fallback), not .planning/
508
508
  const result = getProjectRoot(fixture.cwd);
509
- assert.equal(result, '.planning');
509
+ assert.equal(result, '.');
510
510
  } finally {
511
511
  fixture.cleanup();
512
512
  }
@@ -514,8 +514,9 @@ describe('edge cases', () => {
514
514
 
515
515
  it('rejects current_project with path traversal (..)', () => {
516
516
  const fixture = createFixture({
517
- '.planning/config.json': JSON.stringify({ current_project: '../etc' }),
518
- '.planning/PROJECTS.md': '# Projects\n',
517
+ 'config.json': JSON.stringify({}),
518
+ 'config.local.json': JSON.stringify({ current_project: '../etc' }),
519
+ 'PROJECTS.md': '# Projects\n',
519
520
  });
520
521
 
521
522
  try {
@@ -530,8 +531,9 @@ describe('edge cases', () => {
530
531
 
531
532
  it('rejects current_project with forward slash', () => {
532
533
  const fixture = createFixture({
533
- '.planning/config.json': JSON.stringify({ current_project: 'foo/bar' }),
534
- '.planning/PROJECTS.md': '# Projects\n',
534
+ 'config.json': JSON.stringify({}),
535
+ 'config.local.json': JSON.stringify({ current_project: 'foo/bar' }),
536
+ 'PROJECTS.md': '# Projects\n',
535
537
  });
536
538
 
537
539
  try {
@@ -546,8 +548,9 @@ describe('edge cases', () => {
546
548
 
547
549
  it('rejects current_project with backslash', () => {
548
550
  const fixture = createFixture({
549
- '.planning/config.json': JSON.stringify({ current_project: 'foo\\bar' }),
550
- '.planning/PROJECTS.md': '# Projects\n',
551
+ 'config.json': JSON.stringify({}),
552
+ 'config.local.json': JSON.stringify({ current_project: 'foo\\bar' }),
553
+ 'PROJECTS.md': '# Projects\n',
551
554
  });
552
555
 
553
556
  try {
@@ -564,12 +567,18 @@ describe('edge cases', () => {
564
567
  // ─── requireProjectRoot Tests ─────────────────────────────────────────────────
565
568
 
566
569
  describe('requireProjectRoot', () => {
567
- it('returns .planning/projects/<project> when v2 install has current_project set and directory exists', () => {
568
- const fixture = createTempProject({ version: 2, project: 'my-app' });
570
+ it('returns projects/<project> when v2 install has current_project set and directory exists', () => {
571
+ const fixture = createFixture({
572
+ 'config.json': JSON.stringify({}),
573
+ 'config.local.json': JSON.stringify({ current_project: 'my-app' }),
574
+ 'PROJECTS.md': '# Projects\n',
575
+ 'REPOS.md': '# Repos\n',
576
+ 'projects/my-app/STATE.md': '# State',
577
+ });
569
578
 
570
579
  try {
571
580
  const result = requireProjectRoot(fixture.cwd);
572
- assert.equal(result, path.join('.planning', 'projects', 'my-app'));
581
+ assert.equal(result, path.join('projects', 'my-app'));
573
582
  } finally {
574
583
  fixture.cleanup();
575
584
  }
@@ -577,8 +586,8 @@ describe('requireProjectRoot', () => {
577
586
 
578
587
  it('throws NO_CURRENT_PROJECT_V2 when v2 install has no current_project', () => {
579
588
  const fixture = createFixture({
580
- '.planning/config.json': JSON.stringify({}),
581
- '.planning/PROJECTS.md': '# Projects\n\n| Project | Status |\n',
589
+ 'config.json': JSON.stringify({}),
590
+ 'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
582
591
  });
583
592
 
584
593
  try {
@@ -593,8 +602,9 @@ describe('requireProjectRoot', () => {
593
602
 
594
603
  it('throws NO_CURRENT_PROJECT_V2 when v2 install has empty string current_project', () => {
595
604
  const fixture = createFixture({
596
- '.planning/config.json': JSON.stringify({ current_project: '' }),
597
- '.planning/PROJECTS.md': '# Projects\n\n| Project | Status |\n',
605
+ 'config.json': JSON.stringify({}),
606
+ 'config.local.json': JSON.stringify({ current_project: '' }),
607
+ 'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
598
608
  });
599
609
 
600
610
  try {
@@ -609,8 +619,9 @@ describe('requireProjectRoot', () => {
609
619
 
610
620
  it('throws NO_CURRENT_PROJECT_V2 when v2 install has whitespace-only current_project', () => {
611
621
  const fixture = createFixture({
612
- '.planning/config.json': JSON.stringify({ current_project: ' ' }),
613
- '.planning/PROJECTS.md': '# Projects\n\n| Project | Status |\n',
622
+ 'config.json': JSON.stringify({}),
623
+ 'config.local.json': JSON.stringify({ current_project: ' ' }),
624
+ 'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
614
625
  });
615
626
 
616
627
  try {
@@ -624,13 +635,12 @@ describe('requireProjectRoot', () => {
624
635
  });
625
636
 
626
637
  it('throws PROJECT_NOT_FOUND when v2 install has current_project pointing to nonexistent directory', () => {
627
- const fixture = createTempProject({ version: 2, project: 'real-project' });
628
-
629
- // Manually update local config to point at a nonexistent project
630
- fs.writeFileSync(
631
- path.join(fixture.cwd, '.planning', 'config.local.json'),
632
- JSON.stringify({ current_project: 'ghost-project' })
633
- );
638
+ const fixture = createFixture({
639
+ 'config.json': JSON.stringify({}),
640
+ 'config.local.json': JSON.stringify({ current_project: 'ghost-project' }),
641
+ 'PROJECTS.md': '# Projects\n',
642
+ 'REPOS.md': '# Repos\n',
643
+ });
634
644
 
635
645
  try {
636
646
  assert.throws(
@@ -643,12 +653,12 @@ describe('requireProjectRoot', () => {
643
653
  });
644
654
 
645
655
  it('throws INVALID_PROJECT_NAME when current_project contains ..', () => {
646
- const fixture = createTempProject({ version: 2, project: 'real-project' });
647
-
648
- fs.writeFileSync(
649
- path.join(fixture.cwd, '.planning', 'config.local.json'),
650
- JSON.stringify({ current_project: '../etc' })
651
- );
656
+ const fixture = createFixture({
657
+ 'config.json': JSON.stringify({}),
658
+ 'config.local.json': JSON.stringify({ current_project: '../etc' }),
659
+ 'PROJECTS.md': '# Projects\n',
660
+ 'REPOS.md': '# Repos\n',
661
+ });
652
662
 
653
663
  try {
654
664
  assert.throws(
@@ -660,21 +670,10 @@ describe('requireProjectRoot', () => {
660
670
  }
661
671
  });
662
672
 
663
- it('returns .planning for v1 install (backward compatible)', () => {
664
- const fixture = createTempProject({ version: 1 });
665
-
666
- try {
667
- const result = requireProjectRoot(fixture.cwd);
668
- assert.equal(result, '.planning');
669
- } finally {
670
- fixture.cleanup();
671
- }
672
- });
673
-
674
673
  it('resolveProjectPath throws NO_CURRENT_PROJECT_V2 on v2 install without current_project (guard propagates)', () => {
675
674
  const fixture = createFixture({
676
- '.planning/config.json': JSON.stringify({}),
677
- '.planning/PROJECTS.md': '# Projects\n\n| Project | Status |\n',
675
+ 'config.json': JSON.stringify({}),
676
+ 'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
678
677
  });
679
678
 
680
679
  try {
@@ -699,19 +698,18 @@ describe('root layout', () => {
699
698
  });
700
699
 
701
700
  it('loadConfig reads from root-layout config.json', () => {
702
- fixture = createTempProject({ layout: 'root', withConfig: { model_profile: 'quality' } });
701
+ fixture = createTempProject({ withConfig: { model_profile: 'quality' } });
703
702
  const config = loadConfig(fixture.cwd);
704
703
  assert.equal(config.model_profile, 'quality');
705
704
  });
706
705
 
707
706
  it('loadConfig returns defaults when root-layout config.json has no model_profile', () => {
708
- fixture = createTempProject({ layout: 'root' });
707
+ fixture = createTempProject();
709
708
  const config = loadConfig(fixture.cwd);
710
709
  assert.equal(config.model_profile, 'balanced');
711
710
  });
712
711
 
713
- it('getProjectRoot returns . for v1 root-layout', () => {
714
- // Create a root-layout v1 fixture (no v2 markers with valid headers)
712
+ it('getProjectRoot returns . for root-layout without v2 markers', () => {
715
713
  fixture = createFixture({
716
714
  'config.local.json': JSON.stringify({ planningRoot: '.' }),
717
715
  'config.json': JSON.stringify({}),
@@ -725,12 +723,12 @@ describe('root layout', () => {
725
723
  });
726
724
 
727
725
  it('isV2Install detects v2 markers in root-layout', () => {
728
- fixture = createTempProject({ layout: 'root' });
726
+ fixture = createTempProject();
729
727
  const result = isV2Install(fixture.cwd);
730
728
  assert.equal(result, true);
731
729
  });
732
730
 
733
- it('isV2Install returns false for root-layout v1', () => {
731
+ it('isV2Install returns false for root-layout without v2 markers', () => {
734
732
  fixture = createFixture({
735
733
  'config.local.json': JSON.stringify({ planningRoot: '.' }),
736
734
  'config.json': JSON.stringify({}),
@@ -755,7 +753,6 @@ describe('root layout', () => {
755
753
  });
756
754
 
757
755
  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)
759
756
  fixture = createFixture({
760
757
  'PROJECT.md': '# Project\n',
761
758
  'config.json': JSON.stringify({ model_profile: 'budget' }),
@@ -769,13 +766,19 @@ describe('root layout', () => {
769
766
  // ─── getProjectDir Tests ─────────────────────────────────────────────────────
770
767
 
771
768
  describe('getProjectDir', () => {
772
- it('returns absolute path under .planning/projects/<slug> for v2 install', () => {
773
- const fixture = createTempProject({ version: 2, project: 'my-app' });
769
+ it('returns absolute path under projects/<slug> for v2 install', () => {
770
+ const fixture = createFixture({
771
+ 'config.json': JSON.stringify({}),
772
+ 'config.local.json': JSON.stringify({ current_project: 'my-app' }),
773
+ 'PROJECTS.md': '# Projects\n',
774
+ 'REPOS.md': '# Repos\n',
775
+ 'projects/my-app/STATE.md': '# State',
776
+ });
774
777
 
775
778
  try {
776
779
  const result = getProjectDir(fixture.cwd, 'my-app');
777
780
  assert.ok(path.isAbsolute(result), 'Expected absolute path');
778
- assert.equal(result, path.join(fixture.cwd, '.planning', 'projects', 'my-app'));
781
+ assert.equal(result, path.join(fixture.cwd, 'projects', 'my-app'));
779
782
  } finally {
780
783
  fixture.cleanup();
781
784
  }
@@ -783,7 +786,7 @@ describe('getProjectDir', () => {
783
786
 
784
787
  it('returns absolute path for any slug even if directory does not exist', () => {
785
788
  const fixture = createFixture({
786
- '.planning/config.json': JSON.stringify({}),
789
+ 'config.json': JSON.stringify({}),
787
790
  });
788
791
 
789
792
  try {
@@ -808,8 +811,8 @@ describe('config two-file merge', () => {
808
811
 
809
812
  it('loadConfig merges config.json and config.local.json', () => {
810
813
  fixture = createFixture({
811
- '.planning/config.json': JSON.stringify({ model_profile: 'quality' }),
812
- '.planning/config.local.json': JSON.stringify({ current_project: 'my-app' }),
814
+ 'config.json': JSON.stringify({ model_profile: 'quality' }),
815
+ 'config.local.json': JSON.stringify({ current_project: 'my-app' }),
813
816
  });
814
817
  const config = loadConfig(fixture.cwd);
815
818
  assert.equal(config.model_profile, 'quality');
@@ -818,8 +821,8 @@ describe('config two-file merge', () => {
818
821
 
819
822
  it('loadConfig local overrides shared for overlapping keys', () => {
820
823
  fixture = createFixture({
821
- '.planning/config.json': JSON.stringify({ model_profile: 'speed' }),
822
- '.planning/config.local.json': JSON.stringify({ model_profile: 'quality' }),
824
+ 'config.json': JSON.stringify({ model_profile: 'speed' }),
825
+ 'config.local.json': JSON.stringify({ model_profile: 'quality' }),
823
826
  });
824
827
  const config = loadConfig(fixture.cwd);
825
828
  assert.equal(config.model_profile, 'quality');
@@ -827,17 +830,10 @@ describe('config two-file merge', () => {
827
830
 
828
831
  it('loadConfig reads config.json when no config.local.json exists', () => {
829
832
  fixture = createFixture({
830
- '.planning/config.json': JSON.stringify({ model_profile: 'speed' }),
833
+ 'config.json': JSON.stringify({ model_profile: 'speed' }),
831
834
  });
832
835
  const config = loadConfig(fixture.cwd);
833
836
  assert.equal(config.model_profile, 'speed');
834
837
  });
835
838
 
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
- });
843
839
  });