gsd-opencode 1.22.1 → 1.33.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/agents/gsd-advisor-researcher.md +112 -0
- package/agents/gsd-assumptions-analyzer.md +110 -0
- package/agents/gsd-codebase-mapper.md +0 -2
- package/agents/gsd-debugger.md +117 -2
- package/agents/gsd-doc-verifier.md +207 -0
- package/agents/gsd-doc-writer.md +608 -0
- package/agents/gsd-executor.md +45 -4
- package/agents/gsd-integration-checker.md +0 -2
- package/agents/gsd-nyquist-auditor.md +0 -2
- package/agents/gsd-phase-researcher.md +191 -5
- package/agents/gsd-plan-checker.md +152 -5
- package/agents/gsd-planner.md +131 -157
- package/agents/gsd-project-researcher.md +28 -3
- package/agents/gsd-research-synthesizer.md +0 -2
- package/agents/gsd-roadmapper.md +29 -2
- package/agents/gsd-security-auditor.md +129 -0
- package/agents/gsd-ui-auditor.md +485 -0
- package/agents/gsd-ui-checker.md +305 -0
- package/agents/gsd-ui-researcher.md +368 -0
- package/agents/gsd-user-profiler.md +173 -0
- package/agents/gsd-verifier.md +207 -22
- package/commands/gsd/gsd-add-backlog.md +76 -0
- package/commands/gsd/gsd-analyze-dependencies.md +34 -0
- package/commands/gsd/gsd-audit-uat.md +24 -0
- package/commands/gsd/gsd-autonomous.md +45 -0
- package/commands/gsd/gsd-cleanup.md +5 -0
- package/commands/gsd/gsd-debug.md +29 -21
- package/commands/gsd/gsd-discuss-phase.md +15 -36
- package/commands/gsd/gsd-do.md +30 -0
- package/commands/gsd/gsd-docs-update.md +48 -0
- package/commands/gsd/gsd-execute-phase.md +24 -2
- package/commands/gsd/gsd-fast.md +30 -0
- package/commands/gsd/gsd-forensics.md +56 -0
- package/commands/gsd/gsd-help.md +2 -0
- package/commands/gsd/gsd-join-discord.md +2 -1
- package/commands/gsd/gsd-list-workspaces.md +19 -0
- package/commands/gsd/gsd-manager.md +40 -0
- package/commands/gsd/gsd-milestone-summary.md +51 -0
- package/commands/gsd/gsd-new-project.md +4 -0
- package/commands/gsd/gsd-new-workspace.md +44 -0
- package/commands/gsd/gsd-next.md +24 -0
- package/commands/gsd/gsd-note.md +34 -0
- package/commands/gsd/gsd-plan-phase.md +8 -1
- package/commands/gsd/gsd-plant-seed.md +28 -0
- package/commands/gsd/gsd-pr-branch.md +25 -0
- package/commands/gsd/gsd-profile-user.md +46 -0
- package/commands/gsd/gsd-quick.md +7 -3
- package/commands/gsd/gsd-reapply-patches.md +178 -45
- package/commands/gsd/gsd-remove-workspace.md +26 -0
- package/commands/gsd/gsd-research-phase.md +7 -12
- package/commands/gsd/gsd-review-backlog.md +62 -0
- package/commands/gsd/gsd-review.md +38 -0
- package/commands/gsd/gsd-secure-phase.md +35 -0
- package/commands/gsd/gsd-session-report.md +19 -0
- package/commands/gsd/gsd-set-profile.md +24 -23
- package/commands/gsd/gsd-ship.md +23 -0
- package/commands/gsd/gsd-stats.md +18 -0
- package/commands/gsd/gsd-thread.md +127 -0
- package/commands/gsd/gsd-ui-phase.md +34 -0
- package/commands/gsd/gsd-ui-review.md +32 -0
- package/commands/gsd/gsd-workstreams.md +71 -0
- package/get-shit-done/bin/gsd-tools.cjs +450 -90
- package/get-shit-done/bin/lib/commands.cjs +489 -24
- package/get-shit-done/bin/lib/config.cjs +329 -48
- package/get-shit-done/bin/lib/core.cjs +1143 -102
- package/get-shit-done/bin/lib/docs.cjs +267 -0
- package/get-shit-done/bin/lib/frontmatter.cjs +125 -43
- package/get-shit-done/bin/lib/init.cjs +918 -106
- package/get-shit-done/bin/lib/milestone.cjs +65 -33
- package/get-shit-done/bin/lib/model-profiles.cjs +70 -0
- package/get-shit-done/bin/lib/phase.cjs +434 -404
- package/get-shit-done/bin/lib/profile-output.cjs +1048 -0
- package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
- package/get-shit-done/bin/lib/roadmap.cjs +156 -101
- package/get-shit-done/bin/lib/schema-detect.cjs +238 -0
- package/get-shit-done/bin/lib/security.cjs +384 -0
- package/get-shit-done/bin/lib/state.cjs +711 -79
- package/get-shit-done/bin/lib/template.cjs +2 -2
- package/get-shit-done/bin/lib/uat.cjs +282 -0
- package/get-shit-done/bin/lib/verify.cjs +254 -42
- package/get-shit-done/bin/lib/workstream.cjs +495 -0
- package/get-shit-done/references/agent-contracts.md +79 -0
- package/get-shit-done/references/artifact-types.md +113 -0
- package/get-shit-done/references/checkpoints.md +12 -10
- package/get-shit-done/references/context-budget.md +49 -0
- package/get-shit-done/references/continuation-format.md +15 -15
- package/get-shit-done/references/decimal-phase-calculation.md +2 -3
- package/get-shit-done/references/domain-probes.md +125 -0
- package/get-shit-done/references/gate-prompts.md +100 -0
- package/get-shit-done/references/git-integration.md +47 -0
- package/get-shit-done/references/model-profile-resolution.md +2 -0
- package/get-shit-done/references/model-profiles.md +62 -16
- package/get-shit-done/references/phase-argument-parsing.md +2 -2
- package/get-shit-done/references/planner-gap-closure.md +62 -0
- package/get-shit-done/references/planner-reviews.md +39 -0
- package/get-shit-done/references/planner-revision.md +87 -0
- package/get-shit-done/references/planning-config.md +18 -1
- package/get-shit-done/references/revision-loop.md +97 -0
- package/get-shit-done/references/ui-brand.md +2 -2
- package/get-shit-done/references/universal-anti-patterns.md +58 -0
- package/get-shit-done/references/user-profiling.md +681 -0
- package/get-shit-done/references/workstream-flag.md +111 -0
- package/get-shit-done/templates/SECURITY.md +61 -0
- package/get-shit-done/templates/UAT.md +21 -3
- package/get-shit-done/templates/UI-SPEC.md +100 -0
- package/get-shit-done/templates/VALIDATION.md +3 -3
- package/get-shit-done/templates/claude-md.md +145 -0
- package/get-shit-done/templates/config.json +14 -3
- package/get-shit-done/templates/context.md +61 -6
- package/get-shit-done/templates/debug-subagent-prompt.md +2 -6
- package/get-shit-done/templates/dev-preferences.md +21 -0
- package/get-shit-done/templates/discussion-log.md +63 -0
- package/get-shit-done/templates/phase-prompt.md +46 -5
- package/get-shit-done/templates/planner-subagent-prompt.md +2 -10
- package/get-shit-done/templates/project.md +2 -0
- package/get-shit-done/templates/state.md +2 -2
- package/get-shit-done/templates/user-profile.md +146 -0
- package/get-shit-done/workflows/add-phase.md +4 -4
- package/get-shit-done/workflows/add-tests.md +4 -4
- package/get-shit-done/workflows/add-todo.md +4 -4
- package/get-shit-done/workflows/analyze-dependencies.md +96 -0
- package/get-shit-done/workflows/audit-milestone.md +20 -16
- package/get-shit-done/workflows/audit-uat.md +109 -0
- package/get-shit-done/workflows/autonomous.md +1036 -0
- package/get-shit-done/workflows/check-todos.md +4 -4
- package/get-shit-done/workflows/cleanup.md +4 -4
- package/get-shit-done/workflows/complete-milestone.md +22 -10
- package/get-shit-done/workflows/diagnose-issues.md +21 -7
- package/get-shit-done/workflows/discovery-phase.md +2 -2
- package/get-shit-done/workflows/discuss-phase-assumptions.md +671 -0
- package/get-shit-done/workflows/discuss-phase-power.md +291 -0
- package/get-shit-done/workflows/discuss-phase.md +558 -47
- package/get-shit-done/workflows/do.md +104 -0
- package/get-shit-done/workflows/docs-update.md +1093 -0
- package/get-shit-done/workflows/execute-phase.md +741 -58
- package/get-shit-done/workflows/execute-plan.md +77 -12
- package/get-shit-done/workflows/fast.md +105 -0
- package/get-shit-done/workflows/forensics.md +265 -0
- package/get-shit-done/workflows/health.md +28 -6
- package/get-shit-done/workflows/help.md +127 -7
- package/get-shit-done/workflows/insert-phase.md +4 -4
- package/get-shit-done/workflows/list-phase-assumptions.md +2 -2
- package/get-shit-done/workflows/list-workspaces.md +56 -0
- package/get-shit-done/workflows/manager.md +363 -0
- package/get-shit-done/workflows/map-codebase.md +83 -44
- package/get-shit-done/workflows/milestone-summary.md +223 -0
- package/get-shit-done/workflows/new-milestone.md +133 -25
- package/get-shit-done/workflows/new-project.md +216 -54
- package/get-shit-done/workflows/new-workspace.md +237 -0
- package/get-shit-done/workflows/next.md +97 -0
- package/get-shit-done/workflows/node-repair.md +92 -0
- package/get-shit-done/workflows/note.md +156 -0
- package/get-shit-done/workflows/pause-work.md +132 -15
- package/get-shit-done/workflows/plan-milestone-gaps.md +6 -7
- package/get-shit-done/workflows/plan-phase.md +513 -62
- package/get-shit-done/workflows/plant-seed.md +169 -0
- package/get-shit-done/workflows/pr-branch.md +129 -0
- package/get-shit-done/workflows/profile-user.md +450 -0
- package/get-shit-done/workflows/progress.md +154 -29
- package/get-shit-done/workflows/quick.md +285 -111
- package/get-shit-done/workflows/remove-phase.md +2 -2
- package/get-shit-done/workflows/remove-workspace.md +90 -0
- package/get-shit-done/workflows/research-phase.md +13 -9
- package/get-shit-done/workflows/resume-project.md +37 -18
- package/get-shit-done/workflows/review.md +281 -0
- package/get-shit-done/workflows/secure-phase.md +154 -0
- package/get-shit-done/workflows/session-report.md +146 -0
- package/get-shit-done/workflows/set-profile.md +2 -2
- package/get-shit-done/workflows/settings.md +91 -11
- package/get-shit-done/workflows/ship.md +237 -0
- package/get-shit-done/workflows/stats.md +60 -0
- package/get-shit-done/workflows/transition.md +150 -23
- package/get-shit-done/workflows/ui-phase.md +292 -0
- package/get-shit-done/workflows/ui-review.md +183 -0
- package/get-shit-done/workflows/update.md +262 -30
- package/get-shit-done/workflows/validate-phase.md +14 -17
- package/get-shit-done/workflows/verify-phase.md +143 -11
- package/get-shit-done/workflows/verify-work.md +141 -39
- package/package.json +1 -1
- package/skills/gsd-audit-milestone/SKILL.md +29 -0
- package/skills/gsd-cleanup/SKILL.md +19 -0
- package/skills/gsd-complete-milestone/SKILL.md +131 -0
- package/skills/gsd-discuss-phase/SKILL.md +54 -0
- package/skills/gsd-execute-phase/SKILL.md +49 -0
- package/skills/gsd-plan-phase/SKILL.md +37 -0
- package/skills/gsd-ui-phase/SKILL.md +24 -0
- package/skills/gsd-ui-review/SKILL.md +24 -0
- package/skills/gsd-verify-work/SKILL.md +30 -0
|
@@ -4,8 +4,36 @@
|
|
|
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,
|
|
7
|
+
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, planningDir, planningPaths, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
|
+
const { MODEL_PROFILES } = require('./model-profiles.cjs');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Determine phase status by checking plan/summary counts AND verification state.
|
|
13
|
+
* Introduces "Executed" for phases with all summaries but no passing verification.
|
|
14
|
+
*/
|
|
15
|
+
function determinePhaseStatus(plans, summaries, phaseDir, defaultPending) {
|
|
16
|
+
if (plans === 0) return defaultPending;
|
|
17
|
+
if (summaries < plans && summaries > 0) return 'In Progress';
|
|
18
|
+
if (summaries < plans) return 'Planned';
|
|
19
|
+
|
|
20
|
+
// summaries >= plans — check verification
|
|
21
|
+
try {
|
|
22
|
+
const files = fs.readdirSync(phaseDir);
|
|
23
|
+
const verificationFile = files.find(f => f === 'VERIFICATION.md' || f.endsWith('-VERIFICATION.md'));
|
|
24
|
+
if (verificationFile) {
|
|
25
|
+
const content = fs.readFileSync(path.join(phaseDir, verificationFile), 'utf-8');
|
|
26
|
+
if (/status:\s*passed/i.test(content)) return 'Complete';
|
|
27
|
+
if (/status:\s*human_needed/i.test(content)) return 'Needs Review';
|
|
28
|
+
if (/status:\s*gaps_found/i.test(content)) return 'Executed';
|
|
29
|
+
// Verification exists but unrecognized status — treat as executed
|
|
30
|
+
return 'Executed';
|
|
31
|
+
}
|
|
32
|
+
} catch { /* directory read failed — fall through */ }
|
|
33
|
+
|
|
34
|
+
// No verification file — executed but not verified
|
|
35
|
+
return 'Executed';
|
|
36
|
+
}
|
|
9
37
|
|
|
10
38
|
function cmdGenerateSlug(text, raw) {
|
|
11
39
|
if (!text) {
|
|
@@ -15,7 +43,8 @@ function cmdGenerateSlug(text, raw) {
|
|
|
15
43
|
const slug = text
|
|
16
44
|
.toLowerCase()
|
|
17
45
|
.replace(/[^a-z0-9]+/g, '-')
|
|
18
|
-
.replace(/^-+|-+$/g, '')
|
|
46
|
+
.replace(/^-+|-+$/g, '')
|
|
47
|
+
.substring(0, 60);
|
|
19
48
|
|
|
20
49
|
const result = { slug };
|
|
21
50
|
output(result, raw, slug);
|
|
@@ -42,7 +71,7 @@ function cmdCurrentTimestamp(format, raw) {
|
|
|
42
71
|
}
|
|
43
72
|
|
|
44
73
|
function cmdListTodos(cwd, area, raw) {
|
|
45
|
-
const pendingDir = path.join(cwd, '
|
|
74
|
+
const pendingDir = path.join(planningDir(cwd), 'todos', 'pending');
|
|
46
75
|
|
|
47
76
|
let count = 0;
|
|
48
77
|
const todos = [];
|
|
@@ -68,11 +97,11 @@ function cmdListTodos(cwd, area, raw) {
|
|
|
68
97
|
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
|
69
98
|
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
70
99
|
area: todoArea,
|
|
71
|
-
path: toPosixPath(path.
|
|
100
|
+
path: toPosixPath(path.relative(cwd, path.join(pendingDir, file))),
|
|
72
101
|
});
|
|
73
|
-
} catch {}
|
|
102
|
+
} catch { /* intentionally empty */ }
|
|
74
103
|
}
|
|
75
|
-
} catch {}
|
|
104
|
+
} catch { /* intentionally empty */ }
|
|
76
105
|
|
|
77
106
|
const result = { count, todos };
|
|
78
107
|
output(result, raw, count.toString());
|
|
@@ -83,6 +112,11 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
|
|
|
83
112
|
error('path required for verification');
|
|
84
113
|
}
|
|
85
114
|
|
|
115
|
+
// Reject null bytes and validate path does not contain traversal attempts
|
|
116
|
+
if (targetPath.includes('\0')) {
|
|
117
|
+
error('path contains null bytes');
|
|
118
|
+
}
|
|
119
|
+
|
|
86
120
|
const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
|
|
87
121
|
|
|
88
122
|
try {
|
|
@@ -97,7 +131,7 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
|
|
|
97
131
|
}
|
|
98
132
|
|
|
99
133
|
function cmdHistoryDigest(cwd, raw) {
|
|
100
|
-
const phasesDir =
|
|
134
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
101
135
|
const digest = { phases: {}, decisions: [], tech_stack: new Set() };
|
|
102
136
|
|
|
103
137
|
// Collect all phase directories: archived + current
|
|
@@ -119,7 +153,7 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
119
153
|
for (const dir of currentDirs) {
|
|
120
154
|
allPhaseDirs.push({ name: dir, fullPath: path.join(phasesDir, dir), milestone: null });
|
|
121
155
|
}
|
|
122
|
-
} catch {}
|
|
156
|
+
} catch { /* intentionally empty */ }
|
|
123
157
|
}
|
|
124
158
|
|
|
125
159
|
if (allPhaseDirs.length === 0) {
|
|
@@ -213,11 +247,18 @@ function cmdResolveModel(cwd, agentType, raw) {
|
|
|
213
247
|
output(result, raw, model);
|
|
214
248
|
}
|
|
215
249
|
|
|
216
|
-
function cmdCommit(cwd, message, files, raw, amend) {
|
|
250
|
+
function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
217
251
|
if (!message && !amend) {
|
|
218
252
|
error('commit message required');
|
|
219
253
|
}
|
|
220
254
|
|
|
255
|
+
// Sanitize commit message: strip invisible chars and injection markers
|
|
256
|
+
// that could hijack agent context when commit messages are read back
|
|
257
|
+
if (message) {
|
|
258
|
+
const { sanitizeForPrompt } = require('./security.cjs');
|
|
259
|
+
message = sanitizeForPrompt(message);
|
|
260
|
+
}
|
|
261
|
+
|
|
221
262
|
const config = loadConfig(cwd);
|
|
222
263
|
|
|
223
264
|
// Check commit_docs config
|
|
@@ -234,14 +275,58 @@ function cmdCommit(cwd, message, files, raw, amend) {
|
|
|
234
275
|
return;
|
|
235
276
|
}
|
|
236
277
|
|
|
278
|
+
// Ensure branching strategy branch exists before first commit (#1278).
|
|
279
|
+
// Pre-execution workflows (discuss, plan, research) commit artifacts but the branch
|
|
280
|
+
// was previously only created during execute-phase — too late.
|
|
281
|
+
if (config.branching_strategy && config.branching_strategy !== 'none') {
|
|
282
|
+
let branchName = null;
|
|
283
|
+
if (config.branching_strategy === 'phase') {
|
|
284
|
+
// Determine which phase we're committing for from the file paths
|
|
285
|
+
const phaseMatch = (files || []).join(' ').match(/(\d+(?:\.\d+)*)-/);
|
|
286
|
+
if (phaseMatch) {
|
|
287
|
+
const phaseNum = phaseMatch[1];
|
|
288
|
+
const phaseInfo = findPhaseInternal(cwd, phaseNum);
|
|
289
|
+
if (phaseInfo) {
|
|
290
|
+
branchName = config.phase_branch_template
|
|
291
|
+
.replace('{phase}', phaseInfo.phase_number)
|
|
292
|
+
.replace('{slug}', phaseInfo.phase_slug || 'phase');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} else if (config.branching_strategy === 'milestone') {
|
|
296
|
+
const milestone = getMilestoneInfo(cwd);
|
|
297
|
+
if (milestone && milestone.version) {
|
|
298
|
+
branchName = config.milestone_branch_template
|
|
299
|
+
.replace('{milestone}', milestone.version)
|
|
300
|
+
.replace('{slug}', generateSlugInternal(milestone.name) || 'milestone');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (branchName) {
|
|
304
|
+
const currentBranch = execGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
305
|
+
if (currentBranch.exitCode === 0 && currentBranch.stdout.trim() !== branchName) {
|
|
306
|
+
// Create branch if it doesn't exist, or switch to it if it does
|
|
307
|
+
const create = execGit(cwd, ['checkout', '-b', branchName]);
|
|
308
|
+
if (create.exitCode !== 0) {
|
|
309
|
+
execGit(cwd, ['checkout', branchName]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
237
315
|
// Stage files
|
|
238
316
|
const filesToStage = files && files.length > 0 ? files : ['.planning/'];
|
|
239
317
|
for (const file of filesToStage) {
|
|
240
|
-
|
|
318
|
+
const fullPath = path.join(cwd, file);
|
|
319
|
+
if (!fs.existsSync(fullPath)) {
|
|
320
|
+
// File was deleted/moved — stage the deletion
|
|
321
|
+
execGit(cwd, ['rm', '--cached', '--ignore-unmatch', file]);
|
|
322
|
+
} else {
|
|
323
|
+
execGit(cwd, ['add', file]);
|
|
324
|
+
}
|
|
241
325
|
}
|
|
242
326
|
|
|
243
|
-
// Commit
|
|
327
|
+
// Commit (--no-verify skips pre-commit hooks, used by parallel executor agents)
|
|
244
328
|
const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', message];
|
|
329
|
+
if (noVerify) commitArgs.push('--no-verify');
|
|
245
330
|
const commitResult = execGit(cwd, commitArgs);
|
|
246
331
|
if (commitResult.exitCode !== 0) {
|
|
247
332
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
@@ -261,6 +346,74 @@ function cmdCommit(cwd, message, files, raw, amend) {
|
|
|
261
346
|
output(result, raw, hash || 'committed');
|
|
262
347
|
}
|
|
263
348
|
|
|
349
|
+
function cmdCommitToSubrepo(cwd, message, files, raw) {
|
|
350
|
+
if (!message) {
|
|
351
|
+
error('commit message required');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const config = loadConfig(cwd);
|
|
355
|
+
const subRepos = config.sub_repos;
|
|
356
|
+
|
|
357
|
+
if (!subRepos || subRepos.length === 0) {
|
|
358
|
+
error('no sub_repos configured in .planning/config.json');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!files || files.length === 0) {
|
|
362
|
+
error('--files required for commit-to-subrepo');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Group files by sub-repo prefix
|
|
366
|
+
const grouped = {};
|
|
367
|
+
const unmatched = [];
|
|
368
|
+
for (const file of files) {
|
|
369
|
+
const match = subRepos.find(repo => file.startsWith(repo + '/'));
|
|
370
|
+
if (match) {
|
|
371
|
+
if (!grouped[match]) grouped[match] = [];
|
|
372
|
+
grouped[match].push(file);
|
|
373
|
+
} else {
|
|
374
|
+
unmatched.push(file);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (unmatched.length > 0) {
|
|
379
|
+
process.stderr.write(`Warning: ${unmatched.length} file(s) did not match any sub-repo prefix: ${unmatched.join(', ')}\n`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const repos = {};
|
|
383
|
+
for (const [repo, repoFiles] of Object.entries(grouped)) {
|
|
384
|
+
const repoCwd = path.join(cwd, repo);
|
|
385
|
+
|
|
386
|
+
// Stage files (strip sub-repo prefix for paths relative to that repo)
|
|
387
|
+
for (const file of repoFiles) {
|
|
388
|
+
const relativePath = file.slice(repo.length + 1);
|
|
389
|
+
execGit(repoCwd, ['add', relativePath]);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Commit
|
|
393
|
+
const commitResult = execGit(repoCwd, ['commit', '-m', message]);
|
|
394
|
+
if (commitResult.exitCode !== 0) {
|
|
395
|
+
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
396
|
+
repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'nothing_to_commit' };
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'error', error: commitResult.stderr };
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Get hash
|
|
404
|
+
const hashResult = execGit(repoCwd, ['rev-parse', '--short', 'HEAD']);
|
|
405
|
+
const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
|
|
406
|
+
repos[repo] = { committed: true, hash, files: repoFiles };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const result = {
|
|
410
|
+
committed: Object.values(repos).some(r => r.committed),
|
|
411
|
+
repos,
|
|
412
|
+
unmatched: unmatched.length > 0 ? unmatched : undefined,
|
|
413
|
+
};
|
|
414
|
+
output(result, raw, Object.entries(repos).map(([r, v]) => `${r}:${v.hash || 'skip'}`).join(' '));
|
|
415
|
+
}
|
|
416
|
+
|
|
264
417
|
function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
|
|
265
418
|
if (!summaryPath) {
|
|
266
419
|
error('summary-path required for summary-extract');
|
|
@@ -294,7 +447,7 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
|
|
|
294
447
|
// Build full result
|
|
295
448
|
const fullResult = {
|
|
296
449
|
path: summaryPath,
|
|
297
|
-
one_liner: fm['one-liner'] || null,
|
|
450
|
+
one_liner: fm['one-liner'] || extractOneLinerFromBody(content) || null,
|
|
298
451
|
key_files: fm['key-files'] || [],
|
|
299
452
|
tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
|
|
300
453
|
patterns: fm['patterns-established'] || [],
|
|
@@ -380,8 +533,8 @@ async function cmdWebsearch(query, options, raw) {
|
|
|
380
533
|
}
|
|
381
534
|
|
|
382
535
|
function cmdProgressRender(cwd, format, raw) {
|
|
383
|
-
const phasesDir =
|
|
384
|
-
const roadmapPath =
|
|
536
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
537
|
+
const roadmapPath = planningPaths(cwd).roadmap;
|
|
385
538
|
const milestone = getMilestoneInfo(cwd);
|
|
386
539
|
|
|
387
540
|
const phases = [];
|
|
@@ -403,15 +556,11 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
403
556
|
totalPlans += plans;
|
|
404
557
|
totalSummaries += summaries;
|
|
405
558
|
|
|
406
|
-
|
|
407
|
-
if (plans === 0) status = 'Pending';
|
|
408
|
-
else if (summaries >= plans) status = 'Complete';
|
|
409
|
-
else if (summaries > 0) status = 'In Progress';
|
|
410
|
-
else status = 'Planned';
|
|
559
|
+
const status = determinePhaseStatus(plans, summaries, path.join(phasesDir, dir), 'Pending');
|
|
411
560
|
|
|
412
561
|
phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
|
|
413
562
|
}
|
|
414
|
-
} catch {}
|
|
563
|
+
} catch { /* intentionally empty */ }
|
|
415
564
|
|
|
416
565
|
const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
|
|
417
566
|
|
|
@@ -447,13 +596,137 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
447
596
|
}
|
|
448
597
|
}
|
|
449
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Match pending todos against a phase's goal/name/requirements.
|
|
601
|
+
* Returns todos with relevance scores based on keyword, area, and file overlap.
|
|
602
|
+
* Used by discuss-phase to surface relevant todos before scope-setting.
|
|
603
|
+
*/
|
|
604
|
+
function cmdTodoMatchPhase(cwd, phase, raw) {
|
|
605
|
+
if (!phase) { error('phase required for todo match-phase'); }
|
|
606
|
+
|
|
607
|
+
const pendingDir = path.join(planningDir(cwd), 'todos', 'pending');
|
|
608
|
+
const todos = [];
|
|
609
|
+
|
|
610
|
+
// Load pending todos
|
|
611
|
+
try {
|
|
612
|
+
const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
|
|
613
|
+
for (const file of files) {
|
|
614
|
+
try {
|
|
615
|
+
const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
|
|
616
|
+
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
|
617
|
+
const areaMatch = content.match(/^area:\s*(.+)$/m);
|
|
618
|
+
const filesMatch = content.match(/^files:\s*(.+)$/m);
|
|
619
|
+
const body = content.replace(/^(title|area|files|created|priority):.*$/gm, '').trim();
|
|
620
|
+
|
|
621
|
+
todos.push({
|
|
622
|
+
file,
|
|
623
|
+
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
624
|
+
area: areaMatch ? areaMatch[1].trim() : 'general',
|
|
625
|
+
files: filesMatch ? filesMatch[1].trim().split(/[,\s]+/).filter(Boolean) : [],
|
|
626
|
+
body: body.slice(0, 200), // first 200 chars for context
|
|
627
|
+
});
|
|
628
|
+
} catch {}
|
|
629
|
+
}
|
|
630
|
+
} catch {}
|
|
631
|
+
|
|
632
|
+
if (todos.length === 0) {
|
|
633
|
+
output({ phase, matches: [], todo_count: 0 }, raw);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Load phase goal/name from ROADMAP
|
|
638
|
+
const phaseInfo = getRoadmapPhaseInternal(cwd, phase);
|
|
639
|
+
const phaseName = phaseInfo ? (phaseInfo.phase_name || '') : '';
|
|
640
|
+
const phaseGoal = phaseInfo ? (phaseInfo.goal || '') : '';
|
|
641
|
+
const phaseSection = phaseInfo ? (phaseInfo.section || '') : '';
|
|
642
|
+
|
|
643
|
+
// Build keyword set from phase name + goal + section text
|
|
644
|
+
const phaseText = `${phaseName} ${phaseGoal} ${phaseSection}`.toLowerCase();
|
|
645
|
+
const stopWords = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'will', 'are', 'was', 'has', 'have', 'been', 'not', 'but', 'all', 'can', 'into', 'each', 'when', 'any', 'use', 'new']);
|
|
646
|
+
const phaseKeywords = new Set(
|
|
647
|
+
phaseText.split(/[\s\-_/.,;:()\[\]{}|]+/)
|
|
648
|
+
.map(w => w.replace(/[^a-z0-9]/g, ''))
|
|
649
|
+
.filter(w => w.length > 2 && !stopWords.has(w))
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
// Find phase directory to get expected file paths
|
|
653
|
+
const phaseInfoDisk = findPhaseInternal(cwd, phase);
|
|
654
|
+
const phasePlans = [];
|
|
655
|
+
if (phaseInfoDisk && phaseInfoDisk.found) {
|
|
656
|
+
try {
|
|
657
|
+
const phaseDir = path.join(cwd, phaseInfoDisk.directory);
|
|
658
|
+
const planFiles = fs.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md'));
|
|
659
|
+
for (const pf of planFiles) {
|
|
660
|
+
try {
|
|
661
|
+
const planContent = fs.readFileSync(path.join(phaseDir, pf), 'utf-8');
|
|
662
|
+
const fmFiles = planContent.match(/files_modified:\s*\[([^\]]*)\]/);
|
|
663
|
+
if (fmFiles) {
|
|
664
|
+
phasePlans.push(...fmFiles[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean));
|
|
665
|
+
}
|
|
666
|
+
} catch {}
|
|
667
|
+
}
|
|
668
|
+
} catch {}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Score each todo for relevance
|
|
672
|
+
const matches = [];
|
|
673
|
+
for (const todo of todos) {
|
|
674
|
+
let score = 0;
|
|
675
|
+
const reasons = [];
|
|
676
|
+
|
|
677
|
+
// Keyword match: todo title/body terms in phase text
|
|
678
|
+
const todoWords = `${todo.title} ${todo.body}`.toLowerCase()
|
|
679
|
+
.split(/[\s\-_/.,;:()\[\]{}|]+/)
|
|
680
|
+
.map(w => w.replace(/[^a-z0-9]/g, ''))
|
|
681
|
+
.filter(w => w.length > 2 && !stopWords.has(w));
|
|
682
|
+
|
|
683
|
+
const matchedKeywords = todoWords.filter(w => phaseKeywords.has(w));
|
|
684
|
+
if (matchedKeywords.length > 0) {
|
|
685
|
+
score += Math.min(matchedKeywords.length * 0.2, 0.6);
|
|
686
|
+
reasons.push(`keywords: ${[...new Set(matchedKeywords)].slice(0, 5).join(', ')}`);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Area match: todo area appears in phase text
|
|
690
|
+
if (todo.area !== 'general' && phaseText.includes(todo.area.toLowerCase())) {
|
|
691
|
+
score += 0.3;
|
|
692
|
+
reasons.push(`area: ${todo.area}`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// File match: todo files overlap with phase plan files
|
|
696
|
+
if (todo.files.length > 0 && phasePlans.length > 0) {
|
|
697
|
+
const fileOverlap = todo.files.filter(f =>
|
|
698
|
+
phasePlans.some(pf => pf.includes(f) || f.includes(pf))
|
|
699
|
+
);
|
|
700
|
+
if (fileOverlap.length > 0) {
|
|
701
|
+
score += 0.4;
|
|
702
|
+
reasons.push(`files: ${fileOverlap.slice(0, 3).join(', ')}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (score > 0) {
|
|
707
|
+
matches.push({
|
|
708
|
+
file: todo.file,
|
|
709
|
+
title: todo.title,
|
|
710
|
+
area: todo.area,
|
|
711
|
+
score: Math.round(score * 100) / 100,
|
|
712
|
+
reasons,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Sort by score descending
|
|
718
|
+
matches.sort((a, b) => b.score - a.score);
|
|
719
|
+
|
|
720
|
+
output({ phase, matches, todo_count: todos.length }, raw);
|
|
721
|
+
}
|
|
722
|
+
|
|
450
723
|
function cmdTodoComplete(cwd, filename, raw) {
|
|
451
724
|
if (!filename) {
|
|
452
725
|
error('filename required for todo complete');
|
|
453
726
|
}
|
|
454
727
|
|
|
455
|
-
const pendingDir = path.join(cwd, '
|
|
456
|
-
const completedDir = path.join(cwd, '
|
|
728
|
+
const pendingDir = path.join(planningDir(cwd), 'todos', 'pending');
|
|
729
|
+
const completedDir = path.join(planningDir(cwd), 'todos', 'completed');
|
|
457
730
|
const sourcePath = path.join(pendingDir, filename);
|
|
458
731
|
|
|
459
732
|
if (!fs.existsSync(sourcePath)) {
|
|
@@ -511,11 +784,11 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
511
784
|
}
|
|
512
785
|
const slug = generateSlugInternal(name);
|
|
513
786
|
const dirName = `${padded}-${slug}`;
|
|
514
|
-
const phasesParent =
|
|
787
|
+
const phasesParent = planningPaths(cwd).phases;
|
|
515
788
|
fs.mkdirSync(phasesParent, { recursive: true });
|
|
516
789
|
const dirPath = path.join(phasesParent, dirName);
|
|
517
790
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
518
|
-
output({ created: true, directory:
|
|
791
|
+
output({ created: true, directory: toPosixPath(path.relative(cwd, dirPath)), path: dirPath }, raw, dirPath);
|
|
519
792
|
return;
|
|
520
793
|
}
|
|
521
794
|
default:
|
|
@@ -532,6 +805,194 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
532
805
|
output({ created: true, path: relPath }, raw, relPath);
|
|
533
806
|
}
|
|
534
807
|
|
|
808
|
+
function cmdStats(cwd, format, raw) {
|
|
809
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
810
|
+
const roadmapPath = planningPaths(cwd).roadmap;
|
|
811
|
+
const reqPath = planningPaths(cwd).requirements;
|
|
812
|
+
const statePath = planningPaths(cwd).state;
|
|
813
|
+
const milestone = getMilestoneInfo(cwd);
|
|
814
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
815
|
+
|
|
816
|
+
// Phase & plan stats (reuse progress pattern)
|
|
817
|
+
const phasesByNumber = new Map();
|
|
818
|
+
let totalPlans = 0;
|
|
819
|
+
let totalSummaries = 0;
|
|
820
|
+
|
|
821
|
+
try {
|
|
822
|
+
const roadmapContent = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
|
|
823
|
+
const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
824
|
+
let match;
|
|
825
|
+
while ((match = headingPattern.exec(roadmapContent)) !== null) {
|
|
826
|
+
phasesByNumber.set(match[1], {
|
|
827
|
+
number: match[1],
|
|
828
|
+
name: match[2].replace(/\(INSERTED\)/i, '').trim(),
|
|
829
|
+
plans: 0,
|
|
830
|
+
summaries: 0,
|
|
831
|
+
status: 'Not Started',
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
} catch { /* intentionally empty */ }
|
|
835
|
+
|
|
836
|
+
try {
|
|
837
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
838
|
+
const dirs = entries
|
|
839
|
+
.filter(e => e.isDirectory())
|
|
840
|
+
.map(e => e.name)
|
|
841
|
+
.filter(isDirInMilestone)
|
|
842
|
+
.sort((a, b) => comparePhaseNum(a, b));
|
|
843
|
+
|
|
844
|
+
for (const dir of dirs) {
|
|
845
|
+
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
846
|
+
const phaseNum = dm ? dm[1] : dir;
|
|
847
|
+
const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
|
|
848
|
+
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
849
|
+
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
|
850
|
+
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
|
851
|
+
|
|
852
|
+
totalPlans += plans;
|
|
853
|
+
totalSummaries += summaries;
|
|
854
|
+
|
|
855
|
+
const status = determinePhaseStatus(plans, summaries, path.join(phasesDir, dir), 'Not Started');
|
|
856
|
+
|
|
857
|
+
const existing = phasesByNumber.get(phaseNum);
|
|
858
|
+
phasesByNumber.set(phaseNum, {
|
|
859
|
+
number: phaseNum,
|
|
860
|
+
name: existing?.name || phaseName,
|
|
861
|
+
plans: (existing?.plans || 0) + plans,
|
|
862
|
+
summaries: (existing?.summaries || 0) + summaries,
|
|
863
|
+
status,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
} catch { /* intentionally empty */ }
|
|
867
|
+
|
|
868
|
+
const phases = [...phasesByNumber.values()].sort((a, b) => comparePhaseNum(a.number, b.number));
|
|
869
|
+
const completedPhases = phases.filter(p => p.status === 'Complete').length;
|
|
870
|
+
const planPercent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
|
|
871
|
+
const percent = phases.length > 0 ? Math.min(100, Math.round((completedPhases / phases.length) * 100)) : 0;
|
|
872
|
+
|
|
873
|
+
// Requirements stats
|
|
874
|
+
let requirementsTotal = 0;
|
|
875
|
+
let requirementsComplete = 0;
|
|
876
|
+
try {
|
|
877
|
+
if (fs.existsSync(reqPath)) {
|
|
878
|
+
const reqContent = fs.readFileSync(reqPath, 'utf-8');
|
|
879
|
+
const checked = reqContent.match(/^- \[x\] \*\*/gm);
|
|
880
|
+
const unchecked = reqContent.match(/^- \[ \] \*\*/gm);
|
|
881
|
+
requirementsComplete = checked ? checked.length : 0;
|
|
882
|
+
requirementsTotal = requirementsComplete + (unchecked ? unchecked.length : 0);
|
|
883
|
+
}
|
|
884
|
+
} catch { /* intentionally empty */ }
|
|
885
|
+
|
|
886
|
+
// Last activity from STATE.md
|
|
887
|
+
let lastActivity = null;
|
|
888
|
+
try {
|
|
889
|
+
if (fs.existsSync(statePath)) {
|
|
890
|
+
const stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
891
|
+
const activityMatch = stateContent.match(/^last_activity:\s*(.+)$/im)
|
|
892
|
+
|| stateContent.match(/\*\*Last Activity:\*\*\s*(.+)/i)
|
|
893
|
+
|| stateContent.match(/^Last Activity:\s*(.+)$/im)
|
|
894
|
+
|| stateContent.match(/^Last activity:\s*(.+)$/im);
|
|
895
|
+
if (activityMatch) lastActivity = activityMatch[1].trim();
|
|
896
|
+
}
|
|
897
|
+
} catch { /* intentionally empty */ }
|
|
898
|
+
|
|
899
|
+
// Git stats
|
|
900
|
+
let gitCommits = 0;
|
|
901
|
+
let gitFirstCommitDate = null;
|
|
902
|
+
const commitCount = execGit(cwd, ['rev-list', '--count', 'HEAD']);
|
|
903
|
+
if (commitCount.exitCode === 0) {
|
|
904
|
+
gitCommits = parseInt(commitCount.stdout, 10) || 0;
|
|
905
|
+
}
|
|
906
|
+
const rootHash = execGit(cwd, ['rev-list', '--max-parents=0', 'HEAD']);
|
|
907
|
+
if (rootHash.exitCode === 0 && rootHash.stdout) {
|
|
908
|
+
const firstCommit = rootHash.stdout.split('\n')[0].trim();
|
|
909
|
+
const firstDate = execGit(cwd, ['show', '-s', '--format=%as', firstCommit]);
|
|
910
|
+
if (firstDate.exitCode === 0) {
|
|
911
|
+
gitFirstCommitDate = firstDate.stdout || null;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const result = {
|
|
916
|
+
milestone_version: milestone.version,
|
|
917
|
+
milestone_name: milestone.name,
|
|
918
|
+
phases,
|
|
919
|
+
phases_completed: completedPhases,
|
|
920
|
+
phases_total: phases.length,
|
|
921
|
+
total_plans: totalPlans,
|
|
922
|
+
total_summaries: totalSummaries,
|
|
923
|
+
percent,
|
|
924
|
+
plan_percent: planPercent,
|
|
925
|
+
requirements_total: requirementsTotal,
|
|
926
|
+
requirements_complete: requirementsComplete,
|
|
927
|
+
git_commits: gitCommits,
|
|
928
|
+
git_first_commit_date: gitFirstCommitDate,
|
|
929
|
+
last_activity: lastActivity,
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
if (format === 'table') {
|
|
933
|
+
const barWidth = 10;
|
|
934
|
+
const filled = Math.round((percent / 100) * barWidth);
|
|
935
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
936
|
+
let out = `# ${milestone.version} ${milestone.name} \u2014 Statistics\n\n`;
|
|
937
|
+
out += `**Progress:** [${bar}] ${completedPhases}/${phases.length} phases (${percent}%)\n`;
|
|
938
|
+
if (totalPlans > 0) {
|
|
939
|
+
out += `**Plans:** ${totalSummaries}/${totalPlans} complete (${planPercent}%)\n`;
|
|
940
|
+
}
|
|
941
|
+
out += `**Phases:** ${completedPhases}/${phases.length} complete\n`;
|
|
942
|
+
if (requirementsTotal > 0) {
|
|
943
|
+
out += `**Requirements:** ${requirementsComplete}/${requirementsTotal} complete\n`;
|
|
944
|
+
}
|
|
945
|
+
out += '\n';
|
|
946
|
+
out += `| Phase | Name | Plans | Completed | Status |\n`;
|
|
947
|
+
out += `|-------|------|-------|-----------|--------|\n`;
|
|
948
|
+
for (const p of phases) {
|
|
949
|
+
out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} |\n`;
|
|
950
|
+
}
|
|
951
|
+
if (gitCommits > 0) {
|
|
952
|
+
out += `\n**Git:** ${gitCommits} commits`;
|
|
953
|
+
if (gitFirstCommitDate) out += ` (since ${gitFirstCommitDate})`;
|
|
954
|
+
out += '\n';
|
|
955
|
+
}
|
|
956
|
+
if (lastActivity) out += `**Last activity:** ${lastActivity}\n`;
|
|
957
|
+
output({ rendered: out }, raw, out);
|
|
958
|
+
} else {
|
|
959
|
+
output(result, raw);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Check whether a commit should be allowed based on commit_docs config.
|
|
965
|
+
* When commit_docs is false, rejects commits that stage .planning/ files.
|
|
966
|
+
* Intended for use as a pre-commit hook guard.
|
|
967
|
+
*/
|
|
968
|
+
function cmdCheckCommit(cwd, raw) {
|
|
969
|
+
const config = loadConfig(cwd);
|
|
970
|
+
|
|
971
|
+
// If commit_docs is true (or not set), allow all commits
|
|
972
|
+
if (config.commit_docs !== false) {
|
|
973
|
+
output({ allowed: true, reason: 'commit_docs_enabled' }, raw, 'allowed');
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// commit_docs is false — check if any .planning/ files are staged
|
|
978
|
+
try {
|
|
979
|
+
const staged = execSync('git diff --cached --name-only', { cwd, encoding: 'utf-8' }).trim();
|
|
980
|
+
const planningFiles = staged.split('\n').filter(f => f.startsWith('.planning/') || f.startsWith('.planning\\'));
|
|
981
|
+
|
|
982
|
+
if (planningFiles.length > 0) {
|
|
983
|
+
error(
|
|
984
|
+
`commit_docs is false but ${planningFiles.length} .planning/ file(s) are staged:\n` +
|
|
985
|
+
planningFiles.map(f => ` ${f}`).join('\n') +
|
|
986
|
+
`\n\nTo unstage: git reset HEAD ${planningFiles.join(' ')}`
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
} catch {
|
|
990
|
+
// git diff --cached failed (no staged files or not a git repo) — allow
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
output({ allowed: true, reason: 'no_planning_files_staged' }, raw, 'allowed');
|
|
994
|
+
}
|
|
995
|
+
|
|
535
996
|
module.exports = {
|
|
536
997
|
cmdGenerateSlug,
|
|
537
998
|
cmdCurrentTimestamp,
|
|
@@ -540,9 +1001,13 @@ module.exports = {
|
|
|
540
1001
|
cmdHistoryDigest,
|
|
541
1002
|
cmdResolveModel,
|
|
542
1003
|
cmdCommit,
|
|
1004
|
+
cmdCommitToSubrepo,
|
|
543
1005
|
cmdSummaryExtract,
|
|
544
1006
|
cmdWebsearch,
|
|
545
1007
|
cmdProgressRender,
|
|
546
1008
|
cmdTodoComplete,
|
|
1009
|
+
cmdTodoMatchPhase,
|
|
547
1010
|
cmdScaffold,
|
|
1011
|
+
cmdStats,
|
|
1012
|
+
cmdCheckCommit,
|
|
548
1013
|
};
|