@ktpartners/dgs-platform 2.9.0 → 3.3.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 +197 -0
- package/README.md +34 -2
- package/agents/dgs-executor.md +124 -3
- package/agents/dgs-idea-researcher.md +447 -0
- package/agents/dgs-plan-checker.md +61 -3
- package/agents/dgs-planner.md +51 -8
- package/bin/install.js +44 -0
- package/commands/dgs/abandon-quick.md +28 -0
- package/commands/dgs/add-tests.md +2 -2
- package/commands/dgs/audit-milestone.md +4 -3
- package/commands/dgs/capture-principle.md +11 -11
- package/commands/dgs/cleanup.md +2 -2
- package/commands/dgs/complete-milestone.md +11 -11
- package/commands/dgs/complete-quick.md +28 -0
- package/commands/dgs/create-milestone-job.md +2 -2
- package/commands/dgs/debug.md +3 -3
- package/commands/dgs/develop-idea.md +1 -1
- package/commands/dgs/diff-report.md +124 -0
- package/commands/dgs/fast.md +3 -1
- package/commands/dgs/health.md +1 -1
- package/commands/dgs/map-codebase.md +6 -6
- package/commands/dgs/new-milestone.md +5 -5
- package/commands/dgs/new-project.md +8 -21
- package/commands/dgs/package-scan.md +43 -0
- package/commands/dgs/plan-milestone-gaps.md +1 -1
- package/commands/dgs/progress.md +3 -3
- package/commands/dgs/quick-abandon.md +8 -0
- package/commands/dgs/quick-complete.md +8 -0
- package/commands/dgs/quick.md +10 -3
- package/commands/dgs/research-idea.md +3 -2
- package/commands/dgs/research-phase.md +3 -3
- package/commands/dgs/switch-project.md +14 -1
- package/commands/dgs/write-spec.md +3 -3
- package/deliver-great-systems/bin/dgs-tools.cjs +401 -32
- package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
- package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
- package/deliver-great-systems/bin/lib/commands.cjs +626 -46
- package/deliver-great-systems/bin/lib/commands.test.cjs +451 -0
- package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
- package/deliver-great-systems/bin/lib/config.cjs +80 -6
- package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
- package/deliver-great-systems/bin/lib/context.cjs +120 -0
- package/deliver-great-systems/bin/lib/core.cjs +35 -14
- package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
- package/deliver-great-systems/bin/lib/execution.cjs +49 -17
- package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
- package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
- package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
- package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
- package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
- package/deliver-great-systems/bin/lib/governance.cjs +211 -0
- package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
- package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
- package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
- package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
- package/deliver-great-systems/bin/lib/init.cjs +357 -61
- package/deliver-great-systems/bin/lib/init.test.cjs +625 -8
- package/deliver-great-systems/bin/lib/jobs.cjs +131 -25
- package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
- package/deliver-great-systems/bin/lib/migration.cjs +409 -1
- package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
- package/deliver-great-systems/bin/lib/milestone.cjs +154 -31
- package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
- package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
- package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
- package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
- package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
- package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
- package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
- package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
- package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
- package/deliver-great-systems/bin/lib/phase.cjs +146 -3
- package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
- package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
- package/deliver-great-systems/bin/lib/projects.cjs +65 -10
- package/deliver-great-systems/bin/lib/projects.test.cjs +198 -2
- package/deliver-great-systems/bin/lib/quick.cjs +739 -0
- package/deliver-great-systems/bin/lib/quick.test.cjs +730 -0
- package/deliver-great-systems/bin/lib/repos.cjs +37 -13
- package/deliver-great-systems/bin/lib/review.cjs +1821 -0
- package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
- package/deliver-great-systems/bin/lib/specs.cjs +3 -81
- package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
- package/deliver-great-systems/bin/lib/state.cjs +147 -55
- package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
- package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
- package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
- package/deliver-great-systems/bin/lib/sync.cjs +75 -0
- package/deliver-great-systems/bin/lib/verify.cjs +198 -7
- package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
- package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
- package/deliver-great-systems/bin/lib/worktrees.cjs +790 -0
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +963 -0
- package/deliver-great-systems/references/agent-step-reliability.md +60 -0
- package/deliver-great-systems/references/conflict-resolution.md +4 -0
- package/deliver-great-systems/references/context-tiers.md +4 -0
- package/deliver-great-systems/references/package-scan-config.md +151 -0
- package/deliver-great-systems/references/questioning.md +0 -30
- package/deliver-great-systems/references/spec-review-loop.md +1 -2
- package/deliver-great-systems/references/workflow-conventions.md +29 -0
- package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
- package/deliver-great-systems/templates/REVIEW.md +35 -0
- package/deliver-great-systems/templates/VALIDATION.md +1 -1
- package/deliver-great-systems/templates/claude-md.md +27 -0
- package/deliver-great-systems/templates/package-scan-report.md +108 -0
- package/deliver-great-systems/templates/project.md +6 -170
- package/deliver-great-systems/templates/summary.md +3 -1
- package/deliver-great-systems/workflows/abandon-quick.md +89 -0
- package/deliver-great-systems/workflows/add-idea.md +3 -3
- package/deliver-great-systems/workflows/add-phase.md +5 -0
- package/deliver-great-systems/workflows/add-tests.md +14 -0
- package/deliver-great-systems/workflows/add-todo.md +1 -0
- package/deliver-great-systems/workflows/approve-spec.md +25 -4
- package/deliver-great-systems/workflows/audit-milestone.md +66 -10
- package/deliver-great-systems/workflows/audit-phase.md +15 -5
- package/deliver-great-systems/workflows/cancel-job.md +2 -2
- package/deliver-great-systems/workflows/check-todos.md +2 -3
- package/deliver-great-systems/workflows/codereview.md +103 -9
- package/deliver-great-systems/workflows/complete-milestone.md +218 -24
- package/deliver-great-systems/workflows/complete-quick.md +106 -0
- package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
- package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
- package/deliver-great-systems/workflows/develop-idea.md +11 -11
- package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
- package/deliver-great-systems/workflows/discuss-idea.md +1 -1
- package/deliver-great-systems/workflows/discuss-phase.md +3 -2
- package/deliver-great-systems/workflows/execute-phase.md +209 -33
- package/deliver-great-systems/workflows/execute-plan.md +22 -22
- package/deliver-great-systems/workflows/help.md +53 -20
- package/deliver-great-systems/workflows/import-spec.md +65 -7
- package/deliver-great-systems/workflows/init-product.md +45 -167
- package/deliver-great-systems/workflows/new-milestone.md +140 -33
- package/deliver-great-systems/workflows/new-project.md +60 -331
- package/deliver-great-systems/workflows/package-scan.md +59 -0
- package/deliver-great-systems/workflows/plan-phase.md +79 -1
- package/deliver-great-systems/workflows/progress-all.md +133 -0
- package/deliver-great-systems/workflows/quick-abandon.md +89 -0
- package/deliver-great-systems/workflows/quick-complete.md +106 -0
- package/deliver-great-systems/workflows/quick.md +328 -26
- package/deliver-great-systems/workflows/refine-spec.md +1 -1
- package/deliver-great-systems/workflows/research-idea.md +77 -139
- package/deliver-great-systems/workflows/resume-project.md +2 -2
- package/deliver-great-systems/workflows/run-job.md +29 -43
- package/deliver-great-systems/workflows/settings.md +13 -77
- package/deliver-great-systems/workflows/validate-phase.md +39 -1
- package/deliver-great-systems/workflows/verify-work.md +14 -0
- package/deliver-great-systems/workflows/write-spec.md +11 -13
- package/hooks/dist/dgs-enforce-discipline.js +196 -0
- package/package.json +1 -1
- package/scripts/build-hooks.js +1 -0
|
@@ -42,9 +42,11 @@ function createProjectSubfolder(cwd, slug, name, options) {
|
|
|
42
42
|
// Create project directory
|
|
43
43
|
fs.mkdirSync(projectDir, { recursive: true });
|
|
44
44
|
|
|
45
|
-
// Create standard subdirectories
|
|
45
|
+
// Create standard subdirectories with .gitkeep so git tracks empty folders
|
|
46
46
|
for (const dir of STANDARD_DIRS) {
|
|
47
|
-
|
|
47
|
+
const dirPath = path.join(projectDir, dir);
|
|
48
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
49
|
+
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
// Create standard files with templates
|
|
@@ -163,21 +165,25 @@ function scanProjectReposTags(cwd, slug) {
|
|
|
163
165
|
return Array.from(allRepos).sort();
|
|
164
166
|
}
|
|
165
167
|
|
|
166
|
-
// ───
|
|
168
|
+
// ─── Projects Readonly Listing ──────────────────────────────────────────────
|
|
167
169
|
|
|
168
170
|
/**
|
|
169
|
-
*
|
|
171
|
+
* Enumerate all project subfolders and build a { projects, warnings } result
|
|
172
|
+
* WITHOUT writing PROJECTS.md. Pure read function — no side effects.
|
|
170
173
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
174
|
+
* Thin-skeleton projects (PROJECT.md present, STATE.md absent) yield a placeholder
|
|
175
|
+
* entry with status "No milestone yet" and emit no warning — this is the post-thin-skeleton
|
|
176
|
+
* contract where /dgs:new-project writes only PROJECT.md and STATE.md is created later
|
|
177
|
+
* by /dgs:new-milestone.
|
|
173
178
|
*
|
|
174
|
-
* Ghost
|
|
175
|
-
*
|
|
179
|
+
* Ghost-project guard (defensive): a directory with neither PROJECT.md nor STATE.md is
|
|
180
|
+
* normally filtered out by getProjectFolders before reaching here. If a future caller
|
|
181
|
+
* passes such a slug directly, the legacy warning is preserved and the entry is omitted.
|
|
176
182
|
*
|
|
177
183
|
* @param {string} cwd - Working directory (product root)
|
|
178
184
|
* @returns {{ projects: Array, warnings: string[] }}
|
|
179
185
|
*/
|
|
180
|
-
function
|
|
186
|
+
function listProjectsReadonly(cwd) {
|
|
181
187
|
const slugs = getProjectFolders(cwd);
|
|
182
188
|
const warnings = [];
|
|
183
189
|
const projects = [];
|
|
@@ -185,7 +191,22 @@ function regenerateProjectsMd(cwd) {
|
|
|
185
191
|
for (const slug of slugs) {
|
|
186
192
|
const state = readProjectState(cwd, slug);
|
|
187
193
|
if (!state) {
|
|
188
|
-
|
|
194
|
+
// No STATE.md (or unreadable): if PROJECT.md is present, treat as thin-skeleton
|
|
195
|
+
// and emit a placeholder row. Only warn when neither marker is present (defensive
|
|
196
|
+
// guard for direct callers that bypass getProjectFolders).
|
|
197
|
+
const projectMdPath = path.join(getProjectDir(cwd, slug), 'PROJECT.md');
|
|
198
|
+
if (fs.existsSync(projectMdPath)) {
|
|
199
|
+
projects.push({
|
|
200
|
+
name: slug,
|
|
201
|
+
status: 'No milestone yet',
|
|
202
|
+
repos_touched: '',
|
|
203
|
+
current_phase: '',
|
|
204
|
+
completed_date: '',
|
|
205
|
+
progress: '',
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
warnings.push(`Ghost project: ${slug} (STATE.md missing or unreadable)`);
|
|
209
|
+
}
|
|
189
210
|
continue;
|
|
190
211
|
}
|
|
191
212
|
|
|
@@ -200,6 +221,25 @@ function regenerateProjectsMd(cwd) {
|
|
|
200
221
|
});
|
|
201
222
|
}
|
|
202
223
|
|
|
224
|
+
return { projects, warnings };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ─── PROJECTS.md Regeneration ───────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Regenerate PROJECTS.md by scanning all project subfolders.
|
|
231
|
+
*
|
|
232
|
+
* Reads each project's STATE.md for status/phase/progress, scans plan
|
|
233
|
+
* <repos> tags for repos touched, and writes the derived PROJECTS.md.
|
|
234
|
+
*
|
|
235
|
+
* Delegates the read phase to listProjectsReadonly.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} cwd - Working directory (product root)
|
|
238
|
+
* @returns {{ projects: Array, warnings: string[] }}
|
|
239
|
+
*/
|
|
240
|
+
function regenerateProjectsMd(cwd) {
|
|
241
|
+
const { projects, warnings } = listProjectsReadonly(cwd);
|
|
242
|
+
|
|
203
243
|
// Write PROJECTS.md
|
|
204
244
|
let content = '# Projects\n\n';
|
|
205
245
|
content += '## Active\n\n';
|
|
@@ -652,6 +692,20 @@ function cmdProjectsReactivate(cwd, options, raw) {
|
|
|
652
692
|
const regenResult = regenerateProjectsMd(cwd);
|
|
653
693
|
const remainingActive = regenResult.projects.filter(p => !p.status.toLowerCase().includes('completed'));
|
|
654
694
|
|
|
695
|
+
// Commit STATE.md and PROJECTS.md so reactivation is persisted
|
|
696
|
+
const planningRoot = getPlanningRoot(cwd);
|
|
697
|
+
const projectDir = getProjectDir(cwd, slug);
|
|
698
|
+
const statePath = path.join(projectDir, 'STATE.md');
|
|
699
|
+
const projectsPath = path.join(planningRoot, 'PROJECTS.md');
|
|
700
|
+
const filesToCommit = [statePath, projectsPath].filter(f => fs.existsSync(f));
|
|
701
|
+
try {
|
|
702
|
+
const { execSync } = require('child_process');
|
|
703
|
+
for (const f of filesToCommit) {
|
|
704
|
+
execSync(`git add "${f}"`, { cwd: planningRoot, stdio: 'pipe' });
|
|
705
|
+
}
|
|
706
|
+
execSync(`git commit -m "docs: reactivate project ${slug}" --allow-empty`, { cwd: planningRoot, stdio: 'pipe' });
|
|
707
|
+
} catch { /* commit may fail if nothing changed — safe to ignore */ }
|
|
708
|
+
|
|
655
709
|
output({ reactivated: true, slug, set_as_current: !!opts.set_current, remaining_active: remainingActive.length }, raw);
|
|
656
710
|
}
|
|
657
711
|
|
|
@@ -677,6 +731,7 @@ module.exports = {
|
|
|
677
731
|
createProjectSubfolder,
|
|
678
732
|
readProjectState,
|
|
679
733
|
scanProjectReposTags,
|
|
734
|
+
listProjectsReadonly,
|
|
680
735
|
regenerateProjectsMd,
|
|
681
736
|
completeProject,
|
|
682
737
|
reactivateProject,
|
|
@@ -11,22 +11,57 @@ const os = require('os');
|
|
|
11
11
|
|
|
12
12
|
const { createTempDir, cleanupDir, createFixture , initGitRepo } = require('./test-helpers.cjs');
|
|
13
13
|
const { resetPaths } = require('./paths.cjs');
|
|
14
|
-
const { getProjectRoot } = require('./core.cjs');
|
|
14
|
+
const { getProjectRoot, loadConfig } = require('./core.cjs');
|
|
15
15
|
|
|
16
16
|
// Helper: create projects/ directory structure
|
|
17
17
|
function setupPlanning(cwd) {
|
|
18
18
|
fs.mkdirSync(path.join(cwd, 'projects'), { recursive: true });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
// Helper: create a project subfolder
|
|
21
|
+
// Helper: create a fully-scaffolded project subfolder under projects/<slug>/
|
|
22
|
+
// (writes PROJECT.md as the canonical project marker plus optional STATE.md for milestone state)
|
|
22
23
|
function createProjectManually(cwd, slug, stateContent) {
|
|
23
24
|
const projDir = path.join(cwd, 'projects', slug);
|
|
24
25
|
fs.mkdirSync(projDir, { recursive: true });
|
|
26
|
+
fs.writeFileSync(path.join(projDir, 'PROJECT.md'), `# Project: ${slug}\n`);
|
|
25
27
|
if (stateContent) {
|
|
26
28
|
fs.writeFileSync(path.join(projDir, 'STATE.md'), stateContent);
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
// Helper: create a thin-skeleton project (PROJECT.md only, no STATE.md) — mirrors what
|
|
33
|
+
// /dgs:new-project writes before /dgs:new-milestone creates STATE.md.
|
|
34
|
+
function createThinSkeletonProject(cwd, slug) {
|
|
35
|
+
const projDir = path.join(cwd, 'projects', slug);
|
|
36
|
+
fs.mkdirSync(projDir, { recursive: true });
|
|
37
|
+
fs.writeFileSync(path.join(projDir, 'PROJECT.md'), `# Project: ${slug}\n`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Capture stdout helper for cmdProjects* CLI calls (mirrors commands.test.cjs:48-70).
|
|
41
|
+
function captureStdout(fn) {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
44
|
+
const origExit = process.exit;
|
|
45
|
+
let exitCode = null;
|
|
46
|
+
process.stdout.write = (data) => { chunks.push(String(data)); return true; };
|
|
47
|
+
process.exit = (code) => {
|
|
48
|
+
exitCode = code == null ? 0 : code;
|
|
49
|
+
throw new Error('__EXIT__');
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
fn();
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (e && e.message !== '__EXIT__') throw e;
|
|
55
|
+
} finally {
|
|
56
|
+
process.stdout.write = origWrite;
|
|
57
|
+
process.exit = origExit;
|
|
58
|
+
}
|
|
59
|
+
const stdout = chunks.join('');
|
|
60
|
+
let json = null;
|
|
61
|
+
try { json = JSON.parse(stdout); } catch { /* not JSON */ }
|
|
62
|
+
return { stdout, exitCode, json };
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
// Helper: create plan file with <repos> tags under projects/<slug>/phases/
|
|
31
66
|
function createPlanFile(cwd, slug, phaseDir, planName, content) {
|
|
32
67
|
const dir = path.join(cwd, 'projects', slug, 'phases', phaseDir);
|
|
@@ -39,10 +74,12 @@ const {
|
|
|
39
74
|
readProjectState,
|
|
40
75
|
scanProjectReposTags,
|
|
41
76
|
regenerateProjectsMd,
|
|
77
|
+
listProjectsReadonly,
|
|
42
78
|
completeProject,
|
|
43
79
|
reactivateProject,
|
|
44
80
|
parseProjectsMd,
|
|
45
81
|
checkSlugPrefixCollision,
|
|
82
|
+
cmdProjectsSwitch,
|
|
46
83
|
} = require('./projects.cjs');
|
|
47
84
|
|
|
48
85
|
// ─── createProjectSubfolder ─────────────────────────────────────────────────
|
|
@@ -321,6 +358,131 @@ describe('regenerateProjectsMd', () => {
|
|
|
321
358
|
const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
|
|
322
359
|
assert.ok(content.startsWith('# Projects'));
|
|
323
360
|
});
|
|
361
|
+
|
|
362
|
+
it('writes thin-skeleton placeholder row into Active table', () => {
|
|
363
|
+
createThinSkeletonProject(tmpDir, 'word-gen');
|
|
364
|
+
regenerateProjectsMd(tmpDir);
|
|
365
|
+
const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
|
|
366
|
+
const [activeSection, completedSection] = content.split('## Completed');
|
|
367
|
+
// Active row should match: | word-gen | No milestone yet | | |
|
|
368
|
+
assert.match(activeSection, /\|\s*word-gen\s*\|\s*No milestone yet\s*\|\s*\|\s*\|/);
|
|
369
|
+
// word-gen must NOT appear in the Completed section
|
|
370
|
+
assert.ok(!completedSection.includes('word-gen'), 'thin-skeleton project must not appear in Completed section');
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// ─── listProjectsReadonly ───────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
describe('listProjectsReadonly', () => {
|
|
377
|
+
let tmpDir;
|
|
378
|
+
|
|
379
|
+
beforeEach(() => {
|
|
380
|
+
tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
|
|
381
|
+
setupPlanning(tmpDir);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
|
|
385
|
+
|
|
386
|
+
it('returns same projects shape as regenerateProjectsMd for a fixture with 2 active + 1 completed', () => {
|
|
387
|
+
createProjectManually(tmpDir, 'active-a', '# Project State\n\nPhase: 1\nStatus: In progress\nProgress: [##--------] 20%\n');
|
|
388
|
+
createProjectManually(tmpDir, 'active-b', '# Project State\n\nPhase: 3\nStatus: Active\nProgress: [#####-----] 50%\n');
|
|
389
|
+
createProjectManually(tmpDir, 'done-c', '# Project State\n\nPhase: 10\nStatus: completed\nProgress: [##########] 100%\nCompleted: 2026-02-15\n');
|
|
390
|
+
|
|
391
|
+
const readResult = listProjectsReadonly(tmpDir);
|
|
392
|
+
const regenResult = regenerateProjectsMd(tmpDir);
|
|
393
|
+
|
|
394
|
+
assert.ok(Array.isArray(readResult.projects));
|
|
395
|
+
assert.ok(Array.isArray(readResult.warnings));
|
|
396
|
+
assert.strictEqual(readResult.projects.length, regenResult.projects.length);
|
|
397
|
+
// Projects should have identical fields
|
|
398
|
+
const readByName = Object.fromEntries(readResult.projects.map(p => [p.name, p]));
|
|
399
|
+
const regenByName = Object.fromEntries(regenResult.projects.map(p => [p.name, p]));
|
|
400
|
+
for (const name of Object.keys(readByName)) {
|
|
401
|
+
assert.deepStrictEqual(readByName[name], regenByName[name]);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('does NOT write PROJECTS.md', () => {
|
|
406
|
+
createProjectManually(tmpDir, 'proj-a', '# Project State\n\nPhase: 2\nStatus: Active\n');
|
|
407
|
+
const projectsMdPath = path.join(tmpDir, 'PROJECTS.md');
|
|
408
|
+
assert.strictEqual(fs.existsSync(projectsMdPath), false);
|
|
409
|
+
|
|
410
|
+
const result = listProjectsReadonly(tmpDir);
|
|
411
|
+
assert.ok(Array.isArray(result.projects));
|
|
412
|
+
|
|
413
|
+
assert.strictEqual(fs.existsSync(projectsMdPath), false, 'listProjectsReadonly must not create PROJECTS.md');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('does NOT modify an existing PROJECTS.md', () => {
|
|
417
|
+
createProjectManually(tmpDir, 'proj-a', '# Project State\n\nPhase: 2\nStatus: Active\n');
|
|
418
|
+
const projectsMdPath = path.join(tmpDir, 'PROJECTS.md');
|
|
419
|
+
const sentinel = '# Projects\n\nSENTINEL CONTENT — should not be overwritten by listProjectsReadonly\n';
|
|
420
|
+
fs.writeFileSync(projectsMdPath, sentinel);
|
|
421
|
+
|
|
422
|
+
listProjectsReadonly(tmpDir);
|
|
423
|
+
|
|
424
|
+
const after = fs.readFileSync(projectsMdPath, 'utf-8');
|
|
425
|
+
assert.strictEqual(after, sentinel, 'listProjectsReadonly must not modify existing PROJECTS.md');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('returns a project entry with all expected fields', () => {
|
|
429
|
+
createProjectManually(tmpDir, 'proj-a', '# Project State\n\nPhase: 5\nStatus: Active\nProgress: [###-------] 30%\n');
|
|
430
|
+
const result = listProjectsReadonly(tmpDir);
|
|
431
|
+
assert.strictEqual(result.projects.length, 1);
|
|
432
|
+
const p = result.projects[0];
|
|
433
|
+
assert.strictEqual(p.name, 'proj-a');
|
|
434
|
+
assert.strictEqual(p.status, 'Active');
|
|
435
|
+
assert.strictEqual(p.current_phase, '5');
|
|
436
|
+
assert.strictEqual(p.progress, 30);
|
|
437
|
+
assert.strictEqual(typeof p.repos_touched, 'string');
|
|
438
|
+
assert.ok('completed_date' in p);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('returns ghost-project behavior consistent with regenerateProjectsMd (omits projects without STATE.md)', () => {
|
|
442
|
+
// Ghost project: directory exists but no STATE.md
|
|
443
|
+
fs.mkdirSync(path.join(tmpDir, 'projects', 'ghost-project'), { recursive: true });
|
|
444
|
+
// Real project
|
|
445
|
+
createProjectManually(tmpDir, 'real-project', '# State\n\nStatus: Active\n');
|
|
446
|
+
|
|
447
|
+
const result = listProjectsReadonly(tmpDir);
|
|
448
|
+
assert.ok(Array.isArray(result.warnings));
|
|
449
|
+
assert.ok(!result.projects.some(p => p.name === 'ghost-project'), 'ghost project should be omitted from projects array');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('empty projects directory returns { projects: [], warnings: [] }', () => {
|
|
453
|
+
const result = listProjectsReadonly(tmpDir);
|
|
454
|
+
assert.deepStrictEqual(result.projects, []);
|
|
455
|
+
assert.ok(Array.isArray(result.warnings));
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('returns placeholder entry with status "No milestone yet" for thin-skeleton (PROJECT.md only) and emits no warning for that slug', () => {
|
|
459
|
+
createThinSkeletonProject(tmpDir, 'word-gen');
|
|
460
|
+
const result = listProjectsReadonly(tmpDir);
|
|
461
|
+
assert.strictEqual(result.projects.length, 1);
|
|
462
|
+
assert.deepStrictEqual(result.projects[0], {
|
|
463
|
+
name: 'word-gen',
|
|
464
|
+
status: 'No milestone yet',
|
|
465
|
+
repos_touched: '',
|
|
466
|
+
current_phase: '',
|
|
467
|
+
completed_date: '',
|
|
468
|
+
progress: '',
|
|
469
|
+
});
|
|
470
|
+
assert.ok(Array.isArray(result.warnings));
|
|
471
|
+
assert.ok(!result.warnings.some(w => w.includes('word-gen')), 'no warning should mention word-gen');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('preserves ghost-warning code path when neither PROJECT.md nor STATE.md exists', () => {
|
|
475
|
+
// Genuine ghost: no marker files at all — getProjectFolders filters it out, so it
|
|
476
|
+
// never reaches listProjectsReadonly's loop. Real project alongside to keep suite non-empty.
|
|
477
|
+
fs.mkdirSync(path.join(tmpDir, 'projects', 'ghost'), { recursive: true });
|
|
478
|
+
createProjectManually(tmpDir, 'real-project', '# State\n\nStatus: Active\n');
|
|
479
|
+
|
|
480
|
+
const result = listProjectsReadonly(tmpDir);
|
|
481
|
+
assert.ok(Array.isArray(result.warnings));
|
|
482
|
+
assert.ok(!result.projects.some(p => p.name === 'ghost'), 'ghost dir should be omitted from projects array');
|
|
483
|
+
// The warnings array remains a defensive code path; legacy callers may still trip it.
|
|
484
|
+
// No assertion on warnings content here — discovery layer (getProjectFolders) handles ghost exclusion.
|
|
485
|
+
});
|
|
324
486
|
});
|
|
325
487
|
|
|
326
488
|
// ─── completeProject ────────────────────────────────────────────────────────
|
|
@@ -857,6 +1019,7 @@ describe('cmdProjectsSwitch guards', () => {
|
|
|
857
1019
|
'config.json': JSON.stringify({ current_project: 'finished-proj' }),
|
|
858
1020
|
'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
1021
|
'REPOS.md': '# Repos\n\n| Name | Path |\n',
|
|
1022
|
+
'projects/finished-proj/PROJECT.md': '# Project: Finished Proj\n',
|
|
860
1023
|
'projects/finished-proj/STATE.md': '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n',
|
|
861
1024
|
});
|
|
862
1025
|
|
|
@@ -870,3 +1033,36 @@ describe('cmdProjectsSwitch guards', () => {
|
|
|
870
1033
|
}
|
|
871
1034
|
});
|
|
872
1035
|
});
|
|
1036
|
+
|
|
1037
|
+
// ─── cmdProjectsSwitch thin-skeleton ────────────────────────────────────────
|
|
1038
|
+
|
|
1039
|
+
describe('cmdProjectsSwitch thin-skeleton', () => {
|
|
1040
|
+
let tmpDir;
|
|
1041
|
+
|
|
1042
|
+
beforeEach(() => {
|
|
1043
|
+
tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
|
|
1044
|
+
setupPlanning(tmpDir);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
|
|
1048
|
+
|
|
1049
|
+
it('cmdProjectsSwitch succeeds on a thin-skeleton slug and writes current_project to config.json', () => {
|
|
1050
|
+
// v2 prerequisites: config.json, PROJECTS.md, REPOS.md at planning root,
|
|
1051
|
+
// plus the thin-skeleton project under projects/<slug>/PROJECT.md
|
|
1052
|
+
fs.writeFileSync(path.join(tmpDir, 'config.json'), '{}');
|
|
1053
|
+
fs.writeFileSync(path.join(tmpDir, 'PROJECTS.md'), '# Projects\n');
|
|
1054
|
+
fs.writeFileSync(path.join(tmpDir, 'REPOS.md'), '# Repos\n');
|
|
1055
|
+
createThinSkeletonProject(tmpDir, 'word-gen');
|
|
1056
|
+
|
|
1057
|
+
const { exitCode } = captureStdout(() => {
|
|
1058
|
+
cmdProjectsSwitch(tmpDir, 'word-gen', true);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
// exitCode 0 (or unset null = success path that did not throw) signals success
|
|
1062
|
+
assert.ok(exitCode === 0 || exitCode === null, `expected success exit, got ${exitCode}`);
|
|
1063
|
+
|
|
1064
|
+
// config.json must now contain current_project: word-gen
|
|
1065
|
+
const config = loadConfig(tmpDir);
|
|
1066
|
+
assert.strictEqual(config.current_project, 'word-gen');
|
|
1067
|
+
});
|
|
1068
|
+
});
|