@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
|
@@ -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;
|
|
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
|
+
}
|
|
53
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,44 +268,86 @@ function cmdResolveModel(cwd, agentType, raw) {
|
|
|
214
268
|
output(result, raw, model);
|
|
215
269
|
}
|
|
216
270
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
// Collect the list of still-dirty paths in `gitCwd` immediately after a
|
|
287
|
+
// commit (or nothing_to_commit). Purely informational: populates
|
|
288
|
+
// `result.dirty_after` so callers can detect verify-step side effects
|
|
289
|
+
// (formatter reflows, type narrowings) that leaked outside the staged
|
|
290
|
+
// file set. Never throws — returns [] on any error.
|
|
291
|
+
function collectDirtyAfter(gitCwd) {
|
|
292
|
+
const porcelain = execGit(gitCwd, ['status', '--porcelain']);
|
|
293
|
+
if (porcelain.exitCode !== 0) return [];
|
|
294
|
+
return (porcelain.stdout || '')
|
|
295
|
+
.split('\n')
|
|
296
|
+
.map(l => l.trim())
|
|
297
|
+
.filter(Boolean)
|
|
298
|
+
.map(l => l.replace(/^..\s+/, '')); // strip two-char XY status prefix + space
|
|
299
|
+
}
|
|
221
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Internal commit primitive — RETURNS the JSON result object instead of
|
|
303
|
+
* calling output()/process.exit. Extracted from cmdCommit for reuse by
|
|
304
|
+
* verifyPlanCommit (REL-01, Phase 156). External callers should keep
|
|
305
|
+
* using cmdCommit; this helper is library-internal only.
|
|
306
|
+
*
|
|
307
|
+
* @param {string} cwd - Planning root (config loaded from here)
|
|
308
|
+
* @param {string} message - Commit message (required unless amend)
|
|
309
|
+
* @param {string[]|undefined} files - Files to stage; if empty/undefined,
|
|
310
|
+
* the historical behaviour is to fall back to ['.'] (sweeping the
|
|
311
|
+
* working tree). REL-01's verifyPlanCommit guards against this fallback
|
|
312
|
+
* for the orchestrator commit case BEFORE it ever reaches commitInternal.
|
|
313
|
+
* @param {boolean} amend
|
|
314
|
+
* @param {boolean} push
|
|
315
|
+
* @param {string} [repoCwd] - Where git operations actually run.
|
|
316
|
+
* @returns {object} JSON result matching cmdCommit's existing contract.
|
|
317
|
+
*/
|
|
318
|
+
function commitInternal(cwd, message, files, amend, push, repoCwd) {
|
|
222
319
|
const config = loadConfig(cwd);
|
|
223
320
|
|
|
224
321
|
// Check commit_docs config
|
|
225
322
|
if (!config.commit_docs) {
|
|
226
|
-
|
|
227
|
-
output(result, raw, 'skipped');
|
|
228
|
-
return;
|
|
323
|
+
return { committed: false, hash: null, reason: 'skipped_commit_docs_false' };
|
|
229
324
|
}
|
|
230
325
|
|
|
326
|
+
// Resolve git-operation cwd: use repoCwd when provided, otherwise cwd.
|
|
327
|
+
// Config is always loaded from cwd (planning root); only git exec targets
|
|
328
|
+
// the worktree filesystem.
|
|
329
|
+
const gitCwd = repoCwd || cwd;
|
|
330
|
+
|
|
231
331
|
// Stage files
|
|
232
332
|
const filesToStage = files && files.length > 0 ? files : ['.'];
|
|
233
333
|
for (const file of filesToStage) {
|
|
234
|
-
execGit(
|
|
334
|
+
execGit(gitCwd, ['add', file]);
|
|
235
335
|
}
|
|
236
336
|
|
|
237
337
|
// Commit
|
|
238
338
|
const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', message];
|
|
239
|
-
const commitResult = execGit(
|
|
339
|
+
const commitResult = execGit(gitCwd, commitArgs);
|
|
240
340
|
if (commitResult.exitCode !== 0) {
|
|
241
341
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
242
|
-
|
|
243
|
-
output(result, raw, 'nothing');
|
|
244
|
-
return;
|
|
342
|
+
return { committed: false, hash: null, reason: 'nothing_to_commit', dirty_after: collectDirtyAfter(gitCwd) };
|
|
245
343
|
}
|
|
246
|
-
|
|
247
|
-
output(result, raw, 'nothing');
|
|
248
|
-
return;
|
|
344
|
+
return { committed: false, hash: null, reason: 'nothing_to_commit', error: commitResult.stderr, dirty_after: collectDirtyAfter(gitCwd) };
|
|
249
345
|
}
|
|
250
346
|
|
|
251
347
|
// Get short hash
|
|
252
|
-
const hashResult = execGit(
|
|
348
|
+
const hashResult = execGit(gitCwd, ['rev-parse', '--short', 'HEAD']);
|
|
253
349
|
const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
|
|
254
|
-
const result = { committed: true, hash, reason: 'committed' };
|
|
350
|
+
const result = { committed: true, hash, reason: 'committed', dirty_after: collectDirtyAfter(gitCwd) };
|
|
255
351
|
|
|
256
352
|
// Handle push if requested
|
|
257
353
|
if (push) {
|
|
@@ -273,7 +369,376 @@ function cmdCommit(cwd, message, files, raw, amend, push) {
|
|
|
273
369
|
// 'off' or any other value: no push fields added
|
|
274
370
|
}
|
|
275
371
|
|
|
276
|
-
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function cmdCommit(cwd, message, files, raw, amend, push, repoCwd) {
|
|
376
|
+
if (!message && !amend) {
|
|
377
|
+
error('commit message required');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const result = commitInternal(cwd, message, files, amend, push, repoCwd);
|
|
381
|
+
|
|
382
|
+
// Branch labels per the original cmdCommit output() calls
|
|
383
|
+
if (result.reason === 'skipped_commit_docs_false') {
|
|
384
|
+
output(result, raw, 'skipped');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (result.reason === 'nothing_to_commit') {
|
|
388
|
+
output(result, raw, 'nothing');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
output(result, raw, result.hash || 'committed');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* REL-01 (Phase 156, plan 02): orchestrator-side commit + verification
|
|
396
|
+
* helper consumed by /dgs:plan-phase to commit planner-reported
|
|
397
|
+
* created_files. Guarantees:
|
|
398
|
+
*
|
|
399
|
+
* - Empty createdFiles → returns plan-commit-incomplete WITHOUT calling
|
|
400
|
+
* commitInternal. Defends against cmdCommit's `['.']` fallback that
|
|
401
|
+
* would otherwise sweep the working tree (Hypothesis C from
|
|
402
|
+
* 156-Q1-FINDINGS.md).
|
|
403
|
+
* - commit_docs:false → silent success ({ ok: true, hash: null,
|
|
404
|
+
* reason: 'skipped_commit_docs_false' }). NOT a failure.
|
|
405
|
+
* - cmdCommit failure → plan-commit-incomplete with reason: 'commit_failed'.
|
|
406
|
+
* - Verification: every entry in (createdFiles + extraFiles) MUST appear
|
|
407
|
+
* in `git show --name-only HEAD`; mismatches → plan-commit-incomplete
|
|
408
|
+
* with reason: 'verification_failed' and a `missing` array.
|
|
409
|
+
*
|
|
410
|
+
* @param {string} cwd - Planning root (config loaded from here)
|
|
411
|
+
* @param {object} options - {
|
|
412
|
+
* message: string (REQUIRED),
|
|
413
|
+
* createdFiles: string[] (REQUIRED),
|
|
414
|
+
* extraFiles?: string[],
|
|
415
|
+
* repoCwd?: string,
|
|
416
|
+
* push?: boolean,
|
|
417
|
+
* }
|
|
418
|
+
* @param {boolean} raw - Emit raw JSON via output() if true.
|
|
419
|
+
* @returns {object} Returns the result object directly (so the test
|
|
420
|
+
* harness can assert on it). Also calls output() when invoked from CLI.
|
|
421
|
+
*/
|
|
422
|
+
function verifyPlanCommit(cwd, options, raw) {
|
|
423
|
+
const opts = options || {};
|
|
424
|
+
const message = opts.message;
|
|
425
|
+
const createdFiles = opts.createdFiles;
|
|
426
|
+
const extraFiles = opts.extraFiles || [];
|
|
427
|
+
const repoCwd = opts.repoCwd;
|
|
428
|
+
const push = !!opts.push;
|
|
429
|
+
|
|
430
|
+
// verifyPlanCommit is a pure helper — it RETURNS the result object
|
|
431
|
+
// (so library callers and tests can assert on it). The `raw` argument
|
|
432
|
+
// is accepted for CLI dispatch parity but is intentionally unused
|
|
433
|
+
// here; the dgs-tools CLI dispatcher (dgs-tools.cjs) is responsible
|
|
434
|
+
// for calling output()/process.exit on the returned object.
|
|
435
|
+
void raw;
|
|
436
|
+
|
|
437
|
+
if (!message) {
|
|
438
|
+
return {
|
|
439
|
+
ok: false,
|
|
440
|
+
exitLabel: 'plan-commit-incomplete',
|
|
441
|
+
reason: 'missing_message',
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Empty-list guard — defends Hypothesis C (cmdCommit `['.']` fallback)
|
|
446
|
+
if (!Array.isArray(createdFiles) || createdFiles.length === 0) {
|
|
447
|
+
return {
|
|
448
|
+
ok: false,
|
|
449
|
+
exitLabel: 'plan-commit-incomplete',
|
|
450
|
+
reason: 'empty_created_files',
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const filesToCommit = [...createdFiles, ...extraFiles];
|
|
455
|
+
|
|
456
|
+
const commitResult = commitInternal(cwd, message, filesToCommit, false, push, repoCwd);
|
|
457
|
+
|
|
458
|
+
// commit_docs config gate — silent success
|
|
459
|
+
if (commitResult.reason === 'skipped_commit_docs_false') {
|
|
460
|
+
return {
|
|
461
|
+
ok: true,
|
|
462
|
+
hash: null,
|
|
463
|
+
reason: 'skipped_commit_docs_false',
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Any non-success commit reason is a plan-commit-incomplete failure
|
|
468
|
+
if (commitResult.committed !== true) {
|
|
469
|
+
return {
|
|
470
|
+
ok: false,
|
|
471
|
+
exitLabel: 'plan-commit-incomplete',
|
|
472
|
+
reason: 'commit_failed',
|
|
473
|
+
commit_reason: commitResult.reason,
|
|
474
|
+
error: commitResult.error,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Verification: every reported path MUST appear in HEAD
|
|
479
|
+
const gitCwd = repoCwd || cwd;
|
|
480
|
+
const showResult = execGit(gitCwd, ['show', '--name-only', '--pretty=', 'HEAD']);
|
|
481
|
+
const committedFiles = (showResult.stdout || '')
|
|
482
|
+
.split('\n')
|
|
483
|
+
.map(l => l.trim())
|
|
484
|
+
.filter(Boolean);
|
|
485
|
+
const missing = filesToCommit.filter(f => !committedFiles.includes(f));
|
|
486
|
+
|
|
487
|
+
if (missing.length > 0) {
|
|
488
|
+
return {
|
|
489
|
+
ok: false,
|
|
490
|
+
exitLabel: 'plan-commit-incomplete',
|
|
491
|
+
reason: 'verification_failed',
|
|
492
|
+
hash: commitResult.hash,
|
|
493
|
+
missing,
|
|
494
|
+
committedFiles,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
ok: true,
|
|
500
|
+
hash: commitResult.hash,
|
|
501
|
+
reason: 'committed',
|
|
502
|
+
files_verified: filesToCommit,
|
|
503
|
+
missing: [],
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* REL-02 (Phase 156, plan 03): compute the executor's final-commit
|
|
509
|
+
* phase-dir sweep. Always sweeps the current phase directory and takes
|
|
510
|
+
* the UNION with the executor-reported modified_files list, then
|
|
511
|
+
* scope-filters out anything that does not start with the
|
|
512
|
+
* ${phasesDir}/${phaseDir}/ prefix.
|
|
513
|
+
*
|
|
514
|
+
* Hard scope guarantee: dirty files in sibling phases, ideas/, specs/,
|
|
515
|
+
* or the project root are NEVER returned in `swept`. They are returned
|
|
516
|
+
* in `dropped` for diagnostic visibility.
|
|
517
|
+
*
|
|
518
|
+
* @param {string} cwd - Planning root (where git status runs)
|
|
519
|
+
* @param {object} options - {
|
|
520
|
+
* phasesDir: string, // e.g. 'projects/gsd/phases'
|
|
521
|
+
* phaseDir: string, // e.g. '156-idea-26-closure-...'
|
|
522
|
+
* modifiedFiles?: string[] // executor-reported paths (planning-root-relative)
|
|
523
|
+
* }
|
|
524
|
+
* @param {boolean} raw
|
|
525
|
+
* @returns {{
|
|
526
|
+
* swept: string[], // commit list — UNION, scope-filtered, sorted
|
|
527
|
+
* dropped: string[], // out-of-scope paths the helper rejected
|
|
528
|
+
* gitDirtyPaths: string[], // git-discovered phase-dir paths (pre-union)
|
|
529
|
+
* reportedPaths: string[], // executor-reported list (pre-union)
|
|
530
|
+
* scopePrefix: string // ${phasesDir}/${phaseDir}/
|
|
531
|
+
* }}
|
|
532
|
+
*/
|
|
533
|
+
function computePhaseSweep(cwd, options, raw) {
|
|
534
|
+
const opts = options || {};
|
|
535
|
+
const phasesDir = opts.phasesDir;
|
|
536
|
+
const phaseDir = opts.phaseDir;
|
|
537
|
+
const reportedPathsRaw = opts.modifiedFiles || [];
|
|
538
|
+
|
|
539
|
+
void raw;
|
|
540
|
+
|
|
541
|
+
if (!phasesDir || !phaseDir) {
|
|
542
|
+
return {
|
|
543
|
+
swept: [],
|
|
544
|
+
dropped: [],
|
|
545
|
+
gitDirtyPaths: [],
|
|
546
|
+
reportedPaths: [],
|
|
547
|
+
scopePrefix: null,
|
|
548
|
+
error: 'phasesDir and phaseDir required for compute-phase-sweep',
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const scopePrefix = `${phasesDir}/${phaseDir}/`;
|
|
553
|
+
|
|
554
|
+
// Run path-scoped porcelain. Use --untracked-files=all so untracked
|
|
555
|
+
// files inside untracked directories are listed individually rather
|
|
556
|
+
// than collapsed into a single directory entry like `phases/{dir}/`.
|
|
557
|
+
const gitArgs = ['status', '--porcelain', '--untracked-files=all', '--', `${phasesDir}/${phaseDir}`];
|
|
558
|
+
const gitResult = execGit(cwd, gitArgs);
|
|
559
|
+
const gitDirtyPaths = gitResult.exitCode === 0
|
|
560
|
+
? (gitResult.stdout || '')
|
|
561
|
+
.split('\n')
|
|
562
|
+
.map(l => l.trim())
|
|
563
|
+
.filter(Boolean)
|
|
564
|
+
// Strip 2-char XY status prefix + space (matches collectDirtyAfter)
|
|
565
|
+
.map(l => l.replace(/^..\s+/, ''))
|
|
566
|
+
: [];
|
|
567
|
+
|
|
568
|
+
// Strip optional `repoName:` prefix from reported paths — multi-repo
|
|
569
|
+
// entries with explicit repoName are out of scope for the planning-root
|
|
570
|
+
// sweep (they route through their own resolveRepoRelativePath flow).
|
|
571
|
+
const reportedPaths = reportedPathsRaw
|
|
572
|
+
.map(p => {
|
|
573
|
+
// If the entry contains a `:` and the leading segment looks like a
|
|
574
|
+
// repo name (no `/`), treat it as repoName:path and drop it for
|
|
575
|
+
// planning-root sweep purposes.
|
|
576
|
+
const colonIdx = p.indexOf(':');
|
|
577
|
+
if (colonIdx > 0 && p.indexOf('/') > colonIdx) return null;
|
|
578
|
+
return p;
|
|
579
|
+
})
|
|
580
|
+
.filter(p => p !== null);
|
|
581
|
+
|
|
582
|
+
// Compute the UNION (dedupe via Set)
|
|
583
|
+
const candidates = Array.from(new Set([...gitDirtyPaths, ...reportedPaths]));
|
|
584
|
+
|
|
585
|
+
// Belt-and-braces: scope-filter via prefix check
|
|
586
|
+
const swept = candidates
|
|
587
|
+
.filter(p => p.startsWith(scopePrefix))
|
|
588
|
+
.sort();
|
|
589
|
+
const dropped = candidates
|
|
590
|
+
.filter(p => !p.startsWith(scopePrefix))
|
|
591
|
+
.sort();
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
swept,
|
|
595
|
+
dropped,
|
|
596
|
+
gitDirtyPaths,
|
|
597
|
+
reportedPaths,
|
|
598
|
+
scopePrefix,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* CLI: `plan finalize <phase> <plan> [--plan-name <name>] [--push]`.
|
|
604
|
+
*
|
|
605
|
+
* Runs state update-progress + roadmap update-plan-progress + requirements
|
|
606
|
+
* mark-complete AND commits PLAN.md + SUMMARY.md + tracking files in a single
|
|
607
|
+
* atomic call. Requirement IDs are auto-extracted from PLAN.md frontmatter.
|
|
608
|
+
* Plan name resolution order: --plan-name flag > PLAN.md frontmatter plan_name
|
|
609
|
+
* > "execution" fallback.
|
|
610
|
+
*
|
|
611
|
+
* Does NOT call cmdCommit or the existing cmdStateUpdateProgress /
|
|
612
|
+
* cmdRoadmapUpdatePlanProgress / cmdRequirementsMarkComplete CLIs (all call
|
|
613
|
+
* output()/exit). Uses their *Internal helpers + execGit directly.
|
|
614
|
+
*/
|
|
615
|
+
function cmdPlanFinalize(cwd, phaseNum, planNum, options, raw) {
|
|
616
|
+
if (!phaseNum) error('phase number required for plan finalize');
|
|
617
|
+
if (!planNum) error('plan number required for plan finalize');
|
|
618
|
+
|
|
619
|
+
const { stateUpdateProgressInternal } = require('./state.cjs');
|
|
620
|
+
const { roadmapUpdatePlanProgressInternal } = require('./roadmap.cjs');
|
|
621
|
+
const { requirementsMarkCompleteInternal } = require('./milestone.cjs');
|
|
622
|
+
|
|
623
|
+
// Resolve phase dir via findPhaseInternal (returns phases/NN-name relative path)
|
|
624
|
+
const phaseInfo = findPhaseInternal(cwd, phaseNum);
|
|
625
|
+
if (!phaseInfo) error(`Phase ${phaseNum} not found`);
|
|
626
|
+
const phaseDir = path.join(cwd, phaseInfo.directory);
|
|
627
|
+
|
|
628
|
+
// Locate PLAN.md + SUMMARY.md within the phase dir using `${phaseNum}-${planNum}` prefix
|
|
629
|
+
// phaseInfo.phase_number is the canonical (padded) number e.g. "01".
|
|
630
|
+
const prefix = `${phaseInfo.phase_number || phaseNum}-${planNum}`;
|
|
631
|
+
let planPath = null;
|
|
632
|
+
let summaryPath = null;
|
|
633
|
+
try {
|
|
634
|
+
const entries = fs.readdirSync(phaseDir);
|
|
635
|
+
const planFile = entries.find(f =>
|
|
636
|
+
f.toLowerCase().startsWith(prefix.toLowerCase()) && /-PLAN\.md$/i.test(f)
|
|
637
|
+
);
|
|
638
|
+
const summaryFile = entries.find(f =>
|
|
639
|
+
f.toLowerCase().startsWith(prefix.toLowerCase()) && /-SUMMARY\.md$/i.test(f)
|
|
640
|
+
);
|
|
641
|
+
if (planFile) planPath = path.join(phaseDir, planFile);
|
|
642
|
+
if (summaryFile) summaryPath = path.join(phaseDir, summaryFile);
|
|
643
|
+
} catch { /* ignore */ }
|
|
644
|
+
|
|
645
|
+
// Extract plan_name + requirements from PLAN.md frontmatter
|
|
646
|
+
let planName = (options && options.planName) || null;
|
|
647
|
+
let reqIds = [];
|
|
648
|
+
if (planPath && fs.existsSync(planPath)) {
|
|
649
|
+
try {
|
|
650
|
+
const content = fs.readFileSync(planPath, 'utf-8');
|
|
651
|
+
const fm = extractFrontmatter(content);
|
|
652
|
+
if (fm) {
|
|
653
|
+
if (!planName && fm.plan_name) planName = String(fm.plan_name);
|
|
654
|
+
if (fm.requirements) {
|
|
655
|
+
reqIds = Array.isArray(fm.requirements)
|
|
656
|
+
? fm.requirements.map(String).map(s => s.trim()).filter(Boolean)
|
|
657
|
+
: String(fm.requirements).replace(/[\[\]]/g, '').split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
} catch { /* ignore */ }
|
|
661
|
+
}
|
|
662
|
+
if (!planName) planName = 'execution';
|
|
663
|
+
|
|
664
|
+
// Resolve tracking file paths (uses same project-root logic as phase finalize)
|
|
665
|
+
let projRoot;
|
|
666
|
+
try { projRoot = require('./core.cjs').getProjectRoot(cwd); } catch { projRoot = '.'; }
|
|
667
|
+
const roadmapPath = path.join(cwd, projRoot, 'ROADMAP.md');
|
|
668
|
+
const statePath = path.join(cwd, projRoot, 'STATE.md');
|
|
669
|
+
const requirementsPath = path.join(cwd, projRoot, 'REQUIREMENTS.md');
|
|
670
|
+
|
|
671
|
+
// Run internal updates (each returns { result, ... } without exit)
|
|
672
|
+
const stateUpdate = stateUpdateProgressInternal(cwd);
|
|
673
|
+
const roadmapUpdate = roadmapUpdatePlanProgressInternal(cwd, phaseNum);
|
|
674
|
+
const reqUpdate = reqIds.length > 0
|
|
675
|
+
? requirementsMarkCompleteInternal(cwd, reqIds)
|
|
676
|
+
: { result: { updated: false, marked_complete: [], not_found: [], total: 0 }, reqPath: requirementsPath };
|
|
677
|
+
|
|
678
|
+
// Stage all files that exist on disk
|
|
679
|
+
const candidates = [planPath, summaryPath, statePath, roadmapPath, requirementsPath];
|
|
680
|
+
const filesToStage = candidates
|
|
681
|
+
.filter(p => p && fs.existsSync(p))
|
|
682
|
+
.map(p => path.relative(cwd, p));
|
|
683
|
+
|
|
684
|
+
const config = loadConfig(cwd);
|
|
685
|
+
const result = {
|
|
686
|
+
phase: phaseNum,
|
|
687
|
+
plan: planNum,
|
|
688
|
+
plan_name: planName,
|
|
689
|
+
state: stateUpdate.result,
|
|
690
|
+
roadmap: roadmapUpdate.result,
|
|
691
|
+
requirements: reqUpdate.result,
|
|
692
|
+
files_committed: [],
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
if (!config.commit_docs) {
|
|
696
|
+
result.committed = false;
|
|
697
|
+
result.commit_reason = 'skipped_commit_docs_false';
|
|
698
|
+
output(result, raw);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const message = `docs(${phaseNum}-${planNum}): complete ${planName} plan`;
|
|
703
|
+
for (const f of filesToStage) {
|
|
704
|
+
execGit(cwd, ['add', f]);
|
|
705
|
+
}
|
|
706
|
+
const commitResult = execGit(cwd, ['commit', '-m', message]);
|
|
707
|
+
if (commitResult.exitCode !== 0) {
|
|
708
|
+
const nothing =
|
|
709
|
+
commitResult.stdout.includes('nothing to commit') ||
|
|
710
|
+
commitResult.stderr.includes('nothing to commit');
|
|
711
|
+
result.committed = false;
|
|
712
|
+
result.commit_reason = nothing ? 'nothing_to_commit' : 'commit_failed';
|
|
713
|
+
if (!nothing) result.commit_error = commitResult.stderr;
|
|
714
|
+
output(result, raw);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const hashResult = execGit(cwd, ['rev-parse', '--short', 'HEAD']);
|
|
718
|
+
result.committed = true;
|
|
719
|
+
result.hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
|
|
720
|
+
result.commit_reason = 'committed';
|
|
721
|
+
result.files_committed = filesToStage;
|
|
722
|
+
|
|
723
|
+
// Optional push (same semantics as cmdCommit / cmdPhaseFinalize)
|
|
724
|
+
if (options && options.push) {
|
|
725
|
+
const syncPush = config.sync_push || 'off';
|
|
726
|
+
if (syncPush === 'auto') {
|
|
727
|
+
try {
|
|
728
|
+
const { pushAll } = require('./sync.cjs');
|
|
729
|
+
const pushRes = pushAll(cwd, { force: true });
|
|
730
|
+
result.pushed = pushRes.ok;
|
|
731
|
+
result.push_result = pushRes;
|
|
732
|
+
} catch (err) {
|
|
733
|
+
result.pushed = false;
|
|
734
|
+
result.push_result = { ok: false, error: err.message };
|
|
735
|
+
}
|
|
736
|
+
} else if (syncPush === 'prompt') {
|
|
737
|
+
result.needs_push = true;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
output(result, raw);
|
|
277
742
|
}
|
|
278
743
|
|
|
279
744
|
function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
|
|
@@ -314,7 +779,9 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
|
|
|
314
779
|
tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
|
|
315
780
|
patterns: fm['patterns-established'] || [],
|
|
316
781
|
decisions: parseDecisions(fm['key-decisions']),
|
|
317
|
-
|
|
782
|
+
// REL-07/10 transitional dual-read: canonical underscore key takes precedence;
|
|
783
|
+
// legacy hyphen key fallback preserves audit-readability for archived v23.1 SUMMARYs.
|
|
784
|
+
requirements_completed: fm['requirements_completed'] || fm['requirements-completed'] || [],
|
|
318
785
|
};
|
|
319
786
|
|
|
320
787
|
// If fields specified, filter to only those fields
|
|
@@ -395,8 +862,17 @@ async function cmdWebsearch(query, options, raw) {
|
|
|
395
862
|
}
|
|
396
863
|
|
|
397
864
|
function cmdProgressRender(cwd, format, raw) {
|
|
398
|
-
|
|
399
|
-
|
|
865
|
+
let phasesDir, roadmapPath;
|
|
866
|
+
try {
|
|
867
|
+
const projectRoot = getProjectRoot(cwd);
|
|
868
|
+
const projectAbs = path.join(cwd, projectRoot);
|
|
869
|
+
phasesDir = path.join(projectAbs, 'phases');
|
|
870
|
+
roadmapPath = path.join(projectAbs, 'ROADMAP.md');
|
|
871
|
+
} catch {
|
|
872
|
+
const planRoot = getPlanningRoot(cwd);
|
|
873
|
+
phasesDir = path.join(planRoot, 'phases');
|
|
874
|
+
roadmapPath = path.join(planRoot, 'ROADMAP.md');
|
|
875
|
+
}
|
|
400
876
|
const milestone = getMilestoneInfo(cwd);
|
|
401
877
|
|
|
402
878
|
const phases = [];
|
|
@@ -462,31 +938,88 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
462
938
|
}
|
|
463
939
|
}
|
|
464
940
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
941
|
+
/**
|
|
942
|
+
* Set a todo's frontmatter status field (internal helper — throws on error).
|
|
943
|
+
*
|
|
944
|
+
* If the todo file has no YAML frontmatter, wraps existing content with a
|
|
945
|
+
* frontmatter block containing status (and completed_date if status is 'done').
|
|
946
|
+
*
|
|
947
|
+
* @param {string} cwd - Working directory
|
|
948
|
+
* @param {string} filename - Todo filename
|
|
949
|
+
* @param {string} status - Target status (must be in TODO_STATUSES)
|
|
950
|
+
* @returns {{ filename: string, previous_status: string, status: string, completed_date?: string }}
|
|
951
|
+
* @throws {Error} If status is invalid or todo not found
|
|
952
|
+
*/
|
|
953
|
+
function setTodoStatus(cwd, filename, status) {
|
|
954
|
+
if (!TODO_STATUSES.includes(status)) {
|
|
955
|
+
throw new Error(`Invalid status: ${status}. Allowed: ${TODO_STATUSES.join(', ')}`);
|
|
468
956
|
}
|
|
469
957
|
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
958
|
+
// Search in both pending and completed directories
|
|
959
|
+
const planningRoot = getPlanningRoot(cwd);
|
|
960
|
+
let todoPath = null;
|
|
961
|
+
let previousStatus = 'pending';
|
|
473
962
|
|
|
474
|
-
|
|
475
|
-
|
|
963
|
+
for (const dir of ['pending', 'completed']) {
|
|
964
|
+
const candidate = path.join(planningRoot, 'todos', dir, filename);
|
|
965
|
+
if (fs.existsSync(candidate)) {
|
|
966
|
+
todoPath = candidate;
|
|
967
|
+
previousStatus = dir === 'completed' ? 'done' : 'pending';
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// Also check flat todos/ directory (future layout)
|
|
972
|
+
if (!todoPath) {
|
|
973
|
+
const flat = path.join(planningRoot, 'todos', filename);
|
|
974
|
+
if (fs.existsSync(flat)) {
|
|
975
|
+
todoPath = flat;
|
|
976
|
+
const content = fs.readFileSync(flat, 'utf-8');
|
|
977
|
+
const fm = extractFrontmatter(content);
|
|
978
|
+
previousStatus = fm.status || 'pending';
|
|
979
|
+
}
|
|
476
980
|
}
|
|
477
981
|
|
|
478
|
-
|
|
479
|
-
|
|
982
|
+
if (!todoPath) {
|
|
983
|
+
throw new Error(`Todo not found: ${filename}`);
|
|
984
|
+
}
|
|
480
985
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const today = new Date().toISOString().split('T')[0];
|
|
484
|
-
content = `completed: ${today}\n` + content;
|
|
986
|
+
let content = fs.readFileSync(todoPath, 'utf-8');
|
|
987
|
+
const fm = extractFrontmatter(content);
|
|
485
988
|
|
|
486
|
-
|
|
487
|
-
|
|
989
|
+
// Build frontmatter fields
|
|
990
|
+
fm.status = status;
|
|
991
|
+
if (status === 'done') {
|
|
992
|
+
fm.completed_date = new Date().toISOString().split('T')[0];
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// If file has frontmatter block, splice it; otherwise add one
|
|
996
|
+
if (content.match(/^---\n/)) {
|
|
997
|
+
content = spliceFrontmatter(content, fm);
|
|
998
|
+
} else {
|
|
999
|
+
// Strip bare "completed: YYYY-MM-DD" line if present (legacy format)
|
|
1000
|
+
content = content.replace(/^completed:\s*\d{4}-\d{2}-\d{2}\n/, '');
|
|
1001
|
+
const yamlStr = reconstructFrontmatter(fm);
|
|
1002
|
+
content = `---\n${yamlStr}\n---\n${content}`;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
fs.writeFileSync(todoPath, content, 'utf-8');
|
|
488
1006
|
|
|
489
|
-
|
|
1007
|
+
const result = { filename, previous_status: previousStatus, status };
|
|
1008
|
+
if (fm.completed_date) result.completed_date = fm.completed_date;
|
|
1009
|
+
return result;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function cmdTodoComplete(cwd, filename, raw) {
|
|
1013
|
+
if (!filename) {
|
|
1014
|
+
error('filename required for todo complete');
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Use setTodoStatus to edit frontmatter status to 'done'
|
|
1018
|
+
// setTodoStatus handles finding the file in flat or legacy directories
|
|
1019
|
+
// and automatically sets completed_date when status is 'done'
|
|
1020
|
+
const result = setTodoStatus(cwd, filename, 'done');
|
|
1021
|
+
|
|
1022
|
+
output({ completed: true, file: filename, date: result.completed_date || new Date().toISOString().split('T')[0] }, raw, 'completed');
|
|
490
1023
|
}
|
|
491
1024
|
|
|
492
1025
|
function cmdScaffold(cwd, type, options, raw) {
|
|
@@ -567,7 +1100,49 @@ function cmdContextHelp(raw) {
|
|
|
567
1100
|
output(result, raw, result.subcommands.map(s => s.usage).join('\n'));
|
|
568
1101
|
}
|
|
569
1102
|
|
|
1103
|
+
// REL-08 (Phase 157): pre-commit precondition gate.
|
|
1104
|
+
// Reads PLAN.md `requirements:` and SUMMARY.md `requirements_completed:` (canonical) /
|
|
1105
|
+
// `requirements-completed:` (legacy fallback). If PLAN is non-empty AND SUMMARY is empty,
|
|
1106
|
+
// writes `summary-frontmatter-mismatch:` label to stderr and exits non-zero.
|
|
1107
|
+
// NEVER writes to the working tree — read-only check.
|
|
1108
|
+
function cmdFinalCommitPrecondition(cwd, options) {
|
|
1109
|
+
const planPath = options && options.plan;
|
|
1110
|
+
const summaryPath = options && options.summary;
|
|
1111
|
+
if (!planPath || !summaryPath) {
|
|
1112
|
+
process.stderr.write('summary-frontmatter-mismatch: --plan and --summary required\n');
|
|
1113
|
+
process.exit(2);
|
|
1114
|
+
}
|
|
1115
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
1116
|
+
const planAbs = path.isAbsolute(planPath) ? planPath : path.join(cwd, planPath);
|
|
1117
|
+
const summaryAbs = path.isAbsolute(summaryPath) ? summaryPath : path.join(cwd, summaryPath);
|
|
1118
|
+
|
|
1119
|
+
if (!fs.existsSync(planAbs)) {
|
|
1120
|
+
process.stderr.write(`summary-frontmatter-mismatch: PLAN not found at ${planPath}\n`);
|
|
1121
|
+
process.exit(2);
|
|
1122
|
+
}
|
|
1123
|
+
if (!fs.existsSync(summaryAbs)) {
|
|
1124
|
+
process.stderr.write(`summary-frontmatter-mismatch: SUMMARY not found at ${summaryPath}\n`);
|
|
1125
|
+
process.exit(2);
|
|
1126
|
+
}
|
|
1127
|
+
const planFm = extractFrontmatter(fs.readFileSync(planAbs, 'utf-8'));
|
|
1128
|
+
const summaryFm = extractFrontmatter(fs.readFileSync(summaryAbs, 'utf-8'));
|
|
1129
|
+
const planReq = Array.isArray(planFm.requirements) ? planFm.requirements : [];
|
|
1130
|
+
// Dual-read: canonical underscore key first, legacy hyphen fallback (REL-07/10 dual-read).
|
|
1131
|
+
const summaryReq = summaryFm['requirements_completed'] || summaryFm['requirements-completed'] || [];
|
|
1132
|
+
|
|
1133
|
+
if (planReq.length > 0 && summaryReq.length === 0) {
|
|
1134
|
+
process.stderr.write(
|
|
1135
|
+
`summary-frontmatter-mismatch: PLAN.md declared requirements [${planReq.join(', ')}]; ` +
|
|
1136
|
+
`SUMMARY.md requirements_completed is empty. Re-run executor or manually populate before committing.\n`
|
|
1137
|
+
);
|
|
1138
|
+
process.exit(2);
|
|
1139
|
+
}
|
|
1140
|
+
process.exit(0);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
570
1143
|
module.exports = {
|
|
1144
|
+
TODO_STATUSES,
|
|
1145
|
+
setTodoStatus,
|
|
571
1146
|
cmdGenerateSlug,
|
|
572
1147
|
cmdCurrentTimestamp,
|
|
573
1148
|
cmdListTodos,
|
|
@@ -575,10 +1150,15 @@ module.exports = {
|
|
|
575
1150
|
cmdHistoryDigest,
|
|
576
1151
|
cmdResolveModel,
|
|
577
1152
|
cmdCommit,
|
|
1153
|
+
commitInternal,
|
|
1154
|
+
verifyPlanCommit,
|
|
1155
|
+
computePhaseSweep,
|
|
1156
|
+
cmdPlanFinalize,
|
|
578
1157
|
cmdSummaryExtract,
|
|
579
1158
|
cmdWebsearch,
|
|
580
1159
|
cmdProgressRender,
|
|
581
1160
|
cmdTodoComplete,
|
|
582
1161
|
cmdScaffold,
|
|
583
1162
|
cmdContextHelp,
|
|
1163
|
+
cmdFinalCommitPrecondition,
|
|
584
1164
|
};
|