gsd-opencode 1.22.0 → 1.30.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 +1 -2
- package/agents/gsd-debugger.md +119 -2
- package/agents/gsd-executor.md +25 -4
- package/agents/gsd-integration-checker.md +1 -2
- package/agents/gsd-nyquist-auditor.md +1 -2
- package/agents/gsd-phase-researcher.md +151 -5
- package/agents/gsd-plan-checker.md +71 -5
- package/agents/gsd-planner.md +50 -4
- package/agents/gsd-project-researcher.md +29 -3
- package/agents/gsd-research-synthesizer.md +1 -2
- package/agents/gsd-roadmapper.md +30 -2
- package/agents/gsd-ui-auditor.md +445 -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 +124 -4
- package/commands/gsd/gsd-add-backlog.md +76 -0
- package/commands/gsd/gsd-audit-uat.md +24 -0
- package/commands/gsd/gsd-autonomous.md +41 -0
- package/commands/gsd/gsd-debug.md +5 -0
- package/commands/gsd/gsd-discuss-phase.md +10 -36
- package/commands/gsd/gsd-do.md +30 -0
- package/commands/gsd/gsd-execute-phase.md +20 -2
- package/commands/gsd/gsd-fast.md +30 -0
- package/commands/gsd/gsd-forensics.md +56 -0
- package/commands/gsd/gsd-list-workspaces.md +19 -0
- package/commands/gsd/gsd-manager.md +39 -0
- package/commands/gsd/gsd-milestone-summary.md +51 -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 +3 -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 +4 -2
- package/commands/gsd/gsd-reapply-patches.md +10 -6
- package/commands/gsd/gsd-remove-workspace.md +26 -0
- package/commands/gsd/gsd-research-phase.md +5 -0
- package/commands/gsd/gsd-resume-work.md +1 -1
- package/commands/gsd/gsd-review-backlog.md +61 -0
- package/commands/gsd/gsd-review.md +37 -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 +66 -0
- package/get-shit-done/bin/gsd-tools.cjs +410 -84
- package/get-shit-done/bin/lib/commands.cjs +429 -18
- package/get-shit-done/bin/lib/config.cjs +318 -45
- package/get-shit-done/bin/lib/core.cjs +822 -84
- package/get-shit-done/bin/lib/frontmatter.cjs +78 -41
- package/get-shit-done/bin/lib/init.cjs +836 -104
- package/get-shit-done/bin/lib/milestone.cjs +44 -33
- package/get-shit-done/bin/lib/model-profiles.cjs +68 -0
- package/get-shit-done/bin/lib/phase.cjs +293 -306
- package/get-shit-done/bin/lib/profile-output.cjs +952 -0
- package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
- package/get-shit-done/bin/lib/roadmap.cjs +55 -24
- package/get-shit-done/bin/lib/security.cjs +382 -0
- package/get-shit-done/bin/lib/state.cjs +363 -53
- 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 +104 -36
- package/get-shit-done/bin/lib/workstream.cjs +491 -0
- package/get-shit-done/references/checkpoints.md +12 -10
- package/get-shit-done/references/decimal-phase-calculation.md +2 -3
- 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/planning-config.md +3 -1
- package/get-shit-done/references/user-profiling.md +681 -0
- package/get-shit-done/references/workstream-flag.md +58 -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/claude-md.md +122 -0
- package/get-shit-done/templates/config.json +10 -3
- package/get-shit-done/templates/context.md +61 -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/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 +2 -2
- package/get-shit-done/workflows/add-tests.md +4 -4
- package/get-shit-done/workflows/add-todo.md +3 -3
- package/get-shit-done/workflows/audit-milestone.md +13 -5
- package/get-shit-done/workflows/audit-uat.md +109 -0
- package/get-shit-done/workflows/autonomous.md +891 -0
- package/get-shit-done/workflows/check-todos.md +2 -2
- package/get-shit-done/workflows/cleanup.md +4 -4
- package/get-shit-done/workflows/complete-milestone.md +9 -6
- package/get-shit-done/workflows/diagnose-issues.md +15 -3
- package/get-shit-done/workflows/discovery-phase.md +2 -2
- package/get-shit-done/workflows/discuss-phase-assumptions.md +653 -0
- package/get-shit-done/workflows/discuss-phase.md +411 -38
- package/get-shit-done/workflows/do.md +104 -0
- package/get-shit-done/workflows/execute-phase.md +405 -18
- 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 +124 -7
- package/get-shit-done/workflows/insert-phase.md +2 -2
- 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 +362 -0
- package/get-shit-done/workflows/map-codebase.md +74 -13
- package/get-shit-done/workflows/milestone-summary.md +223 -0
- package/get-shit-done/workflows/new-milestone.md +120 -18
- package/get-shit-done/workflows/new-project.md +178 -39
- 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 +62 -8
- package/get-shit-done/workflows/plan-milestone-gaps.md +4 -5
- package/get-shit-done/workflows/plan-phase.md +332 -33
- 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 +145 -20
- package/get-shit-done/workflows/quick.md +205 -49
- 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 +11 -3
- package/get-shit-done/workflows/resume-project.md +35 -16
- package/get-shit-done/workflows/review.md +228 -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 +80 -11
- package/get-shit-done/workflows/ship.md +228 -0
- package/get-shit-done/workflows/stats.md +60 -0
- package/get-shit-done/workflows/transition.md +147 -20
- package/get-shit-done/workflows/ui-phase.md +302 -0
- package/get-shit-done/workflows/ui-review.md +165 -0
- package/get-shit-done/workflows/update.md +108 -25
- package/get-shit-done/workflows/validate-phase.md +15 -8
- package/get-shit-done/workflows/verify-phase.md +16 -5
- package/get-shit-done/workflows/verify-work.md +72 -18
- 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,9 @@
|
|
|
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');
|
|
9
10
|
|
|
10
11
|
function cmdGenerateSlug(text, raw) {
|
|
11
12
|
if (!text) {
|
|
@@ -42,7 +43,7 @@ function cmdCurrentTimestamp(format, raw) {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function cmdListTodos(cwd, area, raw) {
|
|
45
|
-
const pendingDir = path.join(cwd, '
|
|
46
|
+
const pendingDir = path.join(planningDir(cwd), 'todos', 'pending');
|
|
46
47
|
|
|
47
48
|
let count = 0;
|
|
48
49
|
const todos = [];
|
|
@@ -68,11 +69,11 @@ function cmdListTodos(cwd, area, raw) {
|
|
|
68
69
|
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
|
69
70
|
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
70
71
|
area: todoArea,
|
|
71
|
-
path: toPosixPath(path.
|
|
72
|
+
path: toPosixPath(path.relative(cwd, path.join(pendingDir, file))),
|
|
72
73
|
});
|
|
73
|
-
} catch {}
|
|
74
|
+
} catch { /* intentionally empty */ }
|
|
74
75
|
}
|
|
75
|
-
} catch {}
|
|
76
|
+
} catch { /* intentionally empty */ }
|
|
76
77
|
|
|
77
78
|
const result = { count, todos };
|
|
78
79
|
output(result, raw, count.toString());
|
|
@@ -83,6 +84,11 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
|
|
|
83
84
|
error('path required for verification');
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// Reject null bytes and validate path does not contain traversal attempts
|
|
88
|
+
if (targetPath.includes('\0')) {
|
|
89
|
+
error('path contains null bytes');
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
|
|
87
93
|
|
|
88
94
|
try {
|
|
@@ -97,7 +103,7 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
|
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
function cmdHistoryDigest(cwd, raw) {
|
|
100
|
-
const phasesDir =
|
|
106
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
101
107
|
const digest = { phases: {}, decisions: [], tech_stack: new Set() };
|
|
102
108
|
|
|
103
109
|
// Collect all phase directories: archived + current
|
|
@@ -119,7 +125,7 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
119
125
|
for (const dir of currentDirs) {
|
|
120
126
|
allPhaseDirs.push({ name: dir, fullPath: path.join(phasesDir, dir), milestone: null });
|
|
121
127
|
}
|
|
122
|
-
} catch {}
|
|
128
|
+
} catch { /* intentionally empty */ }
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
if (allPhaseDirs.length === 0) {
|
|
@@ -213,11 +219,18 @@ function cmdResolveModel(cwd, agentType, raw) {
|
|
|
213
219
|
output(result, raw, model);
|
|
214
220
|
}
|
|
215
221
|
|
|
216
|
-
function cmdCommit(cwd, message, files, raw, amend) {
|
|
222
|
+
function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
217
223
|
if (!message && !amend) {
|
|
218
224
|
error('commit message required');
|
|
219
225
|
}
|
|
220
226
|
|
|
227
|
+
// Sanitize commit message: strip invisible chars and injection markers
|
|
228
|
+
// that could hijack agent context when commit messages are read back
|
|
229
|
+
if (message) {
|
|
230
|
+
const { sanitizeForPrompt } = require('./security.cjs');
|
|
231
|
+
message = sanitizeForPrompt(message);
|
|
232
|
+
}
|
|
233
|
+
|
|
221
234
|
const config = loadConfig(cwd);
|
|
222
235
|
|
|
223
236
|
// Check commit_docs config
|
|
@@ -234,14 +247,58 @@ function cmdCommit(cwd, message, files, raw, amend) {
|
|
|
234
247
|
return;
|
|
235
248
|
}
|
|
236
249
|
|
|
250
|
+
// Ensure branching strategy branch exists before first commit (#1278).
|
|
251
|
+
// Pre-execution workflows (discuss, plan, research) commit artifacts but the branch
|
|
252
|
+
// was previously only created during execute-phase — too late.
|
|
253
|
+
if (config.branching_strategy && config.branching_strategy !== 'none') {
|
|
254
|
+
let branchName = null;
|
|
255
|
+
if (config.branching_strategy === 'phase') {
|
|
256
|
+
// Determine which phase we're committing for from the file paths
|
|
257
|
+
const phaseMatch = (files || []).join(' ').match(/(\d+)-/);
|
|
258
|
+
if (phaseMatch) {
|
|
259
|
+
const phaseNum = phaseMatch[1];
|
|
260
|
+
const phaseInfo = findPhaseInternal(cwd, phaseNum);
|
|
261
|
+
if (phaseInfo) {
|
|
262
|
+
branchName = config.phase_branch_template
|
|
263
|
+
.replace('{phase}', phaseInfo.phase_number)
|
|
264
|
+
.replace('{slug}', phaseInfo.phase_slug || 'phase');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} else if (config.branching_strategy === 'milestone') {
|
|
268
|
+
const milestone = getMilestoneInfo(cwd);
|
|
269
|
+
if (milestone && milestone.version) {
|
|
270
|
+
branchName = config.milestone_branch_template
|
|
271
|
+
.replace('{milestone}', milestone.version)
|
|
272
|
+
.replace('{slug}', generateSlugInternal(milestone.name) || 'milestone');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (branchName) {
|
|
276
|
+
const currentBranch = execGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
277
|
+
if (currentBranch.exitCode === 0 && currentBranch.stdout.trim() !== branchName) {
|
|
278
|
+
// Create branch if it doesn't exist, or switch to it if it does
|
|
279
|
+
const create = execGit(cwd, ['checkout', '-b', branchName]);
|
|
280
|
+
if (create.exitCode !== 0) {
|
|
281
|
+
execGit(cwd, ['checkout', branchName]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
237
287
|
// Stage files
|
|
238
288
|
const filesToStage = files && files.length > 0 ? files : ['.planning/'];
|
|
239
289
|
for (const file of filesToStage) {
|
|
240
|
-
|
|
290
|
+
const fullPath = path.join(cwd, file);
|
|
291
|
+
if (!fs.existsSync(fullPath)) {
|
|
292
|
+
// File was deleted/moved — stage the deletion
|
|
293
|
+
execGit(cwd, ['rm', '--cached', '--ignore-unmatch', file]);
|
|
294
|
+
} else {
|
|
295
|
+
execGit(cwd, ['add', file]);
|
|
296
|
+
}
|
|
241
297
|
}
|
|
242
298
|
|
|
243
|
-
// Commit
|
|
299
|
+
// Commit (--no-verify skips pre-commit hooks, used by parallel executor agents)
|
|
244
300
|
const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', message];
|
|
301
|
+
if (noVerify) commitArgs.push('--no-verify');
|
|
245
302
|
const commitResult = execGit(cwd, commitArgs);
|
|
246
303
|
if (commitResult.exitCode !== 0) {
|
|
247
304
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
@@ -261,6 +318,74 @@ function cmdCommit(cwd, message, files, raw, amend) {
|
|
|
261
318
|
output(result, raw, hash || 'committed');
|
|
262
319
|
}
|
|
263
320
|
|
|
321
|
+
function cmdCommitToSubrepo(cwd, message, files, raw) {
|
|
322
|
+
if (!message) {
|
|
323
|
+
error('commit message required');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const config = loadConfig(cwd);
|
|
327
|
+
const subRepos = config.sub_repos;
|
|
328
|
+
|
|
329
|
+
if (!subRepos || subRepos.length === 0) {
|
|
330
|
+
error('no sub_repos configured in .planning/config.json');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!files || files.length === 0) {
|
|
334
|
+
error('--files required for commit-to-subrepo');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Group files by sub-repo prefix
|
|
338
|
+
const grouped = {};
|
|
339
|
+
const unmatched = [];
|
|
340
|
+
for (const file of files) {
|
|
341
|
+
const match = subRepos.find(repo => file.startsWith(repo + '/'));
|
|
342
|
+
if (match) {
|
|
343
|
+
if (!grouped[match]) grouped[match] = [];
|
|
344
|
+
grouped[match].push(file);
|
|
345
|
+
} else {
|
|
346
|
+
unmatched.push(file);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (unmatched.length > 0) {
|
|
351
|
+
process.stderr.write(`Warning: ${unmatched.length} file(s) did not match any sub-repo prefix: ${unmatched.join(', ')}\n`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const repos = {};
|
|
355
|
+
for (const [repo, repoFiles] of Object.entries(grouped)) {
|
|
356
|
+
const repoCwd = path.join(cwd, repo);
|
|
357
|
+
|
|
358
|
+
// Stage files (strip sub-repo prefix for paths relative to that repo)
|
|
359
|
+
for (const file of repoFiles) {
|
|
360
|
+
const relativePath = file.slice(repo.length + 1);
|
|
361
|
+
execGit(repoCwd, ['add', relativePath]);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Commit
|
|
365
|
+
const commitResult = execGit(repoCwd, ['commit', '-m', message]);
|
|
366
|
+
if (commitResult.exitCode !== 0) {
|
|
367
|
+
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
368
|
+
repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'nothing_to_commit' };
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'error', error: commitResult.stderr };
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Get hash
|
|
376
|
+
const hashResult = execGit(repoCwd, ['rev-parse', '--short', 'HEAD']);
|
|
377
|
+
const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
|
|
378
|
+
repos[repo] = { committed: true, hash, files: repoFiles };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const result = {
|
|
382
|
+
committed: Object.values(repos).some(r => r.committed),
|
|
383
|
+
repos,
|
|
384
|
+
unmatched: unmatched.length > 0 ? unmatched : undefined,
|
|
385
|
+
};
|
|
386
|
+
output(result, raw, Object.entries(repos).map(([r, v]) => `${r}:${v.hash || 'skip'}`).join(' '));
|
|
387
|
+
}
|
|
388
|
+
|
|
264
389
|
function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
|
|
265
390
|
if (!summaryPath) {
|
|
266
391
|
error('summary-path required for summary-extract');
|
|
@@ -294,7 +419,7 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
|
|
|
294
419
|
// Build full result
|
|
295
420
|
const fullResult = {
|
|
296
421
|
path: summaryPath,
|
|
297
|
-
one_liner: fm['one-liner'] || null,
|
|
422
|
+
one_liner: fm['one-liner'] || extractOneLinerFromBody(content) || null,
|
|
298
423
|
key_files: fm['key-files'] || [],
|
|
299
424
|
tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
|
|
300
425
|
patterns: fm['patterns-established'] || [],
|
|
@@ -380,8 +505,8 @@ async function cmdWebsearch(query, options, raw) {
|
|
|
380
505
|
}
|
|
381
506
|
|
|
382
507
|
function cmdProgressRender(cwd, format, raw) {
|
|
383
|
-
const phasesDir =
|
|
384
|
-
const roadmapPath =
|
|
508
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
509
|
+
const roadmapPath = planningPaths(cwd).roadmap;
|
|
385
510
|
const milestone = getMilestoneInfo(cwd);
|
|
386
511
|
|
|
387
512
|
const phases = [];
|
|
@@ -411,7 +536,7 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
411
536
|
|
|
412
537
|
phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
|
|
413
538
|
}
|
|
414
|
-
} catch {}
|
|
539
|
+
} catch { /* intentionally empty */ }
|
|
415
540
|
|
|
416
541
|
const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
|
|
417
542
|
|
|
@@ -447,13 +572,137 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
447
572
|
}
|
|
448
573
|
}
|
|
449
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Match pending todos against a phase's goal/name/requirements.
|
|
577
|
+
* Returns todos with relevance scores based on keyword, area, and file overlap.
|
|
578
|
+
* Used by discuss-phase to surface relevant todos before scope-setting.
|
|
579
|
+
*/
|
|
580
|
+
function cmdTodoMatchPhase(cwd, phase, raw) {
|
|
581
|
+
if (!phase) { error('phase required for todo match-phase'); }
|
|
582
|
+
|
|
583
|
+
const pendingDir = path.join(planningDir(cwd), 'todos', 'pending');
|
|
584
|
+
const todos = [];
|
|
585
|
+
|
|
586
|
+
// Load pending todos
|
|
587
|
+
try {
|
|
588
|
+
const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
|
|
589
|
+
for (const file of files) {
|
|
590
|
+
try {
|
|
591
|
+
const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
|
|
592
|
+
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
|
593
|
+
const areaMatch = content.match(/^area:\s*(.+)$/m);
|
|
594
|
+
const filesMatch = content.match(/^files:\s*(.+)$/m);
|
|
595
|
+
const body = content.replace(/^(title|area|files|created|priority):.*$/gm, '').trim();
|
|
596
|
+
|
|
597
|
+
todos.push({
|
|
598
|
+
file,
|
|
599
|
+
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
600
|
+
area: areaMatch ? areaMatch[1].trim() : 'general',
|
|
601
|
+
files: filesMatch ? filesMatch[1].trim().split(/[,\s]+/).filter(Boolean) : [],
|
|
602
|
+
body: body.slice(0, 200), // first 200 chars for context
|
|
603
|
+
});
|
|
604
|
+
} catch {}
|
|
605
|
+
}
|
|
606
|
+
} catch {}
|
|
607
|
+
|
|
608
|
+
if (todos.length === 0) {
|
|
609
|
+
output({ phase, matches: [], todo_count: 0 }, raw);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Load phase goal/name from ROADMAP
|
|
614
|
+
const phaseInfo = getRoadmapPhaseInternal(cwd, phase);
|
|
615
|
+
const phaseName = phaseInfo ? (phaseInfo.phase_name || '') : '';
|
|
616
|
+
const phaseGoal = phaseInfo ? (phaseInfo.goal || '') : '';
|
|
617
|
+
const phaseSection = phaseInfo ? (phaseInfo.section || '') : '';
|
|
618
|
+
|
|
619
|
+
// Build keyword set from phase name + goal + section text
|
|
620
|
+
const phaseText = `${phaseName} ${phaseGoal} ${phaseSection}`.toLowerCase();
|
|
621
|
+
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']);
|
|
622
|
+
const phaseKeywords = new Set(
|
|
623
|
+
phaseText.split(/[\s\-_/.,;:()\[\]{}|]+/)
|
|
624
|
+
.map(w => w.replace(/[^a-z0-9]/g, ''))
|
|
625
|
+
.filter(w => w.length > 2 && !stopWords.has(w))
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// Find phase directory to get expected file paths
|
|
629
|
+
const phaseInfoDisk = findPhaseInternal(cwd, phase);
|
|
630
|
+
const phasePlans = [];
|
|
631
|
+
if (phaseInfoDisk && phaseInfoDisk.found) {
|
|
632
|
+
try {
|
|
633
|
+
const phaseDir = path.join(cwd, phaseInfoDisk.directory);
|
|
634
|
+
const planFiles = fs.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md'));
|
|
635
|
+
for (const pf of planFiles) {
|
|
636
|
+
try {
|
|
637
|
+
const planContent = fs.readFileSync(path.join(phaseDir, pf), 'utf-8');
|
|
638
|
+
const fmFiles = planContent.match(/files_modified:\s*\[([^\]]*)\]/);
|
|
639
|
+
if (fmFiles) {
|
|
640
|
+
phasePlans.push(...fmFiles[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean));
|
|
641
|
+
}
|
|
642
|
+
} catch {}
|
|
643
|
+
}
|
|
644
|
+
} catch {}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Score each todo for relevance
|
|
648
|
+
const matches = [];
|
|
649
|
+
for (const todo of todos) {
|
|
650
|
+
let score = 0;
|
|
651
|
+
const reasons = [];
|
|
652
|
+
|
|
653
|
+
// Keyword match: todo title/body terms in phase text
|
|
654
|
+
const todoWords = `${todo.title} ${todo.body}`.toLowerCase()
|
|
655
|
+
.split(/[\s\-_/.,;:()\[\]{}|]+/)
|
|
656
|
+
.map(w => w.replace(/[^a-z0-9]/g, ''))
|
|
657
|
+
.filter(w => w.length > 2 && !stopWords.has(w));
|
|
658
|
+
|
|
659
|
+
const matchedKeywords = todoWords.filter(w => phaseKeywords.has(w));
|
|
660
|
+
if (matchedKeywords.length > 0) {
|
|
661
|
+
score += Math.min(matchedKeywords.length * 0.2, 0.6);
|
|
662
|
+
reasons.push(`keywords: ${[...new Set(matchedKeywords)].slice(0, 5).join(', ')}`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Area match: todo area appears in phase text
|
|
666
|
+
if (todo.area !== 'general' && phaseText.includes(todo.area.toLowerCase())) {
|
|
667
|
+
score += 0.3;
|
|
668
|
+
reasons.push(`area: ${todo.area}`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// File match: todo files overlap with phase plan files
|
|
672
|
+
if (todo.files.length > 0 && phasePlans.length > 0) {
|
|
673
|
+
const fileOverlap = todo.files.filter(f =>
|
|
674
|
+
phasePlans.some(pf => pf.includes(f) || f.includes(pf))
|
|
675
|
+
);
|
|
676
|
+
if (fileOverlap.length > 0) {
|
|
677
|
+
score += 0.4;
|
|
678
|
+
reasons.push(`files: ${fileOverlap.slice(0, 3).join(', ')}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (score > 0) {
|
|
683
|
+
matches.push({
|
|
684
|
+
file: todo.file,
|
|
685
|
+
title: todo.title,
|
|
686
|
+
area: todo.area,
|
|
687
|
+
score: Math.round(score * 100) / 100,
|
|
688
|
+
reasons,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Sort by score descending
|
|
694
|
+
matches.sort((a, b) => b.score - a.score);
|
|
695
|
+
|
|
696
|
+
output({ phase, matches, todo_count: todos.length }, raw);
|
|
697
|
+
}
|
|
698
|
+
|
|
450
699
|
function cmdTodoComplete(cwd, filename, raw) {
|
|
451
700
|
if (!filename) {
|
|
452
701
|
error('filename required for todo complete');
|
|
453
702
|
}
|
|
454
703
|
|
|
455
|
-
const pendingDir = path.join(cwd, '
|
|
456
|
-
const completedDir = path.join(cwd, '
|
|
704
|
+
const pendingDir = path.join(planningDir(cwd), 'todos', 'pending');
|
|
705
|
+
const completedDir = path.join(planningDir(cwd), 'todos', 'completed');
|
|
457
706
|
const sourcePath = path.join(pendingDir, filename);
|
|
458
707
|
|
|
459
708
|
if (!fs.existsSync(sourcePath)) {
|
|
@@ -511,11 +760,11 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
511
760
|
}
|
|
512
761
|
const slug = generateSlugInternal(name);
|
|
513
762
|
const dirName = `${padded}-${slug}`;
|
|
514
|
-
const phasesParent =
|
|
763
|
+
const phasesParent = planningPaths(cwd).phases;
|
|
515
764
|
fs.mkdirSync(phasesParent, { recursive: true });
|
|
516
765
|
const dirPath = path.join(phasesParent, dirName);
|
|
517
766
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
518
|
-
output({ created: true, directory:
|
|
767
|
+
output({ created: true, directory: toPosixPath(path.relative(cwd, dirPath)), path: dirPath }, raw, dirPath);
|
|
519
768
|
return;
|
|
520
769
|
}
|
|
521
770
|
default:
|
|
@@ -532,6 +781,165 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
532
781
|
output({ created: true, path: relPath }, raw, relPath);
|
|
533
782
|
}
|
|
534
783
|
|
|
784
|
+
function cmdStats(cwd, format, raw) {
|
|
785
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
786
|
+
const roadmapPath = planningPaths(cwd).roadmap;
|
|
787
|
+
const reqPath = planningPaths(cwd).requirements;
|
|
788
|
+
const statePath = planningPaths(cwd).state;
|
|
789
|
+
const milestone = getMilestoneInfo(cwd);
|
|
790
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
791
|
+
|
|
792
|
+
// Phase & plan stats (reuse progress pattern)
|
|
793
|
+
const phasesByNumber = new Map();
|
|
794
|
+
let totalPlans = 0;
|
|
795
|
+
let totalSummaries = 0;
|
|
796
|
+
|
|
797
|
+
try {
|
|
798
|
+
const roadmapContent = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
|
|
799
|
+
const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
800
|
+
let match;
|
|
801
|
+
while ((match = headingPattern.exec(roadmapContent)) !== null) {
|
|
802
|
+
phasesByNumber.set(match[1], {
|
|
803
|
+
number: match[1],
|
|
804
|
+
name: match[2].replace(/\(INSERTED\)/i, '').trim(),
|
|
805
|
+
plans: 0,
|
|
806
|
+
summaries: 0,
|
|
807
|
+
status: 'Not Started',
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
} catch { /* intentionally empty */ }
|
|
811
|
+
|
|
812
|
+
try {
|
|
813
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
814
|
+
const dirs = entries
|
|
815
|
+
.filter(e => e.isDirectory())
|
|
816
|
+
.map(e => e.name)
|
|
817
|
+
.filter(isDirInMilestone)
|
|
818
|
+
.sort((a, b) => comparePhaseNum(a, b));
|
|
819
|
+
|
|
820
|
+
for (const dir of dirs) {
|
|
821
|
+
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
822
|
+
const phaseNum = dm ? dm[1] : dir;
|
|
823
|
+
const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
|
|
824
|
+
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
825
|
+
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
|
826
|
+
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
|
827
|
+
|
|
828
|
+
totalPlans += plans;
|
|
829
|
+
totalSummaries += summaries;
|
|
830
|
+
|
|
831
|
+
let status;
|
|
832
|
+
if (plans === 0) status = 'Not Started';
|
|
833
|
+
else if (summaries >= plans) status = 'Complete';
|
|
834
|
+
else if (summaries > 0) status = 'In Progress';
|
|
835
|
+
else status = 'Planned';
|
|
836
|
+
|
|
837
|
+
const existing = phasesByNumber.get(phaseNum);
|
|
838
|
+
phasesByNumber.set(phaseNum, {
|
|
839
|
+
number: phaseNum,
|
|
840
|
+
name: existing?.name || phaseName,
|
|
841
|
+
plans,
|
|
842
|
+
summaries,
|
|
843
|
+
status,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
} catch { /* intentionally empty */ }
|
|
847
|
+
|
|
848
|
+
const phases = [...phasesByNumber.values()].sort((a, b) => comparePhaseNum(a.number, b.number));
|
|
849
|
+
const completedPhases = phases.filter(p => p.status === 'Complete').length;
|
|
850
|
+
const planPercent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
|
|
851
|
+
const percent = phases.length > 0 ? Math.min(100, Math.round((completedPhases / phases.length) * 100)) : 0;
|
|
852
|
+
|
|
853
|
+
// Requirements stats
|
|
854
|
+
let requirementsTotal = 0;
|
|
855
|
+
let requirementsComplete = 0;
|
|
856
|
+
try {
|
|
857
|
+
if (fs.existsSync(reqPath)) {
|
|
858
|
+
const reqContent = fs.readFileSync(reqPath, 'utf-8');
|
|
859
|
+
const checked = reqContent.match(/^- \[x\] \*\*/gm);
|
|
860
|
+
const unchecked = reqContent.match(/^- \[ \] \*\*/gm);
|
|
861
|
+
requirementsComplete = checked ? checked.length : 0;
|
|
862
|
+
requirementsTotal = requirementsComplete + (unchecked ? unchecked.length : 0);
|
|
863
|
+
}
|
|
864
|
+
} catch { /* intentionally empty */ }
|
|
865
|
+
|
|
866
|
+
// Last activity from STATE.md
|
|
867
|
+
let lastActivity = null;
|
|
868
|
+
try {
|
|
869
|
+
if (fs.existsSync(statePath)) {
|
|
870
|
+
const stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
871
|
+
const activityMatch = stateContent.match(/^last_activity:\s*(.+)$/im)
|
|
872
|
+
|| stateContent.match(/\*\*Last Activity:\*\*\s*(.+)/i)
|
|
873
|
+
|| stateContent.match(/^Last Activity:\s*(.+)$/im)
|
|
874
|
+
|| stateContent.match(/^Last activity:\s*(.+)$/im);
|
|
875
|
+
if (activityMatch) lastActivity = activityMatch[1].trim();
|
|
876
|
+
}
|
|
877
|
+
} catch { /* intentionally empty */ }
|
|
878
|
+
|
|
879
|
+
// Git stats
|
|
880
|
+
let gitCommits = 0;
|
|
881
|
+
let gitFirstCommitDate = null;
|
|
882
|
+
const commitCount = execGit(cwd, ['rev-list', '--count', 'HEAD']);
|
|
883
|
+
if (commitCount.exitCode === 0) {
|
|
884
|
+
gitCommits = parseInt(commitCount.stdout, 10) || 0;
|
|
885
|
+
}
|
|
886
|
+
const rootHash = execGit(cwd, ['rev-list', '--max-parents=0', 'HEAD']);
|
|
887
|
+
if (rootHash.exitCode === 0 && rootHash.stdout) {
|
|
888
|
+
const firstCommit = rootHash.stdout.split('\n')[0].trim();
|
|
889
|
+
const firstDate = execGit(cwd, ['show', '-s', '--format=%as', firstCommit]);
|
|
890
|
+
if (firstDate.exitCode === 0) {
|
|
891
|
+
gitFirstCommitDate = firstDate.stdout || null;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const result = {
|
|
896
|
+
milestone_version: milestone.version,
|
|
897
|
+
milestone_name: milestone.name,
|
|
898
|
+
phases,
|
|
899
|
+
phases_completed: completedPhases,
|
|
900
|
+
phases_total: phases.length,
|
|
901
|
+
total_plans: totalPlans,
|
|
902
|
+
total_summaries: totalSummaries,
|
|
903
|
+
percent,
|
|
904
|
+
plan_percent: planPercent,
|
|
905
|
+
requirements_total: requirementsTotal,
|
|
906
|
+
requirements_complete: requirementsComplete,
|
|
907
|
+
git_commits: gitCommits,
|
|
908
|
+
git_first_commit_date: gitFirstCommitDate,
|
|
909
|
+
last_activity: lastActivity,
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
if (format === 'table') {
|
|
913
|
+
const barWidth = 10;
|
|
914
|
+
const filled = Math.round((percent / 100) * barWidth);
|
|
915
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
916
|
+
let out = `# ${milestone.version} ${milestone.name} \u2014 Statistics\n\n`;
|
|
917
|
+
out += `**Progress:** [${bar}] ${completedPhases}/${phases.length} phases (${percent}%)\n`;
|
|
918
|
+
if (totalPlans > 0) {
|
|
919
|
+
out += `**Plans:** ${totalSummaries}/${totalPlans} complete (${planPercent}%)\n`;
|
|
920
|
+
}
|
|
921
|
+
out += `**Phases:** ${completedPhases}/${phases.length} complete\n`;
|
|
922
|
+
if (requirementsTotal > 0) {
|
|
923
|
+
out += `**Requirements:** ${requirementsComplete}/${requirementsTotal} complete\n`;
|
|
924
|
+
}
|
|
925
|
+
out += '\n';
|
|
926
|
+
out += `| Phase | Name | Plans | Completed | Status |\n`;
|
|
927
|
+
out += `|-------|------|-------|-----------|--------|\n`;
|
|
928
|
+
for (const p of phases) {
|
|
929
|
+
out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} |\n`;
|
|
930
|
+
}
|
|
931
|
+
if (gitCommits > 0) {
|
|
932
|
+
out += `\n**Git:** ${gitCommits} commits`;
|
|
933
|
+
if (gitFirstCommitDate) out += ` (since ${gitFirstCommitDate})`;
|
|
934
|
+
out += '\n';
|
|
935
|
+
}
|
|
936
|
+
if (lastActivity) out += `**Last activity:** ${lastActivity}\n`;
|
|
937
|
+
output({ rendered: out }, raw, out);
|
|
938
|
+
} else {
|
|
939
|
+
output(result, raw);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
535
943
|
module.exports = {
|
|
536
944
|
cmdGenerateSlug,
|
|
537
945
|
cmdCurrentTimestamp,
|
|
@@ -540,9 +948,12 @@ module.exports = {
|
|
|
540
948
|
cmdHistoryDigest,
|
|
541
949
|
cmdResolveModel,
|
|
542
950
|
cmdCommit,
|
|
951
|
+
cmdCommitToSubrepo,
|
|
543
952
|
cmdSummaryExtract,
|
|
544
953
|
cmdWebsearch,
|
|
545
954
|
cmdProgressRender,
|
|
546
955
|
cmdTodoComplete,
|
|
956
|
+
cmdTodoMatchPhase,
|
|
547
957
|
cmdScaffold,
|
|
958
|
+
cmdStats,
|
|
548
959
|
};
|