@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
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
|
-
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, MODEL_PROFILES, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
|
|
8
|
-
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
7
|
+
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getProjectRoot, resolveModelInternal, MODEL_PROFILES, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
|
|
8
|
+
const { extractFrontmatter, spliceFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
|
9
9
|
const { getPlanningRoot } = require('./paths.cjs');
|
|
10
10
|
|
|
11
|
+
const TODO_STATUSES = ['pending', 'done'];
|
|
12
|
+
|
|
11
13
|
function cmdGenerateSlug(text, raw) {
|
|
12
14
|
if (!text) {
|
|
13
15
|
error('text required for slug generation');
|
|
@@ -43,15 +45,62 @@ function cmdCurrentTimestamp(format, raw) {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
function cmdListTodos(cwd, area, raw) {
|
|
46
|
-
const pendingDir = path.join(getPlanningRoot(cwd), 'todos', 'pending');
|
|
47
|
-
|
|
48
48
|
let count = 0;
|
|
49
49
|
const todos = [];
|
|
50
|
+
const seenFiles = new Set();
|
|
50
51
|
|
|
52
|
+
// Flat-first scan: check todos/ root directory
|
|
53
|
+
const flatDir = path.join(getPlanningRoot(cwd), 'todos');
|
|
51
54
|
try {
|
|
52
|
-
const
|
|
55
|
+
const flatFiles = fs.readdirSync(flatDir).filter(f => f.endsWith('.md') && !fs.statSync(path.join(flatDir, f)).isDirectory());
|
|
56
|
+
for (const file of flatFiles) {
|
|
57
|
+
try {
|
|
58
|
+
const content = fs.readFileSync(path.join(flatDir, file), 'utf-8');
|
|
59
|
+
|
|
60
|
+
// Parse frontmatter status (if present)
|
|
61
|
+
let status = null;
|
|
62
|
+
const fmMatch = content.match(/^---\n([\s\S]+?)\n---/);
|
|
63
|
+
if (fmMatch) {
|
|
64
|
+
const statusMatch = fmMatch[1].match(/^status:\s*(.+)$/m);
|
|
65
|
+
if (statusMatch) status = statusMatch[1].trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Only list pending todos (default behavior)
|
|
69
|
+
if (status && status !== 'pending') continue;
|
|
70
|
+
|
|
71
|
+
const createdMatch = content.match(/^created:\s*(.+)$/m);
|
|
72
|
+
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
|
73
|
+
const areaMatch = content.match(/^area:\s*(.+)$/m);
|
|
74
|
+
|
|
75
|
+
const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
|
|
76
|
+
if (area && todoArea !== area) continue;
|
|
53
77
|
|
|
78
|
+
count++;
|
|
79
|
+
seenFiles.add(file);
|
|
80
|
+
todos.push({
|
|
81
|
+
file,
|
|
82
|
+
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
|
83
|
+
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
84
|
+
area: todoArea,
|
|
85
|
+
status: status || 'pending',
|
|
86
|
+
path: toPosixPath(path.join(path.relative(cwd, getPlanningRoot(cwd)) || '.', 'todos', file)),
|
|
87
|
+
});
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Flat directory may not exist
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Legacy fallback: check todos/pending/
|
|
95
|
+
const pendingDir = path.join(getPlanningRoot(cwd), 'todos', 'pending');
|
|
96
|
+
try {
|
|
97
|
+
const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
|
|
54
98
|
for (const file of files) {
|
|
99
|
+
if (seenFiles.has(file)) continue; // Skip if already found in flat directory
|
|
100
|
+
|
|
101
|
+
// Legacy fallback warning
|
|
102
|
+
process.stderr.write(`[DGS] Warning: todo '${file}' found in legacy pending/ directory. Run migration to flatten.\n`);
|
|
103
|
+
|
|
55
104
|
try {
|
|
56
105
|
const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
|
|
57
106
|
const createdMatch = content.match(/^created:\s*(.+)$/m);
|
|
@@ -59,8 +108,6 @@ function cmdListTodos(cwd, area, raw) {
|
|
|
59
108
|
const areaMatch = content.match(/^area:\s*(.+)$/m);
|
|
60
109
|
|
|
61
110
|
const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
|
|
62
|
-
|
|
63
|
-
// Apply area filter if specified
|
|
64
111
|
if (area && todoArea !== area) continue;
|
|
65
112
|
|
|
66
113
|
count++;
|
|
@@ -69,6 +116,7 @@ function cmdListTodos(cwd, area, raw) {
|
|
|
69
116
|
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
|
70
117
|
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
71
118
|
area: todoArea,
|
|
119
|
+
status: 'pending',
|
|
72
120
|
path: toPosixPath(path.join(path.relative(cwd, getPlanningRoot(cwd)) || '.', 'todos', 'pending', file)),
|
|
73
121
|
});
|
|
74
122
|
} catch {}
|
|
@@ -98,7 +146,13 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
|
|
|
98
146
|
}
|
|
99
147
|
|
|
100
148
|
function cmdHistoryDigest(cwd, raw) {
|
|
101
|
-
|
|
149
|
+
let phasesDir;
|
|
150
|
+
try {
|
|
151
|
+
const projectRoot = getProjectRoot(cwd);
|
|
152
|
+
phasesDir = path.join(cwd, projectRoot, 'phases');
|
|
153
|
+
} catch {
|
|
154
|
+
phasesDir = path.join(getPlanningRoot(cwd), 'phases');
|
|
155
|
+
}
|
|
102
156
|
const digest = { phases: {}, decisions: [], tech_stack: new Set() };
|
|
103
157
|
|
|
104
158
|
// Collect all phase directories: archived + current
|
|
@@ -214,7 +268,22 @@ function cmdResolveModel(cwd, agentType, raw) {
|
|
|
214
268
|
output(result, raw, model);
|
|
215
269
|
}
|
|
216
270
|
|
|
217
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Commit staged/unstaged files with an optional push orchestration.
|
|
273
|
+
*
|
|
274
|
+
* @param {string} cwd - Planning-root cwd. Config (commit_docs, sync_push,
|
|
275
|
+
* current_project, worktree map) is always loaded from this directory.
|
|
276
|
+
* @param {string} message - Commit message (required unless amend=true).
|
|
277
|
+
* @param {string[]} files - Paths to stage. Resolved relative to repoCwd
|
|
278
|
+
* (or cwd if repoCwd is not set). Defaults to ['.'].
|
|
279
|
+
* @param {boolean} raw - Emit raw (non-JSON) output.
|
|
280
|
+
* @param {boolean} amend - Run `git commit --amend --no-edit` instead.
|
|
281
|
+
* @param {boolean} push - Orchestrate push via pushAll() when sync_push=auto.
|
|
282
|
+
* @param {string} [repoCwd] - Optional absolute path where git operations run.
|
|
283
|
+
* Config loading still uses cwd. Used by fast-path in milestone-context to
|
|
284
|
+
* commit in a worktree while loading config from the planning root.
|
|
285
|
+
*/
|
|
286
|
+
function cmdCommit(cwd, message, files, raw, amend, push, repoCwd) {
|
|
218
287
|
if (!message && !amend) {
|
|
219
288
|
error('commit message required');
|
|
220
289
|
}
|
|
@@ -228,15 +297,20 @@ function cmdCommit(cwd, message, files, raw, amend, push) {
|
|
|
228
297
|
return;
|
|
229
298
|
}
|
|
230
299
|
|
|
300
|
+
// Resolve git-operation cwd: use repoCwd when provided, otherwise cwd.
|
|
301
|
+
// Config is always loaded from cwd (planning root); only git exec targets
|
|
302
|
+
// the worktree filesystem.
|
|
303
|
+
const gitCwd = repoCwd || cwd;
|
|
304
|
+
|
|
231
305
|
// Stage files
|
|
232
306
|
const filesToStage = files && files.length > 0 ? files : ['.'];
|
|
233
307
|
for (const file of filesToStage) {
|
|
234
|
-
execGit(
|
|
308
|
+
execGit(gitCwd, ['add', file]);
|
|
235
309
|
}
|
|
236
310
|
|
|
237
311
|
// Commit
|
|
238
312
|
const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', message];
|
|
239
|
-
const commitResult = execGit(
|
|
313
|
+
const commitResult = execGit(gitCwd, commitArgs);
|
|
240
314
|
if (commitResult.exitCode !== 0) {
|
|
241
315
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
242
316
|
const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
|
|
@@ -249,7 +323,7 @@ function cmdCommit(cwd, message, files, raw, amend, push) {
|
|
|
249
323
|
}
|
|
250
324
|
|
|
251
325
|
// Get short hash
|
|
252
|
-
const hashResult = execGit(
|
|
326
|
+
const hashResult = execGit(gitCwd, ['rev-parse', '--short', 'HEAD']);
|
|
253
327
|
const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
|
|
254
328
|
const result = { committed: true, hash, reason: 'committed' };
|
|
255
329
|
|
|
@@ -276,6 +350,148 @@ function cmdCommit(cwd, message, files, raw, amend, push) {
|
|
|
276
350
|
output(result, raw, hash || 'committed');
|
|
277
351
|
}
|
|
278
352
|
|
|
353
|
+
/**
|
|
354
|
+
* CLI: `plan finalize <phase> <plan> [--plan-name <name>] [--push]`.
|
|
355
|
+
*
|
|
356
|
+
* Runs state update-progress + roadmap update-plan-progress + requirements
|
|
357
|
+
* mark-complete AND commits PLAN.md + SUMMARY.md + tracking files in a single
|
|
358
|
+
* atomic call. Requirement IDs are auto-extracted from PLAN.md frontmatter.
|
|
359
|
+
* Plan name resolution order: --plan-name flag > PLAN.md frontmatter plan_name
|
|
360
|
+
* > "execution" fallback.
|
|
361
|
+
*
|
|
362
|
+
* Does NOT call cmdCommit or the existing cmdStateUpdateProgress /
|
|
363
|
+
* cmdRoadmapUpdatePlanProgress / cmdRequirementsMarkComplete CLIs (all call
|
|
364
|
+
* output()/exit). Uses their *Internal helpers + execGit directly.
|
|
365
|
+
*/
|
|
366
|
+
function cmdPlanFinalize(cwd, phaseNum, planNum, options, raw) {
|
|
367
|
+
if (!phaseNum) error('phase number required for plan finalize');
|
|
368
|
+
if (!planNum) error('plan number required for plan finalize');
|
|
369
|
+
|
|
370
|
+
const { stateUpdateProgressInternal } = require('./state.cjs');
|
|
371
|
+
const { roadmapUpdatePlanProgressInternal } = require('./roadmap.cjs');
|
|
372
|
+
const { requirementsMarkCompleteInternal } = require('./milestone.cjs');
|
|
373
|
+
|
|
374
|
+
// Resolve phase dir via findPhaseInternal (returns phases/NN-name relative path)
|
|
375
|
+
const phaseInfo = findPhaseInternal(cwd, phaseNum);
|
|
376
|
+
if (!phaseInfo) error(`Phase ${phaseNum} not found`);
|
|
377
|
+
const phaseDir = path.join(cwd, phaseInfo.directory);
|
|
378
|
+
|
|
379
|
+
// Locate PLAN.md + SUMMARY.md within the phase dir using `${phaseNum}-${planNum}` prefix
|
|
380
|
+
// phaseInfo.phase_number is the canonical (padded) number e.g. "01".
|
|
381
|
+
const prefix = `${phaseInfo.phase_number || phaseNum}-${planNum}`;
|
|
382
|
+
let planPath = null;
|
|
383
|
+
let summaryPath = null;
|
|
384
|
+
try {
|
|
385
|
+
const entries = fs.readdirSync(phaseDir);
|
|
386
|
+
const planFile = entries.find(f =>
|
|
387
|
+
f.toLowerCase().startsWith(prefix.toLowerCase()) && /-PLAN\.md$/i.test(f)
|
|
388
|
+
);
|
|
389
|
+
const summaryFile = entries.find(f =>
|
|
390
|
+
f.toLowerCase().startsWith(prefix.toLowerCase()) && /-SUMMARY\.md$/i.test(f)
|
|
391
|
+
);
|
|
392
|
+
if (planFile) planPath = path.join(phaseDir, planFile);
|
|
393
|
+
if (summaryFile) summaryPath = path.join(phaseDir, summaryFile);
|
|
394
|
+
} catch { /* ignore */ }
|
|
395
|
+
|
|
396
|
+
// Extract plan_name + requirements from PLAN.md frontmatter
|
|
397
|
+
let planName = (options && options.planName) || null;
|
|
398
|
+
let reqIds = [];
|
|
399
|
+
if (planPath && fs.existsSync(planPath)) {
|
|
400
|
+
try {
|
|
401
|
+
const content = fs.readFileSync(planPath, 'utf-8');
|
|
402
|
+
const fm = extractFrontmatter(content);
|
|
403
|
+
if (fm) {
|
|
404
|
+
if (!planName && fm.plan_name) planName = String(fm.plan_name);
|
|
405
|
+
if (fm.requirements) {
|
|
406
|
+
reqIds = Array.isArray(fm.requirements)
|
|
407
|
+
? fm.requirements.map(String).map(s => s.trim()).filter(Boolean)
|
|
408
|
+
: String(fm.requirements).replace(/[\[\]]/g, '').split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} catch { /* ignore */ }
|
|
412
|
+
}
|
|
413
|
+
if (!planName) planName = 'execution';
|
|
414
|
+
|
|
415
|
+
// Resolve tracking file paths (uses same project-root logic as phase finalize)
|
|
416
|
+
let projRoot;
|
|
417
|
+
try { projRoot = require('./core.cjs').getProjectRoot(cwd); } catch { projRoot = '.'; }
|
|
418
|
+
const roadmapPath = path.join(cwd, projRoot, 'ROADMAP.md');
|
|
419
|
+
const statePath = path.join(cwd, projRoot, 'STATE.md');
|
|
420
|
+
const requirementsPath = path.join(cwd, projRoot, 'REQUIREMENTS.md');
|
|
421
|
+
|
|
422
|
+
// Run internal updates (each returns { result, ... } without exit)
|
|
423
|
+
const stateUpdate = stateUpdateProgressInternal(cwd);
|
|
424
|
+
const roadmapUpdate = roadmapUpdatePlanProgressInternal(cwd, phaseNum);
|
|
425
|
+
const reqUpdate = reqIds.length > 0
|
|
426
|
+
? requirementsMarkCompleteInternal(cwd, reqIds)
|
|
427
|
+
: { result: { updated: false, marked_complete: [], not_found: [], total: 0 }, reqPath: requirementsPath };
|
|
428
|
+
|
|
429
|
+
// Stage all files that exist on disk
|
|
430
|
+
const candidates = [planPath, summaryPath, statePath, roadmapPath, requirementsPath];
|
|
431
|
+
const filesToStage = candidates
|
|
432
|
+
.filter(p => p && fs.existsSync(p))
|
|
433
|
+
.map(p => path.relative(cwd, p));
|
|
434
|
+
|
|
435
|
+
const config = loadConfig(cwd);
|
|
436
|
+
const result = {
|
|
437
|
+
phase: phaseNum,
|
|
438
|
+
plan: planNum,
|
|
439
|
+
plan_name: planName,
|
|
440
|
+
state: stateUpdate.result,
|
|
441
|
+
roadmap: roadmapUpdate.result,
|
|
442
|
+
requirements: reqUpdate.result,
|
|
443
|
+
files_committed: [],
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
if (!config.commit_docs) {
|
|
447
|
+
result.committed = false;
|
|
448
|
+
result.commit_reason = 'skipped_commit_docs_false';
|
|
449
|
+
output(result, raw);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const message = `docs(${phaseNum}-${planNum}): complete ${planName} plan`;
|
|
454
|
+
for (const f of filesToStage) {
|
|
455
|
+
execGit(cwd, ['add', f]);
|
|
456
|
+
}
|
|
457
|
+
const commitResult = execGit(cwd, ['commit', '-m', message]);
|
|
458
|
+
if (commitResult.exitCode !== 0) {
|
|
459
|
+
const nothing =
|
|
460
|
+
commitResult.stdout.includes('nothing to commit') ||
|
|
461
|
+
commitResult.stderr.includes('nothing to commit');
|
|
462
|
+
result.committed = false;
|
|
463
|
+
result.commit_reason = nothing ? 'nothing_to_commit' : 'commit_failed';
|
|
464
|
+
if (!nothing) result.commit_error = commitResult.stderr;
|
|
465
|
+
output(result, raw);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const hashResult = execGit(cwd, ['rev-parse', '--short', 'HEAD']);
|
|
469
|
+
result.committed = true;
|
|
470
|
+
result.hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
|
|
471
|
+
result.commit_reason = 'committed';
|
|
472
|
+
result.files_committed = filesToStage;
|
|
473
|
+
|
|
474
|
+
// Optional push (same semantics as cmdCommit / cmdPhaseFinalize)
|
|
475
|
+
if (options && options.push) {
|
|
476
|
+
const syncPush = config.sync_push || 'off';
|
|
477
|
+
if (syncPush === 'auto') {
|
|
478
|
+
try {
|
|
479
|
+
const { pushAll } = require('./sync.cjs');
|
|
480
|
+
const pushRes = pushAll(cwd, { force: true });
|
|
481
|
+
result.pushed = pushRes.ok;
|
|
482
|
+
result.push_result = pushRes;
|
|
483
|
+
} catch (err) {
|
|
484
|
+
result.pushed = false;
|
|
485
|
+
result.push_result = { ok: false, error: err.message };
|
|
486
|
+
}
|
|
487
|
+
} else if (syncPush === 'prompt') {
|
|
488
|
+
result.needs_push = true;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
output(result, raw);
|
|
493
|
+
}
|
|
494
|
+
|
|
279
495
|
function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
|
|
280
496
|
if (!summaryPath) {
|
|
281
497
|
error('summary-path required for summary-extract');
|
|
@@ -395,8 +611,17 @@ async function cmdWebsearch(query, options, raw) {
|
|
|
395
611
|
}
|
|
396
612
|
|
|
397
613
|
function cmdProgressRender(cwd, format, raw) {
|
|
398
|
-
|
|
399
|
-
|
|
614
|
+
let phasesDir, roadmapPath;
|
|
615
|
+
try {
|
|
616
|
+
const projectRoot = getProjectRoot(cwd);
|
|
617
|
+
const projectAbs = path.join(cwd, projectRoot);
|
|
618
|
+
phasesDir = path.join(projectAbs, 'phases');
|
|
619
|
+
roadmapPath = path.join(projectAbs, 'ROADMAP.md');
|
|
620
|
+
} catch {
|
|
621
|
+
const planRoot = getPlanningRoot(cwd);
|
|
622
|
+
phasesDir = path.join(planRoot, 'phases');
|
|
623
|
+
roadmapPath = path.join(planRoot, 'ROADMAP.md');
|
|
624
|
+
}
|
|
400
625
|
const milestone = getMilestoneInfo(cwd);
|
|
401
626
|
|
|
402
627
|
const phases = [];
|
|
@@ -462,31 +687,88 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
462
687
|
}
|
|
463
688
|
}
|
|
464
689
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
690
|
+
/**
|
|
691
|
+
* Set a todo's frontmatter status field (internal helper — throws on error).
|
|
692
|
+
*
|
|
693
|
+
* If the todo file has no YAML frontmatter, wraps existing content with a
|
|
694
|
+
* frontmatter block containing status (and completed_date if status is 'done').
|
|
695
|
+
*
|
|
696
|
+
* @param {string} cwd - Working directory
|
|
697
|
+
* @param {string} filename - Todo filename
|
|
698
|
+
* @param {string} status - Target status (must be in TODO_STATUSES)
|
|
699
|
+
* @returns {{ filename: string, previous_status: string, status: string, completed_date?: string }}
|
|
700
|
+
* @throws {Error} If status is invalid or todo not found
|
|
701
|
+
*/
|
|
702
|
+
function setTodoStatus(cwd, filename, status) {
|
|
703
|
+
if (!TODO_STATUSES.includes(status)) {
|
|
704
|
+
throw new Error(`Invalid status: ${status}. Allowed: ${TODO_STATUSES.join(', ')}`);
|
|
468
705
|
}
|
|
469
706
|
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
707
|
+
// Search in both pending and completed directories
|
|
708
|
+
const planningRoot = getPlanningRoot(cwd);
|
|
709
|
+
let todoPath = null;
|
|
710
|
+
let previousStatus = 'pending';
|
|
473
711
|
|
|
474
|
-
|
|
475
|
-
|
|
712
|
+
for (const dir of ['pending', 'completed']) {
|
|
713
|
+
const candidate = path.join(planningRoot, 'todos', dir, filename);
|
|
714
|
+
if (fs.existsSync(candidate)) {
|
|
715
|
+
todoPath = candidate;
|
|
716
|
+
previousStatus = dir === 'completed' ? 'done' : 'pending';
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
// Also check flat todos/ directory (future layout)
|
|
721
|
+
if (!todoPath) {
|
|
722
|
+
const flat = path.join(planningRoot, 'todos', filename);
|
|
723
|
+
if (fs.existsSync(flat)) {
|
|
724
|
+
todoPath = flat;
|
|
725
|
+
const content = fs.readFileSync(flat, 'utf-8');
|
|
726
|
+
const fm = extractFrontmatter(content);
|
|
727
|
+
previousStatus = fm.status || 'pending';
|
|
728
|
+
}
|
|
476
729
|
}
|
|
477
730
|
|
|
478
|
-
|
|
479
|
-
|
|
731
|
+
if (!todoPath) {
|
|
732
|
+
throw new Error(`Todo not found: ${filename}`);
|
|
733
|
+
}
|
|
480
734
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
735
|
+
let content = fs.readFileSync(todoPath, 'utf-8');
|
|
736
|
+
const fm = extractFrontmatter(content);
|
|
737
|
+
|
|
738
|
+
// Build frontmatter fields
|
|
739
|
+
fm.status = status;
|
|
740
|
+
if (status === 'done') {
|
|
741
|
+
fm.completed_date = new Date().toISOString().split('T')[0];
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// If file has frontmatter block, splice it; otherwise add one
|
|
745
|
+
if (content.match(/^---\n/)) {
|
|
746
|
+
content = spliceFrontmatter(content, fm);
|
|
747
|
+
} else {
|
|
748
|
+
// Strip bare "completed: YYYY-MM-DD" line if present (legacy format)
|
|
749
|
+
content = content.replace(/^completed:\s*\d{4}-\d{2}-\d{2}\n/, '');
|
|
750
|
+
const yamlStr = reconstructFrontmatter(fm);
|
|
751
|
+
content = `---\n${yamlStr}\n---\n${content}`;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
fs.writeFileSync(todoPath, content, 'utf-8');
|
|
755
|
+
|
|
756
|
+
const result = { filename, previous_status: previousStatus, status };
|
|
757
|
+
if (fm.completed_date) result.completed_date = fm.completed_date;
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function cmdTodoComplete(cwd, filename, raw) {
|
|
762
|
+
if (!filename) {
|
|
763
|
+
error('filename required for todo complete');
|
|
764
|
+
}
|
|
485
765
|
|
|
486
|
-
|
|
487
|
-
|
|
766
|
+
// Use setTodoStatus to edit frontmatter status to 'done'
|
|
767
|
+
// setTodoStatus handles finding the file in flat or legacy directories
|
|
768
|
+
// and automatically sets completed_date when status is 'done'
|
|
769
|
+
const result = setTodoStatus(cwd, filename, 'done');
|
|
488
770
|
|
|
489
|
-
output({ completed: true, file: filename, date:
|
|
771
|
+
output({ completed: true, file: filename, date: result.completed_date || new Date().toISOString().split('T')[0] }, raw, 'completed');
|
|
490
772
|
}
|
|
491
773
|
|
|
492
774
|
function cmdScaffold(cwd, type, options, raw) {
|
|
@@ -568,6 +850,8 @@ function cmdContextHelp(raw) {
|
|
|
568
850
|
}
|
|
569
851
|
|
|
570
852
|
module.exports = {
|
|
853
|
+
TODO_STATUSES,
|
|
854
|
+
setTodoStatus,
|
|
571
855
|
cmdGenerateSlug,
|
|
572
856
|
cmdCurrentTimestamp,
|
|
573
857
|
cmdListTodos,
|
|
@@ -575,6 +859,7 @@ module.exports = {
|
|
|
575
859
|
cmdHistoryDigest,
|
|
576
860
|
cmdResolveModel,
|
|
577
861
|
cmdCommit,
|
|
862
|
+
cmdPlanFinalize,
|
|
578
863
|
cmdSummaryExtract,
|
|
579
864
|
cmdWebsearch,
|
|
580
865
|
cmdProgressRender,
|