@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
@@ -19,7 +19,7 @@ const STANDARD_DIRS = ['phases', 'research', 'quick', 'debug'];
19
19
  // ─── Project Subfolder Creation ─────────────────────────────────────────────
20
20
 
21
21
  /**
22
- * Create a project subfolder under .planning/projects/<slug>/ with all standard
22
+ * Create a project subfolder under projects/<slug>/ with all standard
23
23
  * planning files and directories.
24
24
  *
25
25
  * @param {string} cwd - Working directory (product root)
@@ -31,7 +31,7 @@ const STANDARD_DIRS = ['phases', 'research', 'quick', 'debug'];
31
31
  */
32
32
  function createProjectSubfolder(cwd, slug, name, options) {
33
33
  const projectDir = getProjectDir(cwd, slug);
34
- // Ensure .planning/projects/ container exists
34
+ // Ensure projects/ container exists
35
35
  fs.mkdirSync(path.dirname(projectDir), { recursive: true });
36
36
 
37
37
  // Don't overwrite existing project
@@ -77,7 +77,7 @@ function createProjectSubfolder(cwd, slug, name, options) {
77
77
  // ─── Project State Reading ──────────────────────────────────────────────────
78
78
 
79
79
  /**
80
- * Read and parse a project's STATE.md at .planning/projects/<slug>/STATE.md
80
+ * Read and parse a project's STATE.md at projects/<slug>/STATE.md
81
81
  * to extract status information.
82
82
  *
83
83
  * Extracts:
@@ -114,7 +114,7 @@ function readProjectState(cwd, slug) {
114
114
  * Scan a project's plan files for <repos>...</repos> tags and extract
115
115
  * all unique repo names referenced.
116
116
  *
117
- * Scans all PLAN.md files under .planning/projects/<slug>/phases/ subdirectories.
117
+ * Scans all PLAN.md files under projects/<slug>/phases/ subdirectories.
118
118
  *
119
119
  * @param {string} cwd - Working directory (product root)
120
120
  * @param {string} slug - Project slug
@@ -166,7 +166,7 @@ function scanProjectReposTags(cwd, slug) {
166
166
  // ─── PROJECTS.md Regeneration ───────────────────────────────────────────────
167
167
 
168
168
  /**
169
- * Regenerate .planning/PROJECTS.md by scanning all project subfolders.
169
+ * Regenerate PROJECTS.md by scanning all project subfolders.
170
170
  *
171
171
  * Reads each project's STATE.md for status/phase/progress, scans plan
172
172
  * <repos> tags for repos touched, and writes the derived PROJECTS.md.
@@ -229,7 +229,7 @@ function regenerateProjectsMd(cwd) {
229
229
 
230
230
  /**
231
231
  * Mark a project as completed by updating its STATE.md at
232
- * .planning/projects/<slug>/STATE.md.
232
+ * projects/<slug>/STATE.md.
233
233
  *
234
234
  * - Updates "Status:" line to "completed"
235
235
  * - Adds "Completed: YYYY-MM-DD" line with today's date
@@ -286,7 +286,7 @@ function completeProject(cwd, slug) {
286
286
 
287
287
  /**
288
288
  * Reactivate a completed project by updating its STATE.md at
289
- * .planning/projects/<slug>/STATE.md.
289
+ * projects/<slug>/STATE.md.
290
290
  *
291
291
  * - Changes "Status: completed" back to "Status: In progress"
292
292
  * - Removes the "Completed: YYYY-MM-DD" line entirely
@@ -332,7 +332,7 @@ function reactivateProject(cwd, slug) {
332
332
  // ─── PROJECTS.md Parsing ────────────────────────────────────────────────────
333
333
 
334
334
  /**
335
- * Parse .planning/PROJECTS.md into structured data.
335
+ * Parse PROJECTS.md into structured data.
336
336
  *
337
337
  * Returns null if the file doesn't exist or doesn't start with '# Projects'.
338
338
  *
@@ -9,26 +9,27 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const os = require('os');
11
11
 
12
- const { createTempDir, cleanupDir, createFixture } = require('./test-helpers.cjs');
12
+ const { createTempDir, cleanupDir, createFixture , initGitRepo } = require('./test-helpers.cjs');
13
+ const { resetPaths } = require('./paths.cjs');
13
14
  const { getProjectRoot } = require('./core.cjs');
14
15
 
15
- // Helper: create .planning/projects/ directory structure
16
+ // Helper: create projects/ directory structure
16
17
  function setupPlanning(cwd) {
17
- fs.mkdirSync(path.join(cwd, '.planning', 'projects'), { recursive: true });
18
+ fs.mkdirSync(path.join(cwd, 'projects'), { recursive: true });
18
19
  }
19
20
 
20
- // Helper: create a project subfolder with STATE.md manually under .planning/projects/<slug>/
21
+ // Helper: create a project subfolder with STATE.md manually under projects/<slug>/
21
22
  function createProjectManually(cwd, slug, stateContent) {
22
- const projDir = path.join(cwd, '.planning', 'projects', slug);
23
+ const projDir = path.join(cwd, 'projects', slug);
23
24
  fs.mkdirSync(projDir, { recursive: true });
24
25
  if (stateContent) {
25
26
  fs.writeFileSync(path.join(projDir, 'STATE.md'), stateContent);
26
27
  }
27
28
  }
28
29
 
29
- // Helper: create plan file with <repos> tags under .planning/projects/<slug>/phases/
30
+ // Helper: create plan file with <repos> tags under projects/<slug>/phases/
30
31
  function createPlanFile(cwd, slug, phaseDir, planName, content) {
31
- const dir = path.join(cwd, '.planning', 'projects', slug, 'phases', phaseDir);
32
+ const dir = path.join(cwd, 'projects', slug, 'phases', phaseDir);
32
33
  fs.mkdirSync(dir, { recursive: true });
33
34
  fs.writeFileSync(path.join(dir, planName), content);
34
35
  }
@@ -50,16 +51,16 @@ describe('createProjectSubfolder', () => {
50
51
  let tmpDir;
51
52
 
52
53
  beforeEach(() => {
53
- tmpDir = createTempDir();
54
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
54
55
  setupPlanning(tmpDir);
55
56
  });
56
57
 
57
- afterEach(() => cleanupDir(tmpDir));
58
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
58
59
 
59
- it('creates project directory at .planning/projects/<slug>/', () => {
60
+ it('creates project directory at projects/<slug>/', () => {
60
61
  const result = createProjectSubfolder(tmpDir, 'auth-overhaul', 'Auth Overhaul');
61
62
  assert.strictEqual(result.created, true);
62
- assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'auth-overhaul')));
63
+ assert.ok(fs.existsSync(path.join(tmpDir, 'projects', 'auth-overhaul')));
63
64
  });
64
65
 
65
66
  it('creates all required subdirectories', () => {
@@ -67,7 +68,7 @@ describe('createProjectSubfolder', () => {
67
68
  const expectedDirs = ['phases', 'research', 'quick', 'debug'];
68
69
  for (const dir of expectedDirs) {
69
70
  assert.ok(
70
- fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'auth-overhaul', dir)),
71
+ fs.existsSync(path.join(tmpDir, 'projects', 'auth-overhaul', dir)),
71
72
  `Missing directory: ${dir}`
72
73
  );
73
74
  }
@@ -78,7 +79,7 @@ describe('createProjectSubfolder', () => {
78
79
  const expectedFiles = ['PROJECT.md', 'REQUIREMENTS.md', 'ROADMAP.md', 'STATE.md'];
79
80
  for (const file of expectedFiles) {
80
81
  assert.ok(
81
- fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'auth-overhaul', file)),
82
+ fs.existsSync(path.join(tmpDir, 'projects', 'auth-overhaul', file)),
82
83
  `Missing file: ${file}`
83
84
  );
84
85
  }
@@ -87,7 +88,7 @@ describe('createProjectSubfolder', () => {
87
88
  it('PROJECT.md contains project name in header', () => {
88
89
  createProjectSubfolder(tmpDir, 'auth-overhaul', 'Auth Overhaul');
89
90
  const content = fs.readFileSync(
90
- path.join(tmpDir, '.planning', 'projects', 'auth-overhaul', 'PROJECT.md'),
91
+ path.join(tmpDir, 'projects', 'auth-overhaul', 'PROJECT.md'),
91
92
  'utf-8'
92
93
  );
93
94
  assert.ok(content.includes('# Project: Auth Overhaul'));
@@ -96,7 +97,7 @@ describe('createProjectSubfolder', () => {
96
97
  it('STATE.md contains initial "Not started" status', () => {
97
98
  createProjectSubfolder(tmpDir, 'auth-overhaul', 'Auth Overhaul');
98
99
  const content = fs.readFileSync(
99
- path.join(tmpDir, '.planning', 'projects', 'auth-overhaul', 'STATE.md'),
100
+ path.join(tmpDir, 'projects', 'auth-overhaul', 'STATE.md'),
100
101
  'utf-8'
101
102
  );
102
103
  assert.ok(content.includes('Not started'));
@@ -118,7 +119,7 @@ describe('createProjectSubfolder', () => {
118
119
  it('handles slug that is already clean', () => {
119
120
  const result = createProjectSubfolder(tmpDir, 'my-cool-project', 'My Cool Project');
120
121
  assert.strictEqual(result.created, true);
121
- assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'my-cool-project')));
122
+ assert.ok(fs.existsSync(path.join(tmpDir, 'projects', 'my-cool-project')));
122
123
  });
123
124
  });
124
125
 
@@ -128,11 +129,11 @@ describe('readProjectState', () => {
128
129
  let tmpDir;
129
130
 
130
131
  beforeEach(() => {
131
- tmpDir = createTempDir();
132
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
132
133
  setupPlanning(tmpDir);
133
134
  });
134
135
 
135
- afterEach(() => cleanupDir(tmpDir));
136
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
136
137
 
137
138
  it('extracts phase from "Phase: 3 of 7 (Name)" format', () => {
138
139
  createProjectManually(tmpDir, 'proj', '# Project State\n\nPhase: 3 of 7 (Project Lifecycle)\nStatus: In progress\n');
@@ -195,11 +196,11 @@ describe('scanProjectReposTags', () => {
195
196
  let tmpDir;
196
197
 
197
198
  beforeEach(() => {
198
- tmpDir = createTempDir();
199
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
199
200
  setupPlanning(tmpDir);
200
201
  });
201
202
 
202
- afterEach(() => cleanupDir(tmpDir));
203
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
203
204
 
204
205
  it('returns empty array when no phases directory exists', () => {
205
206
  createProjectManually(tmpDir, 'proj', '# State\n');
@@ -209,7 +210,7 @@ describe('scanProjectReposTags', () => {
209
210
 
210
211
  it('returns empty array when no plan files exist', () => {
211
212
  createProjectManually(tmpDir, 'proj', '# State\n');
212
- fs.mkdirSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'phases', '01-setup'), {
213
+ fs.mkdirSync(path.join(tmpDir, 'projects', 'proj', 'phases', '01-setup'), {
213
214
  recursive: true,
214
215
  });
215
216
  const repos = scanProjectReposTags(tmpDir, 'proj');
@@ -252,30 +253,30 @@ describe('regenerateProjectsMd', () => {
252
253
  let tmpDir;
253
254
 
254
255
  beforeEach(() => {
255
- tmpDir = createTempDir();
256
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
256
257
  setupPlanning(tmpDir);
257
258
  });
258
259
 
259
- afterEach(() => cleanupDir(tmpDir));
260
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
260
261
 
261
262
  it('writes Active table with correct columns', () => {
262
263
  createProjectManually(tmpDir, 'proj-a', '# Project State\n\nPhase: 1\nStatus: Active\nProgress: [██░░░░░░░░] 20%\n');
263
264
  regenerateProjectsMd(tmpDir);
264
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
265
+ const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
265
266
  assert.ok(content.includes('| Project | Status | Repos Touched | Current Phase |'));
266
267
  });
267
268
 
268
269
  it('writes Completed table with correct columns', () => {
269
270
  createProjectManually(tmpDir, 'proj-a', '# Project State\n\nStatus: Active\n');
270
271
  regenerateProjectsMd(tmpDir);
271
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
272
+ const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
272
273
  assert.ok(content.includes('| Project | Completed | Duration |'));
273
274
  });
274
275
 
275
276
  it('active projects appear in Active table', () => {
276
277
  createProjectManually(tmpDir, 'proj-a', '# Project State\n\nPhase: 2\nStatus: Active\n');
277
278
  regenerateProjectsMd(tmpDir);
278
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
279
+ const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
279
280
  // Find active section and check for project row
280
281
  const activeSection = content.split('## Completed')[0];
281
282
  assert.ok(activeSection.includes('proj-a'));
@@ -284,14 +285,14 @@ describe('regenerateProjectsMd', () => {
284
285
  it('completed projects appear in Completed table', () => {
285
286
  createProjectManually(tmpDir, 'proj-done', '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n');
286
287
  regenerateProjectsMd(tmpDir);
287
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
288
+ const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
288
289
  const completedSection = content.split('## Completed')[1];
289
290
  assert.ok(completedSection.includes('proj-done'));
290
291
  });
291
292
 
292
293
  it('ghost projects emit warning and are omitted', () => {
293
294
  // Create directory but NO STATE.md
294
- fs.mkdirSync(path.join(tmpDir, '.planning', 'projects', 'ghost-project'), { recursive: true });
295
+ fs.mkdirSync(path.join(tmpDir, 'projects', 'ghost-project'), { recursive: true });
295
296
  // getProjectFolders requires STATE.md, so ghost won't even be found
296
297
  // But let's also test a project whose STATE.md is unreadable
297
298
  createProjectManually(tmpDir, 'real-project', '# State\n\nStatus: Active\n');
@@ -309,7 +310,7 @@ describe('regenerateProjectsMd', () => {
309
310
 
310
311
  it('empty project list produces tables with headers but no data rows', () => {
311
312
  regenerateProjectsMd(tmpDir);
312
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
313
+ const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
313
314
  assert.ok(content.includes('# Projects'));
314
315
  assert.ok(content.includes('## Active'));
315
316
  assert.ok(content.includes('## Completed'));
@@ -317,7 +318,7 @@ describe('regenerateProjectsMd', () => {
317
318
 
318
319
  it('starts with # Projects header (v2 marker)', () => {
319
320
  regenerateProjectsMd(tmpDir);
320
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
321
+ const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
321
322
  assert.ok(content.startsWith('# Projects'));
322
323
  });
323
324
  });
@@ -328,23 +329,23 @@ describe('completeProject', () => {
328
329
  let tmpDir;
329
330
 
330
331
  beforeEach(() => {
331
- tmpDir = createTempDir();
332
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
332
333
  setupPlanning(tmpDir);
333
334
  });
334
335
 
335
- afterEach(() => cleanupDir(tmpDir));
336
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
336
337
 
337
338
  it('adds "Status: completed" to STATE.md', () => {
338
339
  createProjectManually(tmpDir, 'proj', '# Project State\n\nPhase: 3\nStatus: Active\n');
339
340
  completeProject(tmpDir, 'proj');
340
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
341
+ const content = fs.readFileSync(path.join(tmpDir, 'projects', 'proj', 'STATE.md'), 'utf-8');
341
342
  assert.ok(content.includes('Status: completed'));
342
343
  });
343
344
 
344
345
  it('adds "Completed: YYYY-MM-DD" to STATE.md', () => {
345
346
  createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: Active\n');
346
347
  const result = completeProject(tmpDir, 'proj');
347
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
348
+ const content = fs.readFileSync(path.join(tmpDir, 'projects', 'proj', 'STATE.md'), 'utf-8');
348
349
  assert.ok(content.includes('Completed:'));
349
350
  assert.ok(/Completed: \d{4}-\d{2}-\d{2}/.test(content));
350
351
  });
@@ -364,7 +365,7 @@ describe('completeProject', () => {
364
365
  });
365
366
 
366
367
  it('returns error if STATE.md does not exist', () => {
367
- fs.mkdirSync(path.join(tmpDir, '.planning', 'projects', 'no-state'), { recursive: true });
368
+ fs.mkdirSync(path.join(tmpDir, 'projects', 'no-state'), { recursive: true });
368
369
  const result = completeProject(tmpDir, 'no-state');
369
370
  assert.strictEqual(result.completed, false);
370
371
  assert.strictEqual(result.error, 'no_state_md');
@@ -384,23 +385,23 @@ describe('reactivateProject', () => {
384
385
  let tmpDir;
385
386
 
386
387
  beforeEach(() => {
387
- tmpDir = createTempDir();
388
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
388
389
  setupPlanning(tmpDir);
389
390
  });
390
391
 
391
- afterEach(() => cleanupDir(tmpDir));
392
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
392
393
 
393
394
  it('changes Status from completed back to In progress', () => {
394
395
  createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: completed\nCompleted: 2026-01-15\n');
395
396
  reactivateProject(tmpDir, 'proj');
396
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
397
+ const content = fs.readFileSync(path.join(tmpDir, 'projects', 'proj', 'STATE.md'), 'utf-8');
397
398
  assert.ok(content.includes('Status: In progress'));
398
399
  });
399
400
 
400
401
  it('removes Completed date line', () => {
401
402
  createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: completed\nCompleted: 2026-01-15\n');
402
403
  reactivateProject(tmpDir, 'proj');
403
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
404
+ const content = fs.readFileSync(path.join(tmpDir, 'projects', 'proj', 'STATE.md'), 'utf-8');
404
405
  assert.ok(!content.includes('Completed:'));
405
406
  });
406
407
 
@@ -417,7 +418,7 @@ describe('reactivateProject', () => {
417
418
  });
418
419
 
419
420
  it('returns error if STATE.md does not exist', () => {
420
- fs.mkdirSync(path.join(tmpDir, '.planning', 'projects', 'no-state'), { recursive: true });
421
+ fs.mkdirSync(path.join(tmpDir, 'projects', 'no-state'), { recursive: true });
421
422
  const result = reactivateProject(tmpDir, 'no-state');
422
423
  assert.strictEqual(result.reactivated, false);
423
424
  assert.strictEqual(result.error, 'no_state_md');
@@ -434,7 +435,7 @@ describe('reactivateProject', () => {
434
435
  const original = '# Project State\n\n## Current Position\n\nPhase: 5 of 7\nStatus: completed\nCompleted: 2026-01-15\n\n## Decisions\n\n- Some important decision\n';
435
436
  createProjectManually(tmpDir, 'proj', original);
436
437
  reactivateProject(tmpDir, 'proj');
437
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
438
+ const content = fs.readFileSync(path.join(tmpDir, 'projects', 'proj', 'STATE.md'), 'utf-8');
438
439
  assert.ok(content.includes('# Project State'));
439
440
  assert.ok(content.includes('## Decisions'));
440
441
  assert.ok(content.includes('Some important decision'));
@@ -445,7 +446,7 @@ describe('reactivateProject', () => {
445
446
  const original = '# Project State\n\nStatus: Active\n';
446
447
  createProjectManually(tmpDir, 'proj', original);
447
448
  reactivateProject(tmpDir, 'proj');
448
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
449
+ const content = fs.readFileSync(path.join(tmpDir, 'projects', 'proj', 'STATE.md'), 'utf-8');
449
450
  assert.strictEqual(content, original);
450
451
  });
451
452
 
@@ -453,7 +454,7 @@ describe('reactivateProject', () => {
453
454
  createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: Completed\nCompleted: 2026-01-15\n');
454
455
  const result = reactivateProject(tmpDir, 'proj');
455
456
  assert.strictEqual(result.reactivated, true);
456
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
457
+ const content = fs.readFileSync(path.join(tmpDir, 'projects', 'proj', 'STATE.md'), 'utf-8');
457
458
  assert.ok(content.includes('Status: In progress'));
458
459
  });
459
460
  });
@@ -464,11 +465,11 @@ describe('parseProjectsMd', () => {
464
465
  let tmpDir;
465
466
 
466
467
  beforeEach(() => {
467
- tmpDir = createTempDir();
468
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
468
469
  setupPlanning(tmpDir);
469
470
  });
470
471
 
471
- afterEach(() => cleanupDir(tmpDir));
472
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
472
473
 
473
474
  it('parses Active table rows into structured objects', () => {
474
475
  const content = `# Projects
@@ -484,7 +485,7 @@ describe('parseProjectsMd', () => {
484
485
  | Project | Completed | Duration |
485
486
  |---------|-----------|----------|
486
487
  `;
487
- fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), content);
488
+ fs.writeFileSync(path.join(tmpDir, 'PROJECTS.md'), content);
488
489
  const result = parseProjectsMd(tmpDir);
489
490
  assert.strictEqual(result.active.length, 1);
490
491
  assert.strictEqual(result.active[0].name, 'auth-overhaul');
@@ -507,7 +508,7 @@ describe('parseProjectsMd', () => {
507
508
  |---------|-----------|----------|
508
509
  | old-project | 2026-01-15 | 30 days |
509
510
  `;
510
- fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), content);
511
+ fs.writeFileSync(path.join(tmpDir, 'PROJECTS.md'), content);
511
512
  const result = parseProjectsMd(tmpDir);
512
513
  assert.strictEqual(result.completed.length, 1);
513
514
  assert.strictEqual(result.completed[0].name, 'old-project');
@@ -521,7 +522,7 @@ describe('parseProjectsMd', () => {
521
522
  });
522
523
 
523
524
  it('returns null if file does not start with # Projects', () => {
524
- fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), '# Not Projects\n');
525
+ fs.writeFileSync(path.join(tmpDir, 'PROJECTS.md'), '# Not Projects\n');
525
526
  const result = parseProjectsMd(tmpDir);
526
527
  assert.strictEqual(result, null);
527
528
  });
@@ -539,7 +540,7 @@ describe('parseProjectsMd', () => {
539
540
  | Project | Completed | Duration |
540
541
  |---------|-----------|----------|
541
542
  `;
542
- fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), content);
543
+ fs.writeFileSync(path.join(tmpDir, 'PROJECTS.md'), content);
543
544
  const result = parseProjectsMd(tmpDir);
544
545
  assert.strictEqual(result.active.length, 0);
545
546
  assert.strictEqual(result.completed.length, 0);
@@ -552,18 +553,18 @@ describe('createProjectSubfolder edge cases', () => {
552
553
  let tmpDir;
553
554
 
554
555
  beforeEach(() => {
555
- tmpDir = createTempDir();
556
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
556
557
  setupPlanning(tmpDir);
557
558
  });
558
559
 
559
- afterEach(() => cleanupDir(tmpDir));
560
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
560
561
 
561
562
  it('includes initial_repos in PROJECT.md when provided', () => {
562
563
  createProjectSubfolder(tmpDir, 'my-proj', 'My Project', {
563
564
  initial_repos: 'web-app, server, shared-lib',
564
565
  });
565
566
  const content = fs.readFileSync(
566
- path.join(tmpDir, '.planning', 'projects', 'my-proj', 'PROJECT.md'),
567
+ path.join(tmpDir, 'projects', 'my-proj', 'PROJECT.md'),
567
568
  'utf-8'
568
569
  );
569
570
  assert.ok(content.includes('## Initial Repos'));
@@ -573,7 +574,7 @@ describe('createProjectSubfolder edge cases', () => {
573
574
  it('STATE.md has 0% progress bar initially', () => {
574
575
  createProjectSubfolder(tmpDir, 'my-proj', 'My Project');
575
576
  const content = fs.readFileSync(
576
- path.join(tmpDir, '.planning', 'projects', 'my-proj', 'STATE.md'),
577
+ path.join(tmpDir, 'projects', 'my-proj', 'STATE.md'),
577
578
  'utf-8'
578
579
  );
579
580
  assert.ok(content.includes('0%'));
@@ -584,11 +585,11 @@ describe('readProjectState edge cases', () => {
584
585
  let tmpDir;
585
586
 
586
587
  beforeEach(() => {
587
- tmpDir = createTempDir();
588
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
588
589
  setupPlanning(tmpDir);
589
590
  });
590
591
 
591
- afterEach(() => cleanupDir(tmpDir));
592
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
592
593
 
593
594
  it('extracts multi-part Phase field', () => {
594
595
  createProjectManually(
@@ -617,11 +618,11 @@ describe('scanProjectReposTags edge cases', () => {
617
618
  let tmpDir;
618
619
 
619
620
  beforeEach(() => {
620
- tmpDir = createTempDir();
621
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
621
622
  setupPlanning(tmpDir);
622
623
  });
623
624
 
624
- afterEach(() => cleanupDir(tmpDir));
625
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
625
626
 
626
627
  it('handles multiple <repos> tags in one plan file', () => {
627
628
  createProjectManually(tmpDir, 'proj', '# State\n');
@@ -654,18 +655,18 @@ describe('regenerateProjectsMd edge cases', () => {
654
655
  let tmpDir;
655
656
 
656
657
  beforeEach(() => {
657
- tmpDir = createTempDir();
658
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
658
659
  setupPlanning(tmpDir);
659
660
  });
660
661
 
661
- afterEach(() => cleanupDir(tmpDir));
662
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
662
663
 
663
664
  it('handles mix of active and completed projects', () => {
664
665
  createProjectManually(tmpDir, 'active-proj', '# State\n\nPhase: 2\nStatus: In progress\n');
665
666
  createProjectManually(tmpDir, 'done-proj', '# State\n\nStatus: completed\nCompleted: 2026-01-15\n');
666
667
  const result = regenerateProjectsMd(tmpDir);
667
668
  assert.strictEqual(result.projects.length, 2);
668
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
669
+ const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
669
670
  const activeSection = content.split('## Completed')[0];
670
671
  const completedSection = content.split('## Completed')[1];
671
672
  assert.ok(activeSection.includes('active-proj'));
@@ -684,7 +685,7 @@ describe('regenerateProjectsMd edge cases', () => {
684
685
  createProjectManually(tmpDir, 'proj', '# State\n\nStatus: Active\nPhase: 1\n');
685
686
  createPlanFile(tmpDir, 'proj', '01-setup', '01-01-PLAN.md', '<repos>web-app, server</repos>\n');
686
687
  regenerateProjectsMd(tmpDir);
687
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
688
+ const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
688
689
  assert.ok(content.includes('server, web-app'));
689
690
  });
690
691
  });
@@ -693,17 +694,17 @@ describe('completeProject edge cases', () => {
693
694
  let tmpDir;
694
695
 
695
696
  beforeEach(() => {
696
- tmpDir = createTempDir();
697
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
697
698
  setupPlanning(tmpDir);
698
699
  });
699
700
 
700
- afterEach(() => cleanupDir(tmpDir));
701
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
701
702
 
702
703
  it('preserves existing STATE.md content', () => {
703
704
  const original = '# Project State\n\n## Current Position\n\nPhase: 5 of 7\nStatus: In progress\nProgress: [████████░░] 80%\n\n## Decisions\n\n- Some important decision\n';
704
705
  createProjectManually(tmpDir, 'proj', original);
705
706
  completeProject(tmpDir, 'proj');
706
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
707
+ const content = fs.readFileSync(path.join(tmpDir, 'projects', 'proj', 'STATE.md'), 'utf-8');
707
708
  assert.ok(content.includes('# Project State'));
708
709
  assert.ok(content.includes('## Decisions'));
709
710
  assert.ok(content.includes('Some important decision'));
@@ -713,7 +714,7 @@ describe('completeProject edge cases', () => {
713
714
  it('overwrites existing Status line instead of adding duplicate', () => {
714
715
  createProjectManually(tmpDir, 'proj', '# State\n\nStatus: Active\n');
715
716
  completeProject(tmpDir, 'proj');
716
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
717
+ const content = fs.readFileSync(path.join(tmpDir, 'projects', 'proj', 'STATE.md'), 'utf-8');
717
718
  const statusCount = (content.match(/Status:/g) || []).length;
718
719
  assert.strictEqual(statusCount, 1);
719
720
  });
@@ -722,7 +723,7 @@ describe('completeProject edge cases', () => {
722
723
  const original = '# Project State\n\nStatus: completed\nCompleted: 2026-01-15\n';
723
724
  createProjectManually(tmpDir, 'proj', original);
724
725
  completeProject(tmpDir, 'proj');
725
- const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
726
+ const content = fs.readFileSync(path.join(tmpDir, 'projects', 'proj', 'STATE.md'), 'utf-8');
726
727
  assert.strictEqual(content, original);
727
728
  });
728
729
 
@@ -738,11 +739,11 @@ describe('roundtrip integration', () => {
738
739
  let tmpDir;
739
740
 
740
741
  beforeEach(() => {
741
- tmpDir = createTempDir();
742
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
742
743
  setupPlanning(tmpDir);
743
744
  });
744
745
 
745
- afterEach(() => cleanupDir(tmpDir));
746
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
746
747
 
747
748
  it('create -> read -> regenerate -> parse produces consistent data', () => {
748
749
  // Create
@@ -822,11 +823,11 @@ describe('cmdProjectsSwitch guards', () => {
822
823
  let tmpDir;
823
824
 
824
825
  beforeEach(() => {
825
- tmpDir = createTempDir();
826
+ tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
826
827
  setupPlanning(tmpDir);
827
828
  });
828
829
 
829
- afterEach(() => cleanupDir(tmpDir));
830
+ afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
830
831
 
831
832
  it('readProjectState detects completed status for cmdProjectsSwitch guard', () => {
832
833
  createProjectManually(tmpDir, 'completed-proj', '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n');
@@ -853,10 +854,10 @@ describe('cmdProjectsSwitch guards', () => {
853
854
  it('getProjectRoot throws PROJECT_COMPLETED for completed project (integration)', () => {
854
855
  // Set up a v2 fixture with completed project as current_project
855
856
  const fixture = createFixture({
856
- '.planning/config.json': JSON.stringify({ current_project: 'finished-proj' }),
857
- '.planning/PROJECTS.md': '# Projects\n\n## Active\n\n| Project | Status | Repos Touched | Current Phase |\n|---------|--------|---------------|---------------|\n\n## Completed\n\n| Project | Completed | Duration |\n|---------|-----------|----------|\n| finished-proj | 2026-02-20 | |\n',
858
- '.planning/REPOS.md': '# Repos\n\n| Name | Path |\n',
859
- '.planning/projects/finished-proj/STATE.md': '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n',
857
+ 'config.json': JSON.stringify({ current_project: 'finished-proj' }),
858
+ 'PROJECTS.md': '# Projects\n\n## Active\n\n| Project | Status | Repos Touched | Current Phase |\n|---------|--------|---------------|---------------|\n\n## Completed\n\n| Project | Completed | Duration |\n|---------|-----------|----------|\n| finished-proj | 2026-02-20 | |\n',
859
+ 'REPOS.md': '# Repos\n\n| Name | Path |\n',
860
+ 'projects/finished-proj/STATE.md': '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n',
860
861
  });
861
862
 
862
863
  try {