@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.
- package/CHANGELOG.md +30 -0
- package/README.md +15 -12
- package/agents/dgs-executor.md +0 -52
- package/deliver-great-systems/bin/dgs-tools.cjs +66 -10
- package/deliver-great-systems/bin/lib/commands.cjs +1 -8
- package/deliver-great-systems/bin/lib/config.cjs +9 -90
- package/deliver-great-systems/bin/lib/context.cjs +2 -2
- package/deliver-great-systems/bin/lib/context.test.cjs +100 -100
- package/deliver-great-systems/bin/lib/core.cjs +17 -57
- package/deliver-great-systems/bin/lib/core.test.cjs +166 -170
- package/deliver-great-systems/bin/lib/docs.cjs +3 -3
- package/deliver-great-systems/bin/lib/docs.test.cjs +14 -7
- package/deliver-great-systems/bin/lib/execution.cjs +2 -2
- package/deliver-great-systems/bin/lib/execution.test.cjs +65 -67
- package/deliver-great-systems/bin/lib/ideas.cjs +4 -4
- package/deliver-great-systems/bin/lib/ideas.test.cjs +45 -44
- package/deliver-great-systems/bin/lib/init.cjs +9 -4
- package/deliver-great-systems/bin/lib/init.test.cjs +242 -175
- package/deliver-great-systems/bin/lib/jobs.cjs +1 -1
- package/deliver-great-systems/bin/lib/jobs.test.cjs +203 -202
- package/deliver-great-systems/bin/lib/migration.cjs +256 -281
- package/deliver-great-systems/bin/lib/migration.test.cjs +385 -440
- package/deliver-great-systems/bin/lib/milestone.cjs +1 -1
- package/deliver-great-systems/bin/lib/overlap.cjs +4 -4
- package/deliver-great-systems/bin/lib/overlap.test.cjs +45 -44
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +16 -22
- package/deliver-great-systems/bin/lib/paths.cjs +60 -59
- package/deliver-great-systems/bin/lib/paths.test.cjs +192 -225
- package/deliver-great-systems/bin/lib/phase.cjs +5 -4
- package/deliver-great-systems/bin/lib/projects.cjs +8 -8
- package/deliver-great-systems/bin/lib/projects.test.cjs +75 -74
- package/deliver-great-systems/bin/lib/repos.cjs +94 -230
- package/deliver-great-systems/bin/lib/repos.test.cjs +84 -75
- package/deliver-great-systems/bin/lib/search.cjs +4 -4
- package/deliver-great-systems/bin/lib/specs.cjs +2 -2
- package/deliver-great-systems/bin/lib/sync.cjs +1 -1
- package/deliver-great-systems/bin/lib/template.cjs +3 -3
- package/deliver-great-systems/bin/lib/test-helpers.cjs +59 -162
- package/deliver-great-systems/bin/lib/verify.cjs +3 -3
- package/deliver-great-systems/references/planning-config.md +7 -8
- package/deliver-great-systems/workflows/add-tests.md +1 -1
- package/deliver-great-systems/workflows/approve-spec.md +1 -11
- package/deliver-great-systems/workflows/complete-milestone.md +2 -2
- package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
- package/deliver-great-systems/workflows/create-milestone-job.md +2 -2
- package/deliver-great-systems/workflows/discuss-phase.md +2 -2
- package/deliver-great-systems/workflows/execute-phase.md +63 -4
- package/deliver-great-systems/workflows/execute-plan.md +0 -51
- package/deliver-great-systems/workflows/find-related-ideas.md +1 -1
- package/deliver-great-systems/workflows/help.md +55 -84
- package/deliver-great-systems/workflows/init-product.md +14 -451
- package/deliver-great-systems/workflows/map-codebase.md +109 -0
- package/deliver-great-systems/workflows/new-milestone.md +16 -6
- package/deliver-great-systems/workflows/new-project.md +22 -681
- package/deliver-great-systems/workflows/quick.md +2 -2
- package/deliver-great-systems/workflows/run-job.md +56 -0
- package/package.json +1 -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
16
|
+
// Helper: create projects/ directory structure
|
|
16
17
|
function setupPlanning(cwd) {
|
|
17
|
-
fs.mkdirSync(path.join(cwd, '
|
|
18
|
+
fs.mkdirSync(path.join(cwd, 'projects'), { recursive: true });
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
// Helper: create a project subfolder with STATE.md manually under
|
|
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, '
|
|
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
|
|
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, '
|
|
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
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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
|
-
'
|
|
857
|
-
'
|
|
858
|
-
'
|
|
859
|
-
'
|
|
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 {
|