@ktpartners/dgs-platform 2.8.0 → 3.0.4
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 +96 -0
- package/README.md +41 -13
- package/agents/dgs-plan-checker.md +29 -3
- package/agents/dgs-planner.md +10 -0
- package/commands/dgs/abandon-quick.md +28 -0
- package/commands/dgs/add-tests.md +2 -2
- package/commands/dgs/audit-milestone.md +2 -2
- 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/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 +6 -6
- 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 +2 -2
- package/commands/dgs/research-phase.md +3 -3
- package/commands/dgs/switch-project.md +1 -1
- package/commands/dgs/write-spec.md +3 -3
- package/deliver-great-systems/bin/dgs-tools.cjs +284 -30
- package/deliver-great-systems/bin/lib/commands.cjs +316 -31
- package/deliver-great-systems/bin/lib/commands.test.cjs +336 -0
- package/deliver-great-systems/bin/lib/config.cjs +39 -6
- package/deliver-great-systems/bin/lib/context.cjs +120 -0
- package/deliver-great-systems/bin/lib/core.cjs +28 -11
- package/deliver-great-systems/bin/lib/execution.cjs +49 -17
- package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -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 +306 -39
- package/deliver-great-systems/bin/lib/init.test.cjs +416 -6
- package/deliver-great-systems/bin/lib/jobs.cjs +124 -21
- 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 +54 -29
- package/deliver-great-systems/bin/lib/phase.cjs +128 -2
- package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
- package/deliver-great-systems/bin/lib/projects.cjs +28 -8
- package/deliver-great-systems/bin/lib/projects.test.cjs +86 -0
- package/deliver-great-systems/bin/lib/quick.cjs +584 -0
- package/deliver-great-systems/bin/lib/quick.test.cjs +596 -0
- package/deliver-great-systems/bin/lib/repos.cjs +25 -1
- 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 +142 -54
- package/deliver-great-systems/bin/lib/sync.cjs +75 -0
- package/deliver-great-systems/bin/lib/verify.cjs +80 -1
- package/deliver-great-systems/bin/lib/worktrees.cjs +764 -0
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +887 -0
- package/deliver-great-systems/templates/claude-md.md +16 -0
- 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-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-phase.md +15 -5
- package/deliver-great-systems/workflows/cancel-job.md +1 -1
- package/deliver-great-systems/workflows/check-todos.md +2 -3
- package/deliver-great-systems/workflows/complete-milestone.md +197 -22
- package/deliver-great-systems/workflows/complete-quick.md +68 -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/execute-phase.md +121 -32
- package/deliver-great-systems/workflows/execute-plan.md +12 -21
- package/deliver-great-systems/workflows/help.md +33 -29
- package/deliver-great-systems/workflows/init-product.md +2 -18
- package/deliver-great-systems/workflows/new-milestone.md +40 -24
- package/deliver-great-systems/workflows/new-project.md +22 -680
- 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 +68 -0
- package/deliver-great-systems/workflows/quick.md +152 -23
- package/deliver-great-systems/workflows/refine-spec.md +1 -1
- package/deliver-great-systems/workflows/research-idea.md +8 -8
- package/deliver-great-systems/workflows/resume-project.md +2 -2
- package/deliver-great-systems/workflows/run-job.md +8 -8
- 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 +2 -2
- package/package.json +1 -1
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const { execGit, safeReadFile, loadConfig, output, error } = require('./core.cjs');
|
|
12
|
-
const { getPlanningRoot } = require('./paths.cjs');
|
|
12
|
+
const { getPlanningRoot, PROJECTS_DIR } = require('./paths.cjs');
|
|
13
13
|
const { parseReposMd } = require('./repos.cjs');
|
|
14
14
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
15
15
|
const { scanProjectReposTags } = require('./projects.cjs');
|
|
16
16
|
|
|
17
|
+
// Lazy require to avoid circular dependency (context.cjs does not require execution.cjs,
|
|
18
|
+
// but keeping it lazy is a safety practice for future refactoring).
|
|
19
|
+
function _getResolveCodeContext() { return require('./context.cjs').resolveCodeContext; }
|
|
20
|
+
|
|
17
21
|
// ─── Repo Path Resolution ───────────────────────────────────────────────────
|
|
18
22
|
|
|
19
23
|
/**
|
|
@@ -24,7 +28,26 @@ const { scanProjectReposTags } = require('./projects.cjs');
|
|
|
24
28
|
* @param {Array} repos - Parsed repos array (optional, loads from REPOS.md if not provided)
|
|
25
29
|
* @returns {{ absPath: string, repo: Object } | null}
|
|
26
30
|
*/
|
|
27
|
-
function resolveRepoPath(cwd, repoName, repos) {
|
|
31
|
+
function resolveRepoPath(cwd, repoName, repos, useActiveContext) {
|
|
32
|
+
// If active context requested, resolve to worktree directory
|
|
33
|
+
if (useActiveContext) {
|
|
34
|
+
try {
|
|
35
|
+
const ctx = _getResolveCodeContext()(cwd, repoName);
|
|
36
|
+
if (ctx && ctx.type !== 'main') {
|
|
37
|
+
// Resolve the repo object for metadata (still need repo.name etc.)
|
|
38
|
+
if (!repos) {
|
|
39
|
+
const parsed = parseReposMd(cwd);
|
|
40
|
+
if (parsed) repos = parsed.repos;
|
|
41
|
+
}
|
|
42
|
+
const repo = repos ? repos.find(r => r.name === repoName) : null;
|
|
43
|
+
return { absPath: ctx.directory, repo: repo || { name: repoName, path: ctx.directory } };
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Fall through to standard resolution
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Standard resolution via REPOS.md
|
|
28
51
|
if (!repos) {
|
|
29
52
|
const parsed = parseReposMd(cwd);
|
|
30
53
|
if (!parsed) return null;
|
|
@@ -48,8 +71,8 @@ function resolveRepoPath(cwd, repoName, repos) {
|
|
|
48
71
|
* @param {Array} repos - Parsed repos array (optional, loads from REPOS.md if not provided)
|
|
49
72
|
* @returns {{ absFilePath: string, repoAbsPath: string, repo: Object } | null}
|
|
50
73
|
*/
|
|
51
|
-
function resolveRepoRelativePath(cwd, repoName, relativePath, repos) {
|
|
52
|
-
const resolved = resolveRepoPath(cwd, repoName, repos);
|
|
74
|
+
function resolveRepoRelativePath(cwd, repoName, relativePath, repos, useActiveContext) {
|
|
75
|
+
const resolved = resolveRepoPath(cwd, repoName, repos, useActiveContext);
|
|
53
76
|
if (!resolved) return null;
|
|
54
77
|
const absFilePath = path.join(resolved.absPath, relativePath);
|
|
55
78
|
return { absFilePath, repoAbsPath: resolved.absPath, repo: resolved.repo };
|
|
@@ -65,14 +88,14 @@ function resolveRepoRelativePath(cwd, repoName, relativePath, repos) {
|
|
|
65
88
|
* @param {string} [phaseDir] - Optional phase directory to check for partial SUMMARY.md
|
|
66
89
|
* @returns {{ passed: boolean, dirty_repos: Array, partial_execution: Object|null, missing_repos: string[] }}
|
|
67
90
|
*/
|
|
68
|
-
function preflightCheck(cwd, repoNames, phaseDir) {
|
|
91
|
+
function preflightCheck(cwd, repoNames, phaseDir, useActiveContext) {
|
|
69
92
|
const parsed = parseReposMd(cwd);
|
|
70
93
|
const repos = parsed ? parsed.repos : [];
|
|
71
94
|
const dirty_repos = [];
|
|
72
95
|
const missing_repos = [];
|
|
73
96
|
|
|
74
97
|
for (const name of repoNames) {
|
|
75
|
-
const resolved = resolveRepoPath(cwd, name, repos);
|
|
98
|
+
const resolved = resolveRepoPath(cwd, name, repos, useActiveContext);
|
|
76
99
|
if (!resolved) {
|
|
77
100
|
missing_repos.push(name);
|
|
78
101
|
continue;
|
|
@@ -316,13 +339,13 @@ function buildPlanningCommitBody(repoResults) {
|
|
|
316
339
|
* @param {string[]} repoNames - Repo names to check
|
|
317
340
|
* @returns {Array<{repoName: string, repoPath: string, files: string[]}>}
|
|
318
341
|
*/
|
|
319
|
-
function detectRepoChanges(cwd, repoNames) {
|
|
342
|
+
function detectRepoChanges(cwd, repoNames, useActiveContext) {
|
|
320
343
|
const parsed = parseReposMd(cwd);
|
|
321
344
|
const repos = parsed ? parsed.repos : [];
|
|
322
345
|
const changes = [];
|
|
323
346
|
|
|
324
347
|
for (const name of repoNames) {
|
|
325
|
-
const resolved = resolveRepoPath(cwd, name, repos);
|
|
348
|
+
const resolved = resolveRepoPath(cwd, name, repos, useActiveContext);
|
|
326
349
|
if (!resolved) continue;
|
|
327
350
|
if (!fs.existsSync(resolved.absPath)) continue;
|
|
328
351
|
|
|
@@ -347,7 +370,7 @@ function detectRepoChanges(cwd, repoNames) {
|
|
|
347
370
|
if (files.length > 0) {
|
|
348
371
|
changes.push({
|
|
349
372
|
repoName: name,
|
|
350
|
-
repoPath: resolved.repo.path,
|
|
373
|
+
repoPath: useActiveContext ? resolved.absPath : resolved.repo.path,
|
|
351
374
|
files,
|
|
352
375
|
});
|
|
353
376
|
}
|
|
@@ -360,7 +383,6 @@ function detectRepoChanges(cwd, repoNames) {
|
|
|
360
383
|
|
|
361
384
|
/**
|
|
362
385
|
* Create branches in each repo for multi-repo execution.
|
|
363
|
-
* Respects branching_strategy config — if 'none', skips branch creation.
|
|
364
386
|
* Reuses existing branch if it already exists.
|
|
365
387
|
* Detects branch prefix collisions and warns about potential ambiguity.
|
|
366
388
|
*
|
|
@@ -375,15 +397,12 @@ function detectRepoChanges(cwd, repoNames) {
|
|
|
375
397
|
* @param {string} cwd - Product root
|
|
376
398
|
* @param {string[]} repoNames - Repo names
|
|
377
399
|
* @param {string} branchName - Branch name (e.g., dgs/project/phase-slug)
|
|
378
|
-
* @param {
|
|
400
|
+
* @param {object} [config] - Config object (legacy parameter, kept for backward compatibility)
|
|
379
401
|
* @param {string|null} [baseBranch=null] - Base branch to checkout before creating new branch.
|
|
380
402
|
* When null/undefined, creates branch from wherever HEAD is (backwards-compatible behavior).
|
|
381
403
|
* @returns {{ created: boolean, reason?: string, error?: string, repo?: string, branches?: Array<{repo: string, branch: string, action: string}>, warnings?: Array }}
|
|
382
404
|
*/
|
|
383
405
|
function createRepoBranches(cwd, repoNames, branchName, config, baseBranch) {
|
|
384
|
-
if (config && config.branching_strategy === 'none') {
|
|
385
|
-
return { created: false, reason: 'branching_disabled' };
|
|
386
|
-
}
|
|
387
406
|
|
|
388
407
|
const parsed = parseReposMd(cwd);
|
|
389
408
|
const repos = parsed ? parsed.repos : [];
|
|
@@ -521,7 +540,7 @@ function updateRepoStatus(cwd, projectSlug, repoResults) {
|
|
|
521
540
|
let statePath;
|
|
522
541
|
const planRoot = getPlanningRoot(cwd);
|
|
523
542
|
if (projectSlug) {
|
|
524
|
-
statePath = path.join(planRoot, projectSlug, 'STATE.md');
|
|
543
|
+
statePath = path.join(planRoot, PROJECTS_DIR, projectSlug, 'STATE.md');
|
|
525
544
|
}
|
|
526
545
|
if (!statePath || !fs.existsSync(statePath)) {
|
|
527
546
|
statePath = path.join(planRoot, 'STATE.md');
|
|
@@ -622,8 +641,21 @@ function cmdCommitMultiRepo(cwd, options, raw) {
|
|
|
622
641
|
|
|
623
642
|
const warnings = [];
|
|
624
643
|
|
|
644
|
+
// Determine whether to resolve repos against active worktree
|
|
645
|
+
let useActiveContext = false;
|
|
646
|
+
try {
|
|
647
|
+
const { getLocalConfigPath } = require('./config.cjs');
|
|
648
|
+
const localPath = getLocalConfigPath(cwd);
|
|
649
|
+
if (fs.existsSync(localPath)) {
|
|
650
|
+
const localCfg = JSON.parse(fs.readFileSync(localPath, 'utf-8'));
|
|
651
|
+
if (localCfg.execution && localCfg.execution.active_context) {
|
|
652
|
+
useActiveContext = true;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
} catch { /* fall back to standard resolution */ }
|
|
656
|
+
|
|
625
657
|
// Run preflight
|
|
626
|
-
const preflight = preflightCheck(cwd, repoNames, options.phaseDir || null);
|
|
658
|
+
const preflight = preflightCheck(cwd, repoNames, options.phaseDir || null, useActiveContext);
|
|
627
659
|
|
|
628
660
|
// Missing repos is a hard error — block execution
|
|
629
661
|
if (preflight.missing_repos && preflight.missing_repos.length > 0) {
|
|
@@ -649,7 +681,7 @@ function cmdCommitMultiRepo(cwd, options, raw) {
|
|
|
649
681
|
}
|
|
650
682
|
|
|
651
683
|
// Detect changes
|
|
652
|
-
const changes = detectRepoChanges(cwd, repoNames);
|
|
684
|
+
const changes = detectRepoChanges(cwd, repoNames, useActiveContext);
|
|
653
685
|
if (changes.length === 0) {
|
|
654
686
|
output({ success: true, message: 'No changes detected', commits: [], warnings }, raw);
|
|
655
687
|
return;
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for migrateFlatStatus — flat status directory migration
|
|
3
|
+
*
|
|
4
|
+
* Phase 134 (v20.0): Migrates ideas/todos/jobs from state subdirectories
|
|
5
|
+
* to flat directories with frontmatter status fields.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
9
|
+
const assert = require('node:assert/strict');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const { createTempDir, cleanupDir, writeFile, initGitRepo } = require('./test-helpers.cjs');
|
|
14
|
+
const { migrateFlatStatus } = require('./migration.cjs');
|
|
15
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
16
|
+
|
|
17
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a temp directory with git init, returns realpathSync'd path
|
|
21
|
+
* (macOS /var -> /private/var symlink fix from Phase 118).
|
|
22
|
+
*/
|
|
23
|
+
function makeGitDir() {
|
|
24
|
+
const tmpDir = createTempDir('flat-mig-test-');
|
|
25
|
+
initGitRepo(tmpDir);
|
|
26
|
+
return fs.realpathSync(tmpDir);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a temp directory without git, returns realpathSync'd path.
|
|
31
|
+
*/
|
|
32
|
+
function makePlainDir() {
|
|
33
|
+
const tmpDir = createTempDir('flat-mig-test-');
|
|
34
|
+
return fs.realpathSync(tmpDir);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function gitCommitAll(dir, msg) {
|
|
38
|
+
const { execSync } = require('child_process');
|
|
39
|
+
execSync('git add -A', { cwd: dir, stdio: 'pipe' });
|
|
40
|
+
execSync(`git commit -m "${msg}" --allow-empty`, { cwd: dir, stdio: 'pipe' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readFile(dir, relPath) {
|
|
44
|
+
return fs.readFileSync(path.join(dir, relPath), 'utf-8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fileExists(dir, relPath) {
|
|
48
|
+
return fs.existsSync(path.join(dir, relPath));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a minimal idea file with frontmatter
|
|
53
|
+
*/
|
|
54
|
+
function makeIdea(id, title, opts = {}) {
|
|
55
|
+
const fm = [
|
|
56
|
+
'---',
|
|
57
|
+
`id: ${id}`,
|
|
58
|
+
`title: "${title}"`,
|
|
59
|
+
];
|
|
60
|
+
if (opts.status) fm.push(`status: ${opts.status}`);
|
|
61
|
+
if (opts.consolidated_into) fm.push(`consolidated_into: "${opts.consolidated_into}"`);
|
|
62
|
+
if (opts.consolidated_from) {
|
|
63
|
+
fm.push('consolidated_from:');
|
|
64
|
+
for (const src of opts.consolidated_from) {
|
|
65
|
+
fm.push(` - "${src}"`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
fm.push(`created: "2026-01-01T00:00:00Z"`);
|
|
69
|
+
fm.push(`updated: "2026-01-01T00:00:00Z"`);
|
|
70
|
+
fm.push('---');
|
|
71
|
+
fm.push('');
|
|
72
|
+
fm.push(`# ${title}`);
|
|
73
|
+
fm.push('');
|
|
74
|
+
fm.push(opts.body || 'Some idea content.');
|
|
75
|
+
if (opts.researchLog) {
|
|
76
|
+
fm.push('');
|
|
77
|
+
fm.push('## Research Log');
|
|
78
|
+
fm.push('');
|
|
79
|
+
fm.push(opts.researchLog);
|
|
80
|
+
}
|
|
81
|
+
return fm.join('\n') + '\n';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create a minimal todo file (no frontmatter — tests that frontmatter gets added)
|
|
86
|
+
*/
|
|
87
|
+
function makeTodo(title) {
|
|
88
|
+
return `# ${title}\n\nSome todo content.\n`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a minimal job file with bold-key Status header
|
|
93
|
+
*/
|
|
94
|
+
function makeJob(title, status) {
|
|
95
|
+
return `---\ntitle: "${title}"\n---\n\n# ${title}\n\n**Status:** ${status}\n\nJob details.\n`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
describe('migrateFlatStatus', () => {
|
|
101
|
+
let tmpDir;
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
if (tmpDir) cleanupDir(tmpDir);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('idempotency', () => {
|
|
108
|
+
it('returns early when flat_status_migration_done is true', () => {
|
|
109
|
+
tmpDir = makePlainDir();
|
|
110
|
+
writeFile(tmpDir, 'config.local.json', JSON.stringify({ flat_status_migration_done: true }));
|
|
111
|
+
const result = migrateFlatStatus(tmpDir);
|
|
112
|
+
assert.equal(result.migrated, false);
|
|
113
|
+
assert.equal(result.actions.length, 0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('sets flag and returns when no legacy files exist', () => {
|
|
117
|
+
tmpDir = makePlainDir();
|
|
118
|
+
writeFile(tmpDir, 'ideas/001-test.md', makeIdea(1, 'Test', { status: 'pending' }));
|
|
119
|
+
const result = migrateFlatStatus(tmpDir);
|
|
120
|
+
assert.equal(result.migrated, false);
|
|
121
|
+
assert.equal(result.actions.length, 0);
|
|
122
|
+
const config = JSON.parse(readFile(tmpDir, 'config.local.json'));
|
|
123
|
+
assert.equal(config.flat_status_migration_done, true);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('dry-run mode', () => {
|
|
128
|
+
it('returns actions without modifying files', () => {
|
|
129
|
+
tmpDir = makePlainDir();
|
|
130
|
+
writeFile(tmpDir, 'ideas/pending/001-test.md', makeIdea(1, 'Test'));
|
|
131
|
+
|
|
132
|
+
const result = migrateFlatStatus(tmpDir);
|
|
133
|
+
|
|
134
|
+
assert.equal(result.dryRun, true);
|
|
135
|
+
assert.equal(result.migrated, false);
|
|
136
|
+
assert.ok(result.actions.length > 0, 'should have actions');
|
|
137
|
+
assert.equal(result.filesMoved, 1);
|
|
138
|
+
// File should NOT have been moved
|
|
139
|
+
assert.ok(fileExists(tmpDir, 'ideas/pending/001-test.md'), 'original file should still exist');
|
|
140
|
+
assert.ok(!fileExists(tmpDir, 'ideas/001-test.md'), 'destination should not exist');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('reports correct counts for multi-subsystem migration', () => {
|
|
144
|
+
tmpDir = makePlainDir();
|
|
145
|
+
writeFile(tmpDir, 'ideas/pending/001-idea.md', makeIdea(1, 'Idea'));
|
|
146
|
+
writeFile(tmpDir, 'ideas/done/002-done-idea.md', makeIdea(2, 'Done Idea'));
|
|
147
|
+
writeFile(tmpDir, 'todos/pending/todo-1.md', makeTodo('Todo 1'));
|
|
148
|
+
writeFile(tmpDir, 'jobs/completed/job-v1.md', makeJob('Job v1', 'completed'));
|
|
149
|
+
|
|
150
|
+
const result = migrateFlatStatus(tmpDir);
|
|
151
|
+
|
|
152
|
+
assert.equal(result.dryRun, true);
|
|
153
|
+
assert.equal(result.filesMoved, 4);
|
|
154
|
+
assert.equal(result.statusFieldsAdded, 4);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('apply mode', () => {
|
|
159
|
+
it('moves idea files and adds status frontmatter', () => {
|
|
160
|
+
tmpDir = makeGitDir();
|
|
161
|
+
writeFile(tmpDir, 'ideas/pending/001-test.md', makeIdea(1, 'Test'));
|
|
162
|
+
writeFile(tmpDir, 'ideas/done/002-shipped.md', makeIdea(2, 'Shipped'));
|
|
163
|
+
gitCommitAll(tmpDir, 'add ideas');
|
|
164
|
+
|
|
165
|
+
const result = migrateFlatStatus(tmpDir, { apply: true });
|
|
166
|
+
|
|
167
|
+
assert.equal(result.migrated, true);
|
|
168
|
+
assert.equal(result.dryRun, false);
|
|
169
|
+
assert.ok(result.commitHash, 'should have commit hash');
|
|
170
|
+
|
|
171
|
+
// Files moved to flat directory
|
|
172
|
+
assert.ok(fileExists(tmpDir, 'ideas/001-test.md'), 'idea should be in flat dir');
|
|
173
|
+
assert.ok(fileExists(tmpDir, 'ideas/002-shipped.md'), 'done idea should be in flat dir');
|
|
174
|
+
assert.ok(!fileExists(tmpDir, 'ideas/pending/001-test.md'), 'original should be gone');
|
|
175
|
+
assert.ok(!fileExists(tmpDir, 'ideas/done/002-shipped.md'), 'original should be gone');
|
|
176
|
+
|
|
177
|
+
// Status frontmatter added
|
|
178
|
+
const pendingContent = readFile(tmpDir, 'ideas/001-test.md');
|
|
179
|
+
const pendingFm = extractFrontmatter(pendingContent);
|
|
180
|
+
assert.equal(pendingFm.status, 'pending');
|
|
181
|
+
|
|
182
|
+
const doneContent = readFile(tmpDir, 'ideas/002-shipped.md');
|
|
183
|
+
const doneFm = extractFrontmatter(doneContent);
|
|
184
|
+
assert.equal(doneFm.status, 'done');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('maps todo states correctly (completed -> done, resolved -> done)', () => {
|
|
188
|
+
tmpDir = makeGitDir();
|
|
189
|
+
writeFile(tmpDir, 'todos/pending/todo-1.md', makeTodo('Pending Todo'));
|
|
190
|
+
writeFile(tmpDir, 'todos/completed/todo-2.md', makeTodo('Completed Todo'));
|
|
191
|
+
writeFile(tmpDir, 'todos/done/todo-3.md', makeTodo('Done Todo'));
|
|
192
|
+
writeFile(tmpDir, 'todos/resolved/todo-4.md', makeTodo('Resolved Todo'));
|
|
193
|
+
gitCommitAll(tmpDir, 'add todos');
|
|
194
|
+
|
|
195
|
+
const result = migrateFlatStatus(tmpDir, { apply: true });
|
|
196
|
+
|
|
197
|
+
assert.equal(result.migrated, true);
|
|
198
|
+
|
|
199
|
+
// All files in flat dir
|
|
200
|
+
assert.ok(fileExists(tmpDir, 'todos/todo-1.md'));
|
|
201
|
+
assert.ok(fileExists(tmpDir, 'todos/todo-2.md'));
|
|
202
|
+
assert.ok(fileExists(tmpDir, 'todos/todo-3.md'));
|
|
203
|
+
assert.ok(fileExists(tmpDir, 'todos/todo-4.md'));
|
|
204
|
+
|
|
205
|
+
// Status mappings correct
|
|
206
|
+
const todo1 = extractFrontmatter(readFile(tmpDir, 'todos/todo-1.md'));
|
|
207
|
+
assert.equal(todo1.status, 'pending');
|
|
208
|
+
const todo2 = extractFrontmatter(readFile(tmpDir, 'todos/todo-2.md'));
|
|
209
|
+
assert.equal(todo2.status, 'done');
|
|
210
|
+
const todo3 = extractFrontmatter(readFile(tmpDir, 'todos/todo-3.md'));
|
|
211
|
+
assert.equal(todo3.status, 'done');
|
|
212
|
+
const todo4 = extractFrontmatter(readFile(tmpDir, 'todos/todo-4.md'));
|
|
213
|
+
assert.equal(todo4.status, 'done');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('flattens research docs and updates path references in ideas', () => {
|
|
217
|
+
tmpDir = makeGitDir();
|
|
218
|
+
const ideaWithResearch = makeIdea(5, 'Research Idea', {
|
|
219
|
+
researchLog: '**Document:** docs/ideas/pending/research-idea-research.md\n**Date:** 2026-01-01'
|
|
220
|
+
});
|
|
221
|
+
writeFile(tmpDir, 'ideas/pending/005-research-idea.md', ideaWithResearch);
|
|
222
|
+
writeFile(tmpDir, 'docs/ideas/pending/research-idea-research.md', '# Research\n\nFindings here.\n');
|
|
223
|
+
gitCommitAll(tmpDir, 'add research');
|
|
224
|
+
|
|
225
|
+
const result = migrateFlatStatus(tmpDir, { apply: true });
|
|
226
|
+
|
|
227
|
+
assert.equal(result.migrated, true);
|
|
228
|
+
|
|
229
|
+
// Research doc flattened
|
|
230
|
+
assert.ok(fileExists(tmpDir, 'docs/ideas/research-idea-research.md'), 'research doc in flat dir');
|
|
231
|
+
assert.ok(!fileExists(tmpDir, 'docs/ideas/pending/research-idea-research.md'), 'research doc removed from state dir');
|
|
232
|
+
|
|
233
|
+
// Path reference updated in idea
|
|
234
|
+
const ideaContent = readFile(tmpDir, 'ideas/005-research-idea.md');
|
|
235
|
+
assert.ok(ideaContent.includes('docs/ideas/research-idea-research.md'), 'path should be flat');
|
|
236
|
+
assert.ok(!ideaContent.includes('docs/ideas/pending/research-idea-research.md'), 'state path should be gone');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('normalizes consolidated_into and consolidated_from paths', () => {
|
|
240
|
+
tmpDir = makeGitDir();
|
|
241
|
+
const consolidatedIdea = makeIdea(3, 'Old Idea', {
|
|
242
|
+
consolidated_into: 'ideas/pending/010-merged.md'
|
|
243
|
+
});
|
|
244
|
+
const mergedIdea = makeIdea(10, 'Merged', {
|
|
245
|
+
consolidated_from: ['ideas/pending/003-old-idea.md', 'ideas/done/004-another.md']
|
|
246
|
+
});
|
|
247
|
+
writeFile(tmpDir, 'ideas/consolidated/003-old-idea.md', consolidatedIdea);
|
|
248
|
+
writeFile(tmpDir, 'ideas/pending/010-merged.md', mergedIdea);
|
|
249
|
+
gitCommitAll(tmpDir, 'add consolidated');
|
|
250
|
+
|
|
251
|
+
const result = migrateFlatStatus(tmpDir, { apply: true });
|
|
252
|
+
|
|
253
|
+
assert.equal(result.migrated, true);
|
|
254
|
+
assert.ok(result.pathsNormalized > 0, 'should have normalized paths');
|
|
255
|
+
|
|
256
|
+
// Check consolidated_into normalized
|
|
257
|
+
const oldContent = readFile(tmpDir, 'ideas/003-old-idea.md');
|
|
258
|
+
assert.ok(!oldContent.includes('ideas/pending/'), 'should not have state dir in path');
|
|
259
|
+
assert.ok(oldContent.includes('ideas/010-merged.md'), 'should have flat path');
|
|
260
|
+
|
|
261
|
+
// Check consolidated_from normalized
|
|
262
|
+
const mergedContent = readFile(tmpDir, 'ideas/010-merged.md');
|
|
263
|
+
assert.ok(!mergedContent.includes('ideas/pending/003'), 'should not have state dir');
|
|
264
|
+
assert.ok(!mergedContent.includes('ideas/done/004'), 'should not have state dir');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('validates file count (no data loss)', () => {
|
|
268
|
+
tmpDir = makeGitDir();
|
|
269
|
+
writeFile(tmpDir, 'ideas/pending/001-a.md', makeIdea(1, 'A'));
|
|
270
|
+
writeFile(tmpDir, 'ideas/done/002-b.md', makeIdea(2, 'B'));
|
|
271
|
+
writeFile(tmpDir, 'ideas/rejected/003-c.md', makeIdea(3, 'C'));
|
|
272
|
+
gitCommitAll(tmpDir, 'add ideas');
|
|
273
|
+
|
|
274
|
+
const result = migrateFlatStatus(tmpDir, { apply: true });
|
|
275
|
+
|
|
276
|
+
assert.equal(result.migrated, true);
|
|
277
|
+
assert.equal(result.filesMoved, 3);
|
|
278
|
+
|
|
279
|
+
// Count files in flat dir (should be 3 ideas)
|
|
280
|
+
const flatFiles = fs.readdirSync(path.join(tmpDir, 'ideas')).filter(f => f.endsWith('.md'));
|
|
281
|
+
assert.equal(flatFiles.length, 3, 'all 3 ideas should be in flat dir');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('handles jobs with dual-write status (frontmatter + bold-key header)', () => {
|
|
285
|
+
tmpDir = makeGitDir();
|
|
286
|
+
writeFile(tmpDir, 'jobs/completed/milestone-v5.md', makeJob('v5 Build', 'completed'));
|
|
287
|
+
writeFile(tmpDir, 'jobs/in-progress/milestone-v6.md', makeJob('v6 Build', 'in-progress'));
|
|
288
|
+
gitCommitAll(tmpDir, 'add jobs');
|
|
289
|
+
|
|
290
|
+
const result = migrateFlatStatus(tmpDir, { apply: true });
|
|
291
|
+
|
|
292
|
+
assert.equal(result.migrated, true);
|
|
293
|
+
|
|
294
|
+
// Check frontmatter status
|
|
295
|
+
const v5Content = readFile(tmpDir, 'jobs/milestone-v5.md');
|
|
296
|
+
const v5Fm = extractFrontmatter(v5Content);
|
|
297
|
+
assert.equal(v5Fm.status, 'completed');
|
|
298
|
+
|
|
299
|
+
// Check bold-key header still present
|
|
300
|
+
assert.ok(v5Content.includes('**Status:** completed'), 'bold-key header should be updated');
|
|
301
|
+
|
|
302
|
+
const v6Content = readFile(tmpDir, 'jobs/milestone-v6.md');
|
|
303
|
+
const v6Fm = extractFrontmatter(v6Content);
|
|
304
|
+
assert.equal(v6Fm.status, 'in-progress');
|
|
305
|
+
assert.ok(v6Content.includes('**Status:** in-progress'), 'bold-key header should be updated');
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('realistic project fixtures', () => {
|
|
310
|
+
it('handles fc-like project (done ideas, research docs, pending todos, completed jobs)', () => {
|
|
311
|
+
tmpDir = makeGitDir();
|
|
312
|
+
|
|
313
|
+
// fc-like: 7 done ideas, 5 pending ideas, 11 pending todos, 1 resolved todo, 10 completed jobs, 1 research doc
|
|
314
|
+
for (let i = 1; i <= 7; i++) {
|
|
315
|
+
writeFile(tmpDir, `ideas/done/00${i}-done-idea-${i}.md`, makeIdea(i, `Done Idea ${i}`));
|
|
316
|
+
}
|
|
317
|
+
for (let i = 8; i <= 12; i++) {
|
|
318
|
+
writeFile(tmpDir, `ideas/pending/0${i}-pending-idea-${i}.md`, makeIdea(i, `Pending Idea ${i}`));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
for (let i = 1; i <= 11; i++) {
|
|
322
|
+
writeFile(tmpDir, `todos/pending/todo-${i}.md`, makeTodo(`Todo ${i}`));
|
|
323
|
+
}
|
|
324
|
+
writeFile(tmpDir, 'todos/resolved/todo-resolved.md', makeTodo('Resolved Todo'));
|
|
325
|
+
|
|
326
|
+
for (let i = 1; i <= 10; i++) {
|
|
327
|
+
writeFile(tmpDir, `jobs/completed/milestone-v${i}.md`, makeJob(`v${i} Build`, 'completed'));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
writeFile(tmpDir, 'docs/ideas/pending/research-doc.md', '# Research\n\nContent.\n');
|
|
331
|
+
|
|
332
|
+
gitCommitAll(tmpDir, 'init');
|
|
333
|
+
|
|
334
|
+
const result = migrateFlatStatus(tmpDir, { apply: true });
|
|
335
|
+
|
|
336
|
+
assert.equal(result.migrated, true);
|
|
337
|
+
// 7 done + 5 pending ideas + 12 todos + 10 jobs + 1 research = 35
|
|
338
|
+
assert.equal(result.filesMoved, 35);
|
|
339
|
+
|
|
340
|
+
// Spot check: done idea has status field
|
|
341
|
+
const doneIdea = readFile(tmpDir, 'ideas/001-done-idea-1.md');
|
|
342
|
+
assert.ok(doneIdea.includes('status: done'), 'done idea should have status: done');
|
|
343
|
+
|
|
344
|
+
// Resolved todo mapped to done
|
|
345
|
+
const resolvedTodo = readFile(tmpDir, 'todos/todo-resolved.md');
|
|
346
|
+
const resolvedFm = extractFrontmatter(resolvedTodo);
|
|
347
|
+
assert.equal(resolvedFm.status, 'done');
|
|
348
|
+
|
|
349
|
+
// Research doc flattened
|
|
350
|
+
assert.ok(fileExists(tmpDir, 'docs/ideas/research-doc.md'));
|
|
351
|
+
assert.ok(!fileExists(tmpDir, 'docs/ideas/pending/research-doc.md'));
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('handles gsd-like project (mixed todo states, in-progress job)', () => {
|
|
355
|
+
tmpDir = makeGitDir();
|
|
356
|
+
|
|
357
|
+
// gsd-like: 4 done ideas, 5 pending ideas, mixed todos, 1 in-progress job
|
|
358
|
+
for (let i = 1; i <= 4; i++) {
|
|
359
|
+
writeFile(tmpDir, `ideas/done/00${i}-done-${i}.md`, makeIdea(i, `Done ${i}`));
|
|
360
|
+
}
|
|
361
|
+
for (let i = 5; i <= 9; i++) {
|
|
362
|
+
writeFile(tmpDir, `ideas/pending/00${i}-pending-${i}.md`, makeIdea(i, `Pending ${i}`));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (let i = 1; i <= 17; i++) {
|
|
366
|
+
writeFile(tmpDir, `todos/pending/todo-p${i}.md`, makeTodo(`Pending ${i}`));
|
|
367
|
+
}
|
|
368
|
+
for (let i = 1; i <= 4; i++) {
|
|
369
|
+
writeFile(tmpDir, `todos/completed/todo-c${i}.md`, makeTodo(`Completed ${i}`));
|
|
370
|
+
}
|
|
371
|
+
for (let i = 1; i <= 9; i++) {
|
|
372
|
+
writeFile(tmpDir, `todos/done/todo-d${i}.md`, makeTodo(`Done ${i}`));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
writeFile(tmpDir, 'jobs/in-progress/milestone-v20.md', makeJob('v20 Build', 'in-progress'));
|
|
376
|
+
writeFile(tmpDir, 'jobs/completed/milestone-v19.md', makeJob('v19 Build', 'completed'));
|
|
377
|
+
|
|
378
|
+
gitCommitAll(tmpDir, 'init');
|
|
379
|
+
|
|
380
|
+
const result = migrateFlatStatus(tmpDir, { apply: true });
|
|
381
|
+
|
|
382
|
+
assert.equal(result.migrated, true);
|
|
383
|
+
// 4 done + 5 pending ideas + 17 pending + 4 completed + 9 done todos + 2 jobs = 41
|
|
384
|
+
assert.equal(result.filesMoved, 41);
|
|
385
|
+
|
|
386
|
+
// All todos in flat dir with correct status
|
|
387
|
+
const completedTodo = readFile(tmpDir, 'todos/todo-c1.md');
|
|
388
|
+
const completedFm = extractFrontmatter(completedTodo);
|
|
389
|
+
assert.equal(completedFm.status, 'done', 'completed -> done');
|
|
390
|
+
|
|
391
|
+
const doneTodo = readFile(tmpDir, 'todos/todo-d1.md');
|
|
392
|
+
const doneFm = extractFrontmatter(doneTodo);
|
|
393
|
+
assert.equal(doneFm.status, 'done', 'done -> done');
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
});
|