@ktpartners/dgs-platform 2.7.5 → 2.9.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 (57) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +15 -12
  3. package/agents/dgs-executor.md +0 -52
  4. package/deliver-great-systems/bin/dgs-tools.cjs +66 -10
  5. package/deliver-great-systems/bin/lib/commands.cjs +1 -8
  6. package/deliver-great-systems/bin/lib/config.cjs +9 -90
  7. package/deliver-great-systems/bin/lib/context.cjs +2 -2
  8. package/deliver-great-systems/bin/lib/context.test.cjs +100 -100
  9. package/deliver-great-systems/bin/lib/core.cjs +17 -57
  10. package/deliver-great-systems/bin/lib/core.test.cjs +166 -170
  11. package/deliver-great-systems/bin/lib/docs.cjs +3 -3
  12. package/deliver-great-systems/bin/lib/docs.test.cjs +14 -7
  13. package/deliver-great-systems/bin/lib/execution.cjs +2 -2
  14. package/deliver-great-systems/bin/lib/execution.test.cjs +65 -67
  15. package/deliver-great-systems/bin/lib/ideas.cjs +4 -4
  16. package/deliver-great-systems/bin/lib/ideas.test.cjs +45 -44
  17. package/deliver-great-systems/bin/lib/init.cjs +9 -4
  18. package/deliver-great-systems/bin/lib/init.test.cjs +242 -175
  19. package/deliver-great-systems/bin/lib/jobs.cjs +1 -1
  20. package/deliver-great-systems/bin/lib/jobs.test.cjs +203 -202
  21. package/deliver-great-systems/bin/lib/migration.cjs +256 -281
  22. package/deliver-great-systems/bin/lib/migration.test.cjs +385 -440
  23. package/deliver-great-systems/bin/lib/milestone.cjs +1 -1
  24. package/deliver-great-systems/bin/lib/overlap.cjs +4 -4
  25. package/deliver-great-systems/bin/lib/overlap.test.cjs +45 -44
  26. package/deliver-great-systems/bin/lib/path-audit.test.cjs +16 -22
  27. package/deliver-great-systems/bin/lib/paths.cjs +60 -59
  28. package/deliver-great-systems/bin/lib/paths.test.cjs +192 -225
  29. package/deliver-great-systems/bin/lib/phase.cjs +5 -4
  30. package/deliver-great-systems/bin/lib/projects.cjs +8 -8
  31. package/deliver-great-systems/bin/lib/projects.test.cjs +75 -74
  32. package/deliver-great-systems/bin/lib/repos.cjs +94 -230
  33. package/deliver-great-systems/bin/lib/repos.test.cjs +84 -75
  34. package/deliver-great-systems/bin/lib/search.cjs +4 -4
  35. package/deliver-great-systems/bin/lib/specs.cjs +2 -2
  36. package/deliver-great-systems/bin/lib/sync.cjs +1 -1
  37. package/deliver-great-systems/bin/lib/template.cjs +3 -3
  38. package/deliver-great-systems/bin/lib/test-helpers.cjs +59 -162
  39. package/deliver-great-systems/bin/lib/verify.cjs +3 -3
  40. package/deliver-great-systems/references/planning-config.md +7 -8
  41. package/deliver-great-systems/workflows/add-tests.md +1 -1
  42. package/deliver-great-systems/workflows/approve-spec.md +1 -11
  43. package/deliver-great-systems/workflows/complete-milestone.md +2 -2
  44. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  45. package/deliver-great-systems/workflows/create-milestone-job.md +2 -2
  46. package/deliver-great-systems/workflows/discuss-phase.md +2 -2
  47. package/deliver-great-systems/workflows/execute-phase.md +63 -4
  48. package/deliver-great-systems/workflows/execute-plan.md +0 -51
  49. package/deliver-great-systems/workflows/find-related-ideas.md +1 -1
  50. package/deliver-great-systems/workflows/help.md +55 -84
  51. package/deliver-great-systems/workflows/init-product.md +14 -451
  52. package/deliver-great-systems/workflows/map-codebase.md +109 -0
  53. package/deliver-great-systems/workflows/new-milestone.md +16 -6
  54. package/deliver-great-systems/workflows/new-project.md +22 -681
  55. package/deliver-great-systems/workflows/quick.md +2 -2
  56. package/deliver-great-systems/workflows/run-job.md +56 -0
  57. package/package.json +1 -1
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Tests for paths.cjs Planning root detection and PATHS singleton
2
+ * Tests for paths.cjs -- Planning root detection and PATHS singleton
3
3
  *
4
4
  * Uses Node.js built-in test runner (node:test) and assert (node:assert/strict).
5
- * Each test creates an isolated temp directory fixture and cleans up after.
5
+ * Each test creates an isolated temp git repo fixture and cleans up after.
6
6
  *
7
- * Covers PATH-01 through PATH-05 requirements.
7
+ * Covers PATH-01 through PATH-06, OPT-01 requirements.
8
8
  */
9
9
 
10
10
  const { describe, it, afterEach } = require('node:test');
@@ -12,22 +12,42 @@ const assert = require('node:assert/strict');
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const os = require('os');
15
+ const { execSync } = require('child_process');
15
16
 
16
- const { getPlanningRoot, getPaths, initPaths, resetPaths, PROJECTS_DIR } = require('./paths.cjs');
17
+ const { getPlanningRoot, getPaths, initPaths, resetPaths, isV2Install, PROJECTS_DIR } = require('./paths.cjs');
18
+ const { rejectV1Install } = require('./migration.cjs');
17
19
 
18
20
  // ─── Helpers ────────────────────────────────────────────────────────────────
19
21
 
22
+ const ISOLATED_ENV = Object.assign({}, process.env, {
23
+ GIT_CONFIG_GLOBAL: '/dev/null',
24
+ GIT_CONFIG_NOSYSTEM: '1',
25
+ });
26
+
27
+ function makeGitTempDir() {
28
+ // Use realpathSync to resolve macOS /var -> /private/var symlink so
29
+ // comparisons with git rev-parse --show-toplevel output match.
30
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'dgs-paths-test-')));
31
+ execSync('git init', { cwd: dir, stdio: 'pipe', env: ISOLATED_ENV });
32
+ execSync('git config user.name "Test"', { cwd: dir, stdio: 'pipe', env: ISOLATED_ENV });
33
+ execSync('git config user.email "test@test.com"', { cwd: dir, stdio: 'pipe', env: ISOLATED_ENV });
34
+ // Create initial commit so git rev-parse works
35
+ fs.writeFileSync(path.join(dir, '.gitkeep'), '');
36
+ execSync('git add .gitkeep && git commit -m "init"', { cwd: dir, stdio: 'pipe', env: ISOLATED_ENV });
37
+ return dir;
38
+ }
39
+
20
40
  function makeTempDir() {
21
- return fs.mkdtempSync(path.join(os.tmpdir(), 'dgs-paths-test-'));
41
+ return fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'dgs-paths-test-')));
22
42
  }
23
43
 
24
44
  function removeTempDir(dir) {
25
45
  fs.rmSync(dir, { recursive: true, force: true });
26
46
  }
27
47
 
28
- // ─── 1. Detection cascade .planning/ layout (PATH-01, PATH-02) ────────────
48
+ // ─── 1. getPlanningRoot -- git rev-parse (PATH-01) ──────────────────────────
29
49
 
30
- describe('detection cascade .planning/ layout', () => {
50
+ describe('getPlanningRoot -- git rev-parse', () => {
31
51
  let cwd;
32
52
 
33
53
  afterEach(() => {
@@ -36,38 +56,30 @@ describe('detection cascade — .planning/ layout', () => {
36
56
  cwd = null;
37
57
  });
38
58
 
39
- it('detects .planning/ layout from .planning/dgs.config.json', () => {
40
- cwd = makeTempDir();
41
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
42
- fs.writeFileSync(path.join(cwd, '.planning', 'dgs.config.json'), '{}');
43
-
59
+ it('returns git repo root for a git-initialized directory', () => {
60
+ cwd = makeGitTempDir();
44
61
  const root = getPlanningRoot(cwd);
45
- assert.equal(root, path.join(cwd, '.planning'));
62
+ assert.equal(root, cwd);
46
63
  });
47
64
 
48
- it('detects .planning/ layout from .planning/config.json', () => {
49
- cwd = makeTempDir();
50
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
51
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
52
-
53
- const root = getPlanningRoot(cwd);
54
- assert.equal(root, path.join(cwd, '.planning'));
65
+ it('returns git repo root from a subdirectory', () => {
66
+ cwd = makeGitTempDir();
67
+ const sub = path.join(cwd, 'a', 'b', 'c');
68
+ fs.mkdirSync(sub, { recursive: true });
69
+ const root = getPlanningRoot(sub);
70
+ assert.equal(root, cwd);
55
71
  });
56
72
 
57
- it('detects .planning/ layout when both config files exist', () => {
58
- cwd = makeTempDir();
59
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
60
- fs.writeFileSync(path.join(cwd, '.planning', 'dgs.config.json'), '{}');
61
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
62
-
73
+ it('returns absolute path', () => {
74
+ cwd = makeGitTempDir();
63
75
  const root = getPlanningRoot(cwd);
64
- assert.equal(root, path.join(cwd, '.planning'));
76
+ assert.ok(path.isAbsolute(root), 'Expected absolute path');
65
77
  });
66
78
  });
67
79
 
68
- // ─── 2. Detection cascade root layout (PATH-01, PATH-02) ──────────────────
80
+ // ─── 2. getPlanningRoot -- per-process caching (OPT-01) ─────────────────────
69
81
 
70
- describe('detection cascade root layout', () => {
82
+ describe('getPlanningRoot -- per-process cache', () => {
71
83
  let cwd;
72
84
 
73
85
  afterEach(() => {
@@ -76,59 +88,56 @@ describe('detection cascade — root layout', () => {
76
88
  cwd = null;
77
89
  });
78
90
 
79
- it('detects root layout from dgs.config.json with planningRoot "."', () => {
80
- cwd = makeTempDir();
81
- fs.writeFileSync(
82
- path.join(cwd, 'dgs.config.json'),
83
- JSON.stringify({ planningRoot: '.' })
84
- );
85
-
86
- const root = getPlanningRoot(cwd);
87
- assert.equal(root, cwd);
91
+ it('returns cached value on subsequent calls', () => {
92
+ cwd = makeGitTempDir();
93
+ const first = getPlanningRoot(cwd);
94
+ // Second call with different cwd should still return cached value
95
+ const second = getPlanningRoot('/some/other/path');
96
+ assert.equal(first, second, 'Expected cached value to be returned');
88
97
  });
89
98
 
90
- it('returns .planning/ when root dgs.config.json has no planningRoot field', () => {
91
- cwd = makeTempDir();
92
- fs.writeFileSync(
93
- path.join(cwd, 'dgs.config.json'),
94
- JSON.stringify({ mode: 'interactive' })
95
- );
96
-
97
- const root = getPlanningRoot(cwd);
98
- assert.equal(root, path.join(cwd, '.planning'));
99
- });
100
-
101
- it('falls through when root dgs.config.json has malformed JSON', () => {
102
- cwd = makeTempDir();
103
- fs.writeFileSync(path.join(cwd, 'dgs.config.json'), '{not valid json!!!');
104
-
105
- // Should NOT crash — falls through to default
106
- const root = getPlanningRoot(cwd);
107
- // Malformed config falls through; no PROJECT.md => default .planning/
108
- assert.equal(root, path.join(cwd, '.planning'));
109
- });
110
-
111
- it('auto-detects root layout when PROJECT.md exists at root with no .planning/ dir', () => {
112
- cwd = makeTempDir();
113
- fs.writeFileSync(path.join(cwd, 'PROJECT.md'), '# Project: Test');
114
-
115
- const root = getPlanningRoot(cwd);
116
- assert.equal(root, cwd);
99
+ it('resetPaths clears the cache so next call re-detects', () => {
100
+ cwd = makeGitTempDir();
101
+ const first = getPlanningRoot(cwd);
102
+ resetPaths();
103
+ // After reset, calling with same cwd should re-detect (same result, but not from cache)
104
+ const second = getPlanningRoot(cwd);
105
+ assert.equal(first, second, 'Expected same root after cache clear + re-detect');
117
106
  });
107
+ });
118
108
 
119
- it('returns .planning/ when PROJECT.md exists at root AND .planning/ directory exists', () => {
120
- cwd = makeTempDir();
121
- fs.writeFileSync(path.join(cwd, 'PROJECT.md'), '# Project: Test');
122
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
109
+ // ─── 3. getPlanningRoot -- non-git directory ────────────────────────────────
123
110
 
124
- const root = getPlanningRoot(cwd);
125
- assert.equal(root, path.join(cwd, '.planning'));
111
+ describe('getPlanningRoot -- non-git directory', () => {
112
+ it('exits with error when not in a git repo (subprocess)', () => {
113
+ const dir = makeTempDir();
114
+ try {
115
+ const script = `
116
+ const { getPlanningRoot } = require(${JSON.stringify(path.join(__dirname, 'paths.cjs'))});
117
+ getPlanningRoot(${JSON.stringify(dir)});
118
+ `;
119
+ const tmpScript = path.join(dir, 'test-no-git.cjs');
120
+ fs.writeFileSync(tmpScript, script);
121
+ try {
122
+ execSync(`node ${JSON.stringify(tmpScript)}`, {
123
+ encoding: 'utf-8',
124
+ stdio: ['pipe', 'pipe', 'pipe'],
125
+ env: ISOLATED_ENV,
126
+ });
127
+ assert.fail('Expected process to exit with error');
128
+ } catch (err) {
129
+ assert.ok(err.status === 1, `Expected exit code 1, got ${err.status}`);
130
+ assert.ok(err.stderr.includes('DGS requires a git repository'), 'Expected git repo error message');
131
+ }
132
+ } finally {
133
+ removeTempDir(dir);
134
+ }
126
135
  });
127
136
  });
128
137
 
129
- // ─── 3. Detection cascade — default (PATH-02) ───────────────────────────────
138
+ // ─── 4. rejectV1Install ─────────────────────────────────────────────────────
130
139
 
131
- describe('detection cascade — default', () => {
140
+ describe('rejectV1Install', () => {
132
141
  let cwd;
133
142
 
134
143
  afterEach(() => {
@@ -137,17 +146,60 @@ describe('detection cascade — default', () => {
137
146
  cwd = null;
138
147
  });
139
148
 
140
- it('defaults to .planning/ when no signals are found', () => {
141
- cwd = makeTempDir();
149
+ it('does nothing when no .planning/PROJECT.md exists', () => {
150
+ cwd = makeGitTempDir();
151
+ // No error, no exit
152
+ rejectV1Install(cwd);
153
+ });
142
154
 
143
- const root = getPlanningRoot(cwd);
144
- assert.equal(root, path.join(cwd, '.planning'));
155
+ it('does nothing when .planning/PROJECT.md exists with PROJECTS.md at root', () => {
156
+ cwd = makeGitTempDir();
157
+ fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
158
+ fs.writeFileSync(path.join(cwd, '.planning', 'PROJECT.md'), '# Project');
159
+ fs.writeFileSync(path.join(cwd, 'PROJECTS.md'), '# Projects');
160
+ rejectV1Install(cwd);
161
+ });
162
+
163
+ it('does nothing when .planning/PROJECT.md exists with REPOS.md at root', () => {
164
+ cwd = makeGitTempDir();
165
+ fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
166
+ fs.writeFileSync(path.join(cwd, '.planning', 'PROJECT.md'), '# Project');
167
+ fs.writeFileSync(path.join(cwd, 'REPOS.md'), '# Repos');
168
+ rejectV1Install(cwd);
169
+ });
170
+
171
+ it('exits with code 1 and writes V1_UNSUPPORTED.md for V1 layout (subprocess)', () => {
172
+ cwd = makeGitTempDir();
173
+ fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
174
+ fs.writeFileSync(path.join(cwd, '.planning', 'PROJECT.md'), '# Project');
175
+
176
+ const script = `
177
+ const { rejectV1Install } = require(${JSON.stringify(path.join(__dirname, 'migration.cjs'))});
178
+ rejectV1Install(${JSON.stringify(cwd)});
179
+ process.stdout.write('should-not-reach');
180
+ `;
181
+ const tmpScript = path.join(cwd, 'test-v1-reject.cjs');
182
+ fs.writeFileSync(tmpScript, script);
183
+ try {
184
+ execSync(`node ${JSON.stringify(tmpScript)}`, {
185
+ encoding: 'utf-8',
186
+ stdio: ['pipe', 'pipe', 'pipe'],
187
+ env: ISOLATED_ENV,
188
+ });
189
+ assert.fail('Expected process to exit with error');
190
+ } catch (err) {
191
+ assert.equal(err.status, 1, 'Expected exit code 1');
192
+ assert.ok(err.stderr.includes('V1 single-repo layout is no longer supported'), 'Expected V1 rejection error');
193
+ assert.ok(!err.stdout.includes('should-not-reach'), 'Process should have exited before reaching stdout write');
194
+ }
195
+ // V1_UNSUPPORTED.md should have been written
196
+ assert.ok(fs.existsSync(path.join(cwd, 'V1_UNSUPPORTED.md')), 'Expected V1_UNSUPPORTED.md to be written');
145
197
  });
146
198
  });
147
199
 
148
- // ─── 4. Detection cascade — priority (PATH-02) ──────────────────────────────
200
+ // ─── 5. isV2Install ─────────────────────────────────────────────────────────
149
201
 
150
- describe('detection cascade — priority', () => {
202
+ describe('isV2Install', () => {
151
203
  let cwd;
152
204
 
153
205
  afterEach(() => {
@@ -156,23 +208,31 @@ describe('detection cascade — priority', () => {
156
208
  cwd = null;
157
209
  });
158
210
 
159
- it('.planning/config.json takes precedence over root dgs.config.json with planningRoot "."', () => {
160
- cwd = makeTempDir();
161
- // .planning/ layout signal
162
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
163
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
164
- // Root layout signal
165
- fs.writeFileSync(
166
- path.join(cwd, 'dgs.config.json'),
167
- JSON.stringify({ planningRoot: '.' })
168
- );
211
+ it('returns true when PROJECTS.md with correct header exists at repo root', () => {
212
+ cwd = makeGitTempDir();
213
+ fs.writeFileSync(path.join(cwd, 'PROJECTS.md'), '# Projects\n\n| Project | Status |');
214
+ assert.equal(isV2Install(cwd), true);
215
+ });
169
216
 
170
- const root = getPlanningRoot(cwd);
171
- assert.equal(root, path.join(cwd, '.planning'));
217
+ it('returns true when REPOS.md with correct header exists at repo root', () => {
218
+ cwd = makeGitTempDir();
219
+ fs.writeFileSync(path.join(cwd, 'REPOS.md'), '# Repos\n\n| Name | Path |');
220
+ assert.equal(isV2Install(cwd), true);
221
+ });
222
+
223
+ it('returns false when neither marker file exists', () => {
224
+ cwd = makeGitTempDir();
225
+ assert.equal(isV2Install(cwd), false);
226
+ });
227
+
228
+ it('returns false when PROJECTS.md has wrong header', () => {
229
+ cwd = makeGitTempDir();
230
+ fs.writeFileSync(path.join(cwd, 'PROJECTS.md'), 'Not a projects file');
231
+ assert.equal(isV2Install(cwd), false);
172
232
  });
173
233
  });
174
234
 
175
- // ─── 5. PATHS object shape (PATH-03) ────────────────────────────────────────
235
+ // ─── 6. PATHS object shape (PATH-03) ────────────────────────────────────────
176
236
 
177
237
  describe('PATHS object shape', () => {
178
238
  let cwd;
@@ -185,49 +245,26 @@ describe('PATHS object shape', () => {
185
245
 
186
246
  const EXPECTED_KEYS = [
187
247
  'ROOT', 'PHASES', 'IDEAS', 'SPECS', 'JOBS', 'DOCS',
188
- 'CODEBASE', 'MILESTONES', 'CONFIG', 'CONFIG_LOCAL', 'CONFIG_LEGACY',
189
- 'QUICK', 'TODOS', 'RESEARCH', 'DEBUG', 'ARCHIVE', 'PROJECTS', 'LAYOUT',
248
+ 'CODEBASE', 'MILESTONES', 'CONFIG', 'CONFIG_LOCAL',
249
+ 'QUICK', 'TODOS', 'RESEARCH', 'DEBUG', 'ARCHIVE', 'PROJECTS',
190
250
  ];
191
251
 
192
- it('returns object with all 18 expected keys', () => {
193
- cwd = makeTempDir();
194
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
195
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
196
-
252
+ it('returns object with all 16 expected keys', () => {
253
+ cwd = makeGitTempDir();
197
254
  const paths = getPaths(cwd);
198
255
  assert.deepEqual(Object.keys(paths).sort(), EXPECTED_KEYS.slice().sort());
199
256
  });
200
257
 
201
- it('for .planning/ layout: ROOT ends with .planning and LAYOUT is dotplanning', () => {
202
- cwd = makeTempDir();
203
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
204
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
205
-
206
- const paths = getPaths(cwd);
207
- assert.ok(paths.ROOT.endsWith('.planning'));
208
- assert.equal(paths.LAYOUT, 'dotplanning');
209
- });
210
-
211
- it('for root layout: ROOT equals cwd and LAYOUT is root', () => {
212
- cwd = makeTempDir();
213
- fs.writeFileSync(
214
- path.join(cwd, 'dgs.config.json'),
215
- JSON.stringify({ planningRoot: '.' })
216
- );
217
-
258
+ it('ROOT equals git repo root', () => {
259
+ cwd = makeGitTempDir();
218
260
  const paths = getPaths(cwd);
219
261
  assert.equal(paths.ROOT, cwd);
220
- assert.equal(paths.LAYOUT, 'root');
221
262
  });
222
263
 
223
- it('all path values (except LAYOUT) are absolute', () => {
224
- cwd = makeTempDir();
225
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
226
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
227
-
264
+ it('all path values are absolute', () => {
265
+ cwd = makeGitTempDir();
228
266
  const paths = getPaths(cwd);
229
267
  for (const [key, value] of Object.entries(paths)) {
230
- if (key === 'LAYOUT') continue;
231
268
  assert.ok(
232
269
  path.isAbsolute(value),
233
270
  `Expected ${key} to be absolute, got: ${value}`
@@ -236,38 +273,19 @@ describe('PATHS object shape', () => {
236
273
  });
237
274
 
238
275
  it('CONFIG ends with config.json', () => {
239
- cwd = makeTempDir();
240
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
241
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
242
-
276
+ cwd = makeGitTempDir();
243
277
  const paths = getPaths(cwd);
244
278
  assert.ok(paths.CONFIG.endsWith('config.json'));
245
- assert.ok(!paths.CONFIG.endsWith('dgs.config.json'));
246
279
  });
247
280
 
248
281
  it('CONFIG_LOCAL ends with config.local.json', () => {
249
- cwd = makeTempDir();
250
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
251
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
252
-
282
+ cwd = makeGitTempDir();
253
283
  const paths = getPaths(cwd);
254
284
  assert.ok(paths.CONFIG_LOCAL.endsWith('config.local.json'));
255
285
  });
256
286
 
257
- it('CONFIG_LEGACY ends with dgs.config.json', () => {
258
- cwd = makeTempDir();
259
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
260
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
261
-
262
- const paths = getPaths(cwd);
263
- assert.ok(paths.CONFIG_LEGACY.endsWith('dgs.config.json'));
264
- });
265
-
266
287
  it('subdirectory paths are derived from ROOT', () => {
267
- cwd = makeTempDir();
268
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
269
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
270
-
288
+ cwd = makeGitTempDir();
271
289
  const paths = getPaths(cwd);
272
290
  assert.equal(paths.PHASES, path.join(paths.ROOT, 'phases'));
273
291
  assert.equal(paths.IDEAS, path.join(paths.ROOT, 'ideas'));
@@ -283,9 +301,16 @@ describe('PATHS object shape', () => {
283
301
  assert.equal(paths.ARCHIVE, path.join(paths.ROOT, 'archive'));
284
302
  assert.equal(paths.PROJECTS, path.join(paths.ROOT, 'projects'));
285
303
  });
304
+
305
+ it('does not contain LAYOUT or CONFIG_LEGACY keys', () => {
306
+ cwd = makeGitTempDir();
307
+ const paths = getPaths(cwd);
308
+ assert.equal('LAYOUT' in paths, false, 'LAYOUT should not exist in paths object');
309
+ assert.equal('CONFIG_LEGACY' in paths, false, 'CONFIG_LEGACY should not exist in paths object');
310
+ });
286
311
  });
287
312
 
288
- // ─── 6. Object.freeze (PATH-03) ─────────────────────────────────────────────
313
+ // ─── 7. Object.freeze (PATH-03) ─────────────────────────────────────────────
289
314
 
290
315
  describe('Object.freeze', () => {
291
316
  let cwd;
@@ -297,19 +322,13 @@ describe('Object.freeze', () => {
297
322
  });
298
323
 
299
324
  it('getPaths returns a frozen object', () => {
300
- cwd = makeTempDir();
301
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
302
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
303
-
325
+ cwd = makeGitTempDir();
304
326
  const paths = getPaths(cwd);
305
327
  assert.equal(Object.isFrozen(paths), true);
306
328
  });
307
329
 
308
330
  it('attempting to add a property does not modify the object', () => {
309
- cwd = makeTempDir();
310
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
311
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
312
-
331
+ cwd = makeGitTempDir();
313
332
  const paths = getPaths(cwd);
314
333
  try {
315
334
  paths.NEW_PROP = 'value';
@@ -320,7 +339,7 @@ describe('Object.freeze', () => {
320
339
  });
321
340
  });
322
341
 
323
- // ─── 7. Caching (PATH-03, PATH-04) ──────────────────────────────────────────
342
+ // ─── 8. Caching (PATH-03, OPT-01) ──────────────────────────────────────────
324
343
 
325
344
  describe('caching', () => {
326
345
  let cwdA, cwdB;
@@ -333,35 +352,15 @@ describe('caching', () => {
333
352
  cwdB = null;
334
353
  });
335
354
 
336
- it('same cwd returns same object (referential equality)', () => {
337
- cwdA = makeTempDir();
338
- fs.mkdirSync(path.join(cwdA, '.planning'), { recursive: true });
339
- fs.writeFileSync(path.join(cwdA, '.planning', 'config.json'), '{}');
340
-
355
+ it('same cwd returns same getPaths object (referential equality)', () => {
356
+ cwdA = makeGitTempDir();
341
357
  const first = getPaths(cwdA);
342
358
  const second = getPaths(cwdA);
343
359
  assert.equal(first === second, true, 'Expected referential equality');
344
360
  });
345
361
 
346
- it('different cwd returns different object', () => {
347
- cwdA = makeTempDir();
348
- fs.mkdirSync(path.join(cwdA, '.planning'), { recursive: true });
349
- fs.writeFileSync(path.join(cwdA, '.planning', 'config.json'), '{}');
350
-
351
- cwdB = makeTempDir();
352
- fs.mkdirSync(path.join(cwdB, '.planning'), { recursive: true });
353
- fs.writeFileSync(path.join(cwdB, '.planning', 'config.json'), '{}');
354
-
355
- const pathsA = getPaths(cwdA);
356
- const pathsB = getPaths(cwdB);
357
- assert.notEqual(pathsA === pathsB, true, 'Expected different objects');
358
- });
359
-
360
- it('resetPaths clears the cache (new reference after reset)', () => {
361
- cwdA = makeTempDir();
362
- fs.mkdirSync(path.join(cwdA, '.planning'), { recursive: true });
363
- fs.writeFileSync(path.join(cwdA, '.planning', 'config.json'), '{}');
364
-
362
+ it('resetPaths clears the getPaths cache (new reference after reset)', () => {
363
+ cwdA = makeGitTempDir();
365
364
  const before = getPaths(cwdA);
366
365
  resetPaths();
367
366
  const after = getPaths(cwdA);
@@ -369,7 +368,7 @@ describe('caching', () => {
369
368
  });
370
369
  });
371
370
 
372
- // ─── 8. initPaths / resetPaths (PATH-04) ─────────────────────────────────────
371
+ // ─── 9. initPaths / resetPaths (PATH-04) ─────────────────────────────────────
373
372
 
374
373
  describe('initPaths / resetPaths', () => {
375
374
  let cwd;
@@ -381,20 +380,14 @@ describe('initPaths / resetPaths', () => {
381
380
  });
382
381
 
383
382
  it('initPaths returns the same PATHS object as getPaths', () => {
384
- cwd = makeTempDir();
385
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
386
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
387
-
383
+ cwd = makeGitTempDir();
388
384
  const fromInit = initPaths(cwd);
389
385
  const fromGet = getPaths(cwd);
390
386
  assert.equal(fromInit === fromGet, true, 'initPaths and getPaths should return same reference');
391
387
  });
392
388
 
393
389
  it('resetPaths clears cache (verified by reference comparison)', () => {
394
- cwd = makeTempDir();
395
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
396
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
397
-
390
+ cwd = makeGitTempDir();
398
391
  const before = initPaths(cwd);
399
392
  resetPaths();
400
393
  const after = initPaths(cwd);
@@ -402,7 +395,7 @@ describe('initPaths / resetPaths', () => {
402
395
  });
403
396
  });
404
397
 
405
- // ─── 9. Leaf module constraint (PATH-05) ─────────────────────────────────────
398
+ // ─── 10. Leaf module constraint (PATH-05) ────────────────────────────────────
406
399
 
407
400
  describe('leaf module constraint', () => {
408
401
  it('paths.cjs has no DGS module imports (no require("./") patterns)', () => {
@@ -410,12 +403,12 @@ describe('leaf module constraint', () => {
410
403
  path.join(__dirname, 'paths.cjs'),
411
404
  'utf-8'
412
405
  );
413
- // Check for require('./anything') would indicate DGS module import
406
+ // Check for require('./anything') -- would indicate DGS module import
414
407
  const dgsImports = source.match(/require\s*\(\s*['"]\.\/[^'"]*['"]\s*\)/g);
415
408
  assert.equal(dgsImports, null, 'paths.cjs should have zero DGS module imports');
416
409
  });
417
410
 
418
- it('paths.cjs uses fs and path builtins', () => {
411
+ it('paths.cjs uses fs, path, and child_process builtins', () => {
419
412
  const source = fs.readFileSync(
420
413
  path.join(__dirname, 'paths.cjs'),
421
414
  'utf-8'
@@ -428,10 +421,14 @@ describe('leaf module constraint', () => {
428
421
  /require\s*\(\s*['"]path['"]\s*\)/.test(source),
429
422
  'Expected require("path") in paths.cjs'
430
423
  );
424
+ assert.ok(
425
+ /require\s*\(\s*['"]child_process['"]\s*\)/.test(source),
426
+ 'Expected require("child_process") in paths.cjs'
427
+ );
431
428
  });
432
429
  });
433
430
 
434
- // ─── 10. Edge cases ──────────────────────────────────────────────────────────
431
+ // ─── 11. Edge cases ─────────────────────────────────────────────────────────
435
432
 
436
433
  describe('edge cases', () => {
437
434
  let cwd;
@@ -443,11 +440,9 @@ describe('edge cases', () => {
443
440
  });
444
441
 
445
442
  it('paths returned even when subdirectories do not exist on disk', () => {
446
- cwd = makeTempDir();
447
- // No .planning/ directory created, no config files — default case
448
-
443
+ cwd = makeGitTempDir();
449
444
  const paths = getPaths(cwd);
450
- // Should still return all paths even though none of these directories exist
445
+ // Should return all paths even though none of these directories exist
451
446
  assert.ok(paths.PHASES);
452
447
  assert.ok(paths.IDEAS);
453
448
  assert.ok(paths.SPECS);
@@ -459,38 +454,10 @@ describe('edge cases', () => {
459
454
  });
460
455
 
461
456
  it('getPaths().PROJECTS returns absolute path ending in /projects', () => {
462
- cwd = makeTempDir();
463
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
464
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
465
-
457
+ cwd = makeGitTempDir();
466
458
  const paths = getPaths(cwd);
467
459
  assert.ok(path.isAbsolute(paths.PROJECTS), 'Expected PROJECTS to be absolute');
468
460
  assert.ok(paths.PROJECTS.endsWith('/projects') || paths.PROJECTS.endsWith('\\projects'),
469
461
  'Expected PROJECTS path to end with /projects');
470
462
  });
471
-
472
- it('relative cwd is normalized to absolute path', () => {
473
- // This test verifies that passing a relative cwd gives the same result
474
- // as passing the equivalent absolute path
475
- cwd = makeTempDir();
476
- fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
477
- fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
478
-
479
- // Get paths with absolute cwd
480
- const absResult = getPaths(cwd);
481
- resetPaths();
482
-
483
- // Calling with '.' should resolve to process.cwd(), which is different from
484
- // our temp dir, but the key behavior is that relative paths get resolved
485
- // to absolute via path.resolve()
486
- const dotResult = getPaths('.');
487
- // The important assertion: all returned paths are absolute regardless of input
488
- for (const [key, value] of Object.entries(dotResult)) {
489
- if (key === 'LAYOUT') continue;
490
- assert.ok(
491
- path.isAbsolute(value),
492
- `Expected ${key} to be absolute even with relative cwd input`
493
- );
494
- }
495
- });
496
463
  });
@@ -10,27 +10,28 @@ const { writeStateMd } = require('./state.cjs');
10
10
 
11
11
  /**
12
12
  * Resolve the phases directory for the current project context.
13
- * v2: .planning/<project>/phases/ | v1: .planning/phases/
13
+ * Returns path.join(cwd, projectRoot, 'phases') where projectRoot is
14
+ * from getProjectRoot() or '.' as fallback.
14
15
  */
15
16
  function resolvePhasesDir(cwd) {
16
17
  let projectRoot;
17
18
  try {
18
19
  projectRoot = getProjectRoot(cwd);
19
20
  } catch {
20
- projectRoot = '.planning';
21
+ projectRoot = '.';
21
22
  }
22
23
  return path.join(cwd, projectRoot, 'phases');
23
24
  }
24
25
 
25
26
  /**
26
27
  * Resolve the project root path (relative) for the current context.
27
- * v2: .planning/<project> | v1: .planning
28
+ * Returns getProjectRoot() result or '.' as fallback.
28
29
  */
29
30
  function resolveProjectRoot(cwd) {
30
31
  try {
31
32
  return getProjectRoot(cwd);
32
33
  } catch {
33
- return '.planning';
34
+ return '.';
34
35
  }
35
36
  }
36
37