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
|
@@ -5,18 +5,77 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { execSync } = require('child_process');
|
|
8
|
-
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs');
|
|
8
|
+
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, normalizePhaseName, planningPaths, planningDir, planningRoot, toPosixPath, output, error, checkAgentsInstalled, phaseTokenMatches } = require('./core.cjs');
|
|
9
9
|
|
|
10
|
-
function
|
|
10
|
+
function getLatestCompletedMilestone(cwd) {
|
|
11
|
+
const milestonesPath = path.join(planningRoot(cwd), 'MILESTONES.md');
|
|
12
|
+
if (!fs.existsSync(milestonesPath)) return null;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const content = fs.readFileSync(milestonesPath, 'utf-8');
|
|
16
|
+
const match = content.match(/^##\s+(v[\d.]+)\s+(.+?)\s+\(Shipped:/m);
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
return {
|
|
19
|
+
version: match[1],
|
|
20
|
+
name: match[2].trim(),
|
|
21
|
+
};
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Inject `project_root` into an init result object.
|
|
29
|
+
* Workflows use this to prefix `.planning/` paths correctly when OpenCode's CWD
|
|
30
|
+
* differs from the project root (e.g., inside a sub-repo).
|
|
31
|
+
*/
|
|
32
|
+
function withProjectRoot(cwd, result) {
|
|
33
|
+
result.project_root = cwd;
|
|
34
|
+
// Inject agent installation status into all init outputs (#1371).
|
|
35
|
+
// Workflows that spawn named subagents use this to detect when agents
|
|
36
|
+
// are missing and would silently fall back to general-purpose.
|
|
37
|
+
const agentStatus = checkAgentsInstalled();
|
|
38
|
+
result.agents_installed = agentStatus.agents_installed;
|
|
39
|
+
result.missing_agents = agentStatus.missing_agents;
|
|
40
|
+
// Inject response_language into all init outputs (#1399).
|
|
41
|
+
// Workflows propagate this to subagent prompts so user-facing questions
|
|
42
|
+
// stay in the configured language across phase boundaries.
|
|
43
|
+
const config = loadConfig(cwd);
|
|
44
|
+
if (config.response_language) {
|
|
45
|
+
result.response_language = config.response_language;
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
11
51
|
if (!phase) {
|
|
12
52
|
error('phase required for init execute-phase');
|
|
13
53
|
}
|
|
14
54
|
|
|
15
55
|
const config = loadConfig(cwd);
|
|
16
|
-
|
|
56
|
+
let phaseInfo = findPhaseInternal(cwd, phase);
|
|
17
57
|
const milestone = getMilestoneInfo(cwd);
|
|
18
58
|
|
|
19
59
|
const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
|
|
60
|
+
|
|
61
|
+
// Fallback to ROADMAP.md if no phase directory exists yet
|
|
62
|
+
if (!phaseInfo && roadmapPhase?.found) {
|
|
63
|
+
const phaseName = roadmapPhase.phase_name;
|
|
64
|
+
phaseInfo = {
|
|
65
|
+
found: true,
|
|
66
|
+
directory: null,
|
|
67
|
+
phase_number: roadmapPhase.phase_number,
|
|
68
|
+
phase_name: phaseName,
|
|
69
|
+
phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
|
|
70
|
+
plans: [],
|
|
71
|
+
summaries: [],
|
|
72
|
+
incomplete_plans: [],
|
|
73
|
+
has_research: false,
|
|
74
|
+
has_context: false,
|
|
75
|
+
has_verification: false,
|
|
76
|
+
has_reviews: false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
20
79
|
const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
|
|
21
80
|
const reqExtracted = reqMatch
|
|
22
81
|
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
|
|
@@ -30,7 +89,9 @@ function cmdInitExecutePhase(cwd, phase, raw) {
|
|
|
30
89
|
|
|
31
90
|
// Config flags
|
|
32
91
|
commit_docs: config.commit_docs,
|
|
92
|
+
sub_repos: config.sub_repos,
|
|
33
93
|
parallelization: config.parallelization,
|
|
94
|
+
context_window: config.context_window,
|
|
34
95
|
branching_strategy: config.branching_strategy,
|
|
35
96
|
phase_branch_template: config.phase_branch_template,
|
|
36
97
|
milestone_branch_template: config.milestone_branch_template,
|
|
@@ -54,6 +115,7 @@ function cmdInitExecutePhase(cwd, phase, raw) {
|
|
|
54
115
|
// Branch name (pre-computed)
|
|
55
116
|
branch_name: config.branching_strategy === 'phase' && phaseInfo
|
|
56
117
|
? config.phase_branch_template
|
|
118
|
+
.replace('{project}', config.project_code || '')
|
|
57
119
|
.replace('{phase}', phaseInfo.phase_number)
|
|
58
120
|
.replace('{slug}', phaseInfo.phase_slug || 'phase')
|
|
59
121
|
: config.branching_strategy === 'milestone'
|
|
@@ -68,27 +130,74 @@ function cmdInitExecutePhase(cwd, phase, raw) {
|
|
|
68
130
|
milestone_slug: generateSlugInternal(milestone.name),
|
|
69
131
|
|
|
70
132
|
// File existence
|
|
71
|
-
state_exists:
|
|
72
|
-
roadmap_exists:
|
|
73
|
-
config_exists:
|
|
133
|
+
state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
|
|
134
|
+
roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
|
|
135
|
+
config_exists: fs.existsSync(path.join(planningDir(cwd), 'config.json')),
|
|
74
136
|
// File paths
|
|
75
|
-
state_path: '
|
|
76
|
-
roadmap_path: '
|
|
77
|
-
config_path: '
|
|
137
|
+
state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
|
|
138
|
+
roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
|
|
139
|
+
config_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'config.json'))),
|
|
78
140
|
};
|
|
79
141
|
|
|
80
|
-
|
|
142
|
+
// Optional --validate: run state validation and include warnings (#1627)
|
|
143
|
+
if (options.validate) {
|
|
144
|
+
try {
|
|
145
|
+
const { cmdStateValidate } = require('./state.cjs');
|
|
146
|
+
// Capture validate output by temporarily redirecting
|
|
147
|
+
const statePath = path.join(planningDir(cwd), 'STATE.md');
|
|
148
|
+
if (fs.existsSync(statePath)) {
|
|
149
|
+
const stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
150
|
+
const { stateExtractField } = require('./state.cjs');
|
|
151
|
+
const status = stateExtractField(stateContent, 'Status') || '';
|
|
152
|
+
result.state_validation_ran = true;
|
|
153
|
+
// Simple inline validation — check for obvious drift
|
|
154
|
+
const warnings = [];
|
|
155
|
+
const phasesPath = planningPaths(cwd).phases;
|
|
156
|
+
if (phaseInfo && phaseInfo.directory && fs.existsSync(path.join(cwd, phaseInfo.directory))) {
|
|
157
|
+
const files = fs.readdirSync(path.join(cwd, phaseInfo.directory));
|
|
158
|
+
const diskPlans = files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
|
159
|
+
const totalPlansRaw = stateExtractField(stateContent, 'Total Plans in Phase');
|
|
160
|
+
const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
|
|
161
|
+
if (totalPlansInPhase !== null && diskPlans !== totalPlansInPhase) {
|
|
162
|
+
warnings.push(`Plan count mismatch: STATE.md says ${totalPlansInPhase}, disk has ${diskPlans}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
result.state_warnings = warnings;
|
|
166
|
+
}
|
|
167
|
+
} catch { /* intentionally empty */ }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
output(withProjectRoot(cwd, result), raw);
|
|
81
171
|
}
|
|
82
172
|
|
|
83
|
-
function cmdInitPlanPhase(cwd, phase, raw) {
|
|
173
|
+
function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
84
174
|
if (!phase) {
|
|
85
175
|
error('phase required for init plan-phase');
|
|
86
176
|
}
|
|
87
177
|
|
|
88
178
|
const config = loadConfig(cwd);
|
|
89
|
-
|
|
179
|
+
let phaseInfo = findPhaseInternal(cwd, phase);
|
|
90
180
|
|
|
91
181
|
const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
|
|
182
|
+
|
|
183
|
+
// Fallback to ROADMAP.md if no phase directory exists yet
|
|
184
|
+
if (!phaseInfo && roadmapPhase?.found) {
|
|
185
|
+
const phaseName = roadmapPhase.phase_name;
|
|
186
|
+
phaseInfo = {
|
|
187
|
+
found: true,
|
|
188
|
+
directory: null,
|
|
189
|
+
phase_number: roadmapPhase.phase_number,
|
|
190
|
+
phase_name: phaseName,
|
|
191
|
+
phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
|
|
192
|
+
plans: [],
|
|
193
|
+
summaries: [],
|
|
194
|
+
incomplete_plans: [],
|
|
195
|
+
has_research: false,
|
|
196
|
+
has_context: false,
|
|
197
|
+
has_verification: false,
|
|
198
|
+
has_reviews: false,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
92
201
|
const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
|
|
93
202
|
const reqExtracted = reqMatch
|
|
94
203
|
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
|
|
@@ -106,6 +215,7 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
|
|
106
215
|
plan_checker_enabled: config.plan_checker,
|
|
107
216
|
nyquist_validation_enabled: config.nyquist_validation,
|
|
108
217
|
commit_docs: config.commit_docs,
|
|
218
|
+
text_mode: config.text_mode,
|
|
109
219
|
|
|
110
220
|
// Phase info
|
|
111
221
|
phase_found: !!phaseInfo,
|
|
@@ -113,23 +223,24 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
|
|
113
223
|
phase_number: phaseInfo?.phase_number || null,
|
|
114
224
|
phase_name: phaseInfo?.phase_name || null,
|
|
115
225
|
phase_slug: phaseInfo?.phase_slug || null,
|
|
116
|
-
padded_phase: phaseInfo?.phase_number
|
|
226
|
+
padded_phase: phaseInfo?.phase_number ? normalizePhaseName(phaseInfo.phase_number) : null,
|
|
117
227
|
phase_req_ids,
|
|
118
228
|
|
|
119
229
|
// Existing artifacts
|
|
120
230
|
has_research: phaseInfo?.has_research || false,
|
|
121
231
|
has_context: phaseInfo?.has_context || false,
|
|
232
|
+
has_reviews: phaseInfo?.has_reviews || false,
|
|
122
233
|
has_plans: (phaseInfo?.plans?.length || 0) > 0,
|
|
123
234
|
plan_count: phaseInfo?.plans?.length || 0,
|
|
124
235
|
|
|
125
236
|
// Environment
|
|
126
|
-
planning_exists:
|
|
127
|
-
roadmap_exists:
|
|
237
|
+
planning_exists: fs.existsSync(planningDir(cwd)),
|
|
238
|
+
roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
|
|
128
239
|
|
|
129
240
|
// File paths
|
|
130
|
-
state_path: '
|
|
131
|
-
roadmap_path: '
|
|
132
|
-
requirements_path: '
|
|
241
|
+
state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
|
|
242
|
+
roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
|
|
243
|
+
requirements_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'REQUIREMENTS.md'))),
|
|
133
244
|
};
|
|
134
245
|
|
|
135
246
|
if (phaseInfo?.directory) {
|
|
@@ -153,10 +264,33 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
|
|
153
264
|
if (uatFile) {
|
|
154
265
|
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
|
|
155
266
|
}
|
|
156
|
-
|
|
267
|
+
const reviewsFile = files.find(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
|
|
268
|
+
if (reviewsFile) {
|
|
269
|
+
result.reviews_path = toPosixPath(path.join(phaseInfo.directory, reviewsFile));
|
|
270
|
+
}
|
|
271
|
+
} catch { /* intentionally empty */ }
|
|
157
272
|
}
|
|
158
273
|
|
|
159
|
-
|
|
274
|
+
// Optional --validate: run state validation and include warnings (#1627)
|
|
275
|
+
if (options.validate) {
|
|
276
|
+
try {
|
|
277
|
+
const statePath = path.join(planningDir(cwd), 'STATE.md');
|
|
278
|
+
if (fs.existsSync(statePath)) {
|
|
279
|
+
const { stateExtractField } = require('./state.cjs');
|
|
280
|
+
const stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
281
|
+
const warnings = [];
|
|
282
|
+
result.state_validation_ran = true;
|
|
283
|
+
const totalPlansRaw = stateExtractField(stateContent, 'Total Plans in Phase');
|
|
284
|
+
const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
|
|
285
|
+
if (totalPlansInPhase !== null && phaseInfo && totalPlansInPhase !== (phaseInfo.plans?.length || 0)) {
|
|
286
|
+
warnings.push(`Plan count mismatch: STATE.md says ${totalPlansInPhase}, disk has ${phaseInfo.plans?.length || 0}`);
|
|
287
|
+
}
|
|
288
|
+
result.state_warnings = warnings;
|
|
289
|
+
}
|
|
290
|
+
} catch { /* intentionally empty */ }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
output(withProjectRoot(cwd, result), raw);
|
|
160
294
|
}
|
|
161
295
|
|
|
162
296
|
function cmdInitNewProject(cwd, raw) {
|
|
@@ -167,23 +301,67 @@ function cmdInitNewProject(cwd, raw) {
|
|
|
167
301
|
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
|
|
168
302
|
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
|
|
169
303
|
|
|
170
|
-
// Detect
|
|
304
|
+
// Detect Firecrawl API key availability
|
|
305
|
+
const firecrawlKeyFile = path.join(homedir, '.gsd', 'firecrawl_api_key');
|
|
306
|
+
const hasFirecrawl = !!(process.env.FIRECRAWL_API_KEY || fs.existsSync(firecrawlKeyFile));
|
|
307
|
+
|
|
308
|
+
// Detect Exa API key availability
|
|
309
|
+
const exaKeyFile = path.join(homedir, '.gsd', 'exa_api_key');
|
|
310
|
+
const hasExaSearch = !!(process.env.EXA_API_KEY || fs.existsSync(exaKeyFile));
|
|
311
|
+
|
|
312
|
+
// Detect existing code (cross-platform — no Unix `find` dependency)
|
|
171
313
|
let hasCode = false;
|
|
172
314
|
let hasPackageFile = false;
|
|
173
315
|
try {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
316
|
+
const codeExtensions = new Set([
|
|
317
|
+
'.ts', '.js', '.py', '.go', '.rs', '.swift', '.java',
|
|
318
|
+
'.kt', '.kts', // Kotlin (Android, server-side)
|
|
319
|
+
'.c', '.cpp', '.h', // C/C++
|
|
320
|
+
'.cs', // C#
|
|
321
|
+
'.rb', // Ruby
|
|
322
|
+
'.php', // PHP
|
|
323
|
+
'.dart', // Dart (Flutter)
|
|
324
|
+
'.m', '.mm', // Objective-C / Objective-C++
|
|
325
|
+
'.scala', // Scala
|
|
326
|
+
'.groovy', // Groovy (Gradle build scripts)
|
|
327
|
+
'.lua', // Lua
|
|
328
|
+
'.r', '.R', // R
|
|
329
|
+
'.zig', // Zig
|
|
330
|
+
'.ex', '.exs', // Elixir
|
|
331
|
+
'.clj', // Clojure
|
|
332
|
+
]);
|
|
333
|
+
const skipDirs = new Set(['node_modules', '.git', '.planning', '.OpenCode', '.codex', '__pycache__', 'target', 'dist', 'build']);
|
|
334
|
+
function findCodeFiles(dir, depth) {
|
|
335
|
+
if (depth > 3) return false;
|
|
336
|
+
let entries;
|
|
337
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return false; }
|
|
338
|
+
for (const entry of entries) {
|
|
339
|
+
if (entry.isFile() && codeExtensions.has(path.extname(entry.name))) return true;
|
|
340
|
+
if (entry.isDirectory() && !skipDirs.has(entry.name)) {
|
|
341
|
+
if (findCodeFiles(path.join(dir, entry.name), depth + 1)) return true;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
hasCode = findCodeFiles(cwd, 0);
|
|
347
|
+
} catch { /* intentionally empty — best-effort detection */ }
|
|
181
348
|
|
|
182
349
|
hasPackageFile = pathExistsInternal(cwd, 'package.json') ||
|
|
183
350
|
pathExistsInternal(cwd, 'requirements.txt') ||
|
|
184
351
|
pathExistsInternal(cwd, 'Cargo.toml') ||
|
|
185
352
|
pathExistsInternal(cwd, 'go.mod') ||
|
|
186
|
-
pathExistsInternal(cwd, 'Package.swift')
|
|
353
|
+
pathExistsInternal(cwd, 'Package.swift') ||
|
|
354
|
+
pathExistsInternal(cwd, 'build.gradle') ||
|
|
355
|
+
pathExistsInternal(cwd, 'build.gradle.kts') ||
|
|
356
|
+
pathExistsInternal(cwd, 'pom.xml') ||
|
|
357
|
+
pathExistsInternal(cwd, 'Gemfile') ||
|
|
358
|
+
pathExistsInternal(cwd, 'composer.json') ||
|
|
359
|
+
pathExistsInternal(cwd, 'pubspec.yaml') ||
|
|
360
|
+
pathExistsInternal(cwd, 'CMakeLists.txt') ||
|
|
361
|
+
pathExistsInternal(cwd, 'Makefile') ||
|
|
362
|
+
pathExistsInternal(cwd, 'build.zig') ||
|
|
363
|
+
pathExistsInternal(cwd, 'mix.exs') ||
|
|
364
|
+
pathExistsInternal(cwd, 'project.clj');
|
|
187
365
|
|
|
188
366
|
const result = {
|
|
189
367
|
// Models
|
|
@@ -210,17 +388,30 @@ function cmdInitNewProject(cwd, raw) {
|
|
|
210
388
|
|
|
211
389
|
// Enhanced search
|
|
212
390
|
brave_search_available: hasBraveSearch,
|
|
391
|
+
firecrawl_available: hasFirecrawl,
|
|
392
|
+
exa_search_available: hasExaSearch,
|
|
213
393
|
|
|
214
394
|
// File paths
|
|
215
395
|
project_path: '.planning/PROJECT.md',
|
|
216
396
|
};
|
|
217
397
|
|
|
218
|
-
output(result, raw);
|
|
398
|
+
output(withProjectRoot(cwd, result), raw);
|
|
219
399
|
}
|
|
220
400
|
|
|
221
401
|
function cmdInitNewMilestone(cwd, raw) {
|
|
222
402
|
const config = loadConfig(cwd);
|
|
223
403
|
const milestone = getMilestoneInfo(cwd);
|
|
404
|
+
const latestCompleted = getLatestCompletedMilestone(cwd);
|
|
405
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
406
|
+
let phaseDirCount = 0;
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
if (fs.existsSync(phasesDir)) {
|
|
410
|
+
phaseDirCount = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
411
|
+
.filter(entry => entry.isDirectory())
|
|
412
|
+
.length;
|
|
413
|
+
}
|
|
414
|
+
} catch {}
|
|
224
415
|
|
|
225
416
|
const result = {
|
|
226
417
|
// Models
|
|
@@ -235,19 +426,23 @@ function cmdInitNewMilestone(cwd, raw) {
|
|
|
235
426
|
// Current milestone
|
|
236
427
|
current_milestone: milestone.version,
|
|
237
428
|
current_milestone_name: milestone.name,
|
|
429
|
+
latest_completed_milestone: latestCompleted?.version || null,
|
|
430
|
+
latest_completed_milestone_name: latestCompleted?.name || null,
|
|
431
|
+
phase_dir_count: phaseDirCount,
|
|
432
|
+
phase_archive_path: latestCompleted ? toPosixPath(path.relative(cwd, path.join(planningRoot(cwd), 'milestones', `${latestCompleted.version}-phases`))) : null,
|
|
238
433
|
|
|
239
434
|
// File existence
|
|
240
435
|
project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
|
|
241
|
-
roadmap_exists:
|
|
242
|
-
state_exists:
|
|
436
|
+
roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
|
|
437
|
+
state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
|
|
243
438
|
|
|
244
439
|
// File paths
|
|
245
440
|
project_path: '.planning/PROJECT.md',
|
|
246
|
-
roadmap_path: '
|
|
247
|
-
state_path: '
|
|
441
|
+
roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
|
|
442
|
+
state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
|
|
248
443
|
};
|
|
249
444
|
|
|
250
|
-
output(result, raw);
|
|
445
|
+
output(withProjectRoot(cwd, result), raw);
|
|
251
446
|
}
|
|
252
447
|
|
|
253
448
|
function cmdInitQuick(cwd, description, raw) {
|
|
@@ -255,18 +450,25 @@ function cmdInitQuick(cwd, description, raw) {
|
|
|
255
450
|
const now = new Date();
|
|
256
451
|
const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
|
|
257
452
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
453
|
+
// Generate collision-resistant quick task ID: YYMMDD-xxx
|
|
454
|
+
// xxx = 2-second precision blocks since midnight, encoded as 3-char Base36 (lowercase)
|
|
455
|
+
// Range: 000 (00:00:00) to xbz (23:59:58), guaranteed 3 chars for any time of day.
|
|
456
|
+
// Provides ~2s uniqueness window per user — practically collision-free across a team.
|
|
457
|
+
const yy = String(now.getFullYear()).slice(-2);
|
|
458
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
459
|
+
const dd = String(now.getDate()).padStart(2, '0');
|
|
460
|
+
const dateStr = yy + mm + dd;
|
|
461
|
+
const secondsSinceMidnight = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
|
|
462
|
+
const timeBlocks = Math.floor(secondsSinceMidnight / 2);
|
|
463
|
+
const timeEncoded = timeBlocks.toString(36).padStart(3, '0');
|
|
464
|
+
const quickId = dateStr + '-' + timeEncoded;
|
|
465
|
+
const branchSlug = slug || 'quick';
|
|
466
|
+
const quickBranchName = config.quick_branch_template
|
|
467
|
+
? config.quick_branch_template
|
|
468
|
+
.replace('{num}', quickId)
|
|
469
|
+
.replace('{quick}', quickId)
|
|
470
|
+
.replace('{slug}', branchSlug)
|
|
471
|
+
: null;
|
|
270
472
|
|
|
271
473
|
const result = {
|
|
272
474
|
// Models
|
|
@@ -277,9 +479,10 @@ function cmdInitQuick(cwd, description, raw) {
|
|
|
277
479
|
|
|
278
480
|
// Config
|
|
279
481
|
commit_docs: config.commit_docs,
|
|
482
|
+
branch_name: quickBranchName,
|
|
280
483
|
|
|
281
484
|
// Quick task info
|
|
282
|
-
|
|
485
|
+
quick_id: quickId,
|
|
283
486
|
slug: slug,
|
|
284
487
|
description: description || null,
|
|
285
488
|
|
|
@@ -289,15 +492,15 @@ function cmdInitQuick(cwd, description, raw) {
|
|
|
289
492
|
|
|
290
493
|
// Paths
|
|
291
494
|
quick_dir: '.planning/quick',
|
|
292
|
-
task_dir: slug ? `.planning/quick/${
|
|
495
|
+
task_dir: slug ? `.planning/quick/${quickId}-${slug}` : null,
|
|
293
496
|
|
|
294
497
|
// File existence
|
|
295
|
-
roadmap_exists:
|
|
296
|
-
planning_exists:
|
|
498
|
+
roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
|
|
499
|
+
planning_exists: fs.existsSync(planningRoot(cwd)),
|
|
297
500
|
|
|
298
501
|
};
|
|
299
502
|
|
|
300
|
-
output(result, raw);
|
|
503
|
+
output(withProjectRoot(cwd, result), raw);
|
|
301
504
|
}
|
|
302
505
|
|
|
303
506
|
function cmdInitResume(cwd, raw) {
|
|
@@ -306,19 +509,19 @@ function cmdInitResume(cwd, raw) {
|
|
|
306
509
|
// Check for interrupted agent
|
|
307
510
|
let interruptedAgentId = null;
|
|
308
511
|
try {
|
|
309
|
-
interruptedAgentId = fs.readFileSync(path.join(cwd, '
|
|
310
|
-
} catch {}
|
|
512
|
+
interruptedAgentId = fs.readFileSync(path.join(planningRoot(cwd), 'current-agent-id.txt'), 'utf-8').trim();
|
|
513
|
+
} catch { /* intentionally empty */ }
|
|
311
514
|
|
|
312
515
|
const result = {
|
|
313
516
|
// File existence
|
|
314
|
-
state_exists:
|
|
315
|
-
roadmap_exists:
|
|
517
|
+
state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
|
|
518
|
+
roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
|
|
316
519
|
project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
|
|
317
|
-
planning_exists:
|
|
520
|
+
planning_exists: fs.existsSync(planningRoot(cwd)),
|
|
318
521
|
|
|
319
522
|
// File paths
|
|
320
|
-
state_path: '
|
|
321
|
-
roadmap_path: '
|
|
523
|
+
state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
|
|
524
|
+
roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
|
|
322
525
|
project_path: '.planning/PROJECT.md',
|
|
323
526
|
|
|
324
527
|
// Agent state
|
|
@@ -329,7 +532,7 @@ function cmdInitResume(cwd, raw) {
|
|
|
329
532
|
commit_docs: config.commit_docs,
|
|
330
533
|
};
|
|
331
534
|
|
|
332
|
-
output(result, raw);
|
|
535
|
+
output(withProjectRoot(cwd, result), raw);
|
|
333
536
|
}
|
|
334
537
|
|
|
335
538
|
function cmdInitVerifyWork(cwd, phase, raw) {
|
|
@@ -338,7 +541,28 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
338
541
|
}
|
|
339
542
|
|
|
340
543
|
const config = loadConfig(cwd);
|
|
341
|
-
|
|
544
|
+
let phaseInfo = findPhaseInternal(cwd, phase);
|
|
545
|
+
|
|
546
|
+
// Fallback to ROADMAP.md if no phase directory exists yet
|
|
547
|
+
if (!phaseInfo) {
|
|
548
|
+
const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
|
|
549
|
+
if (roadmapPhase?.found) {
|
|
550
|
+
const phaseName = roadmapPhase.phase_name;
|
|
551
|
+
phaseInfo = {
|
|
552
|
+
found: true,
|
|
553
|
+
directory: null,
|
|
554
|
+
phase_number: roadmapPhase.phase_number,
|
|
555
|
+
phase_name: phaseName,
|
|
556
|
+
phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
|
|
557
|
+
plans: [],
|
|
558
|
+
summaries: [],
|
|
559
|
+
incomplete_plans: [],
|
|
560
|
+
has_research: false,
|
|
561
|
+
has_context: false,
|
|
562
|
+
has_verification: false,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
342
566
|
|
|
343
567
|
const result = {
|
|
344
568
|
// Models
|
|
@@ -358,13 +582,36 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
358
582
|
has_verification: phaseInfo?.has_verification || false,
|
|
359
583
|
};
|
|
360
584
|
|
|
361
|
-
output(result, raw);
|
|
585
|
+
output(withProjectRoot(cwd, result), raw);
|
|
362
586
|
}
|
|
363
587
|
|
|
364
588
|
function cmdInitPhaseOp(cwd, phase, raw) {
|
|
365
589
|
const config = loadConfig(cwd);
|
|
366
590
|
let phaseInfo = findPhaseInternal(cwd, phase);
|
|
367
591
|
|
|
592
|
+
// If the only disk match comes from an archived milestone, prefer the
|
|
593
|
+
// current milestone's ROADMAP entry so discuss-phase and similar flows
|
|
594
|
+
// don't attach to shipped work that reused the same phase number.
|
|
595
|
+
if (phaseInfo?.archived) {
|
|
596
|
+
const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
|
|
597
|
+
if (roadmapPhase?.found) {
|
|
598
|
+
const phaseName = roadmapPhase.phase_name;
|
|
599
|
+
phaseInfo = {
|
|
600
|
+
found: true,
|
|
601
|
+
directory: null,
|
|
602
|
+
phase_number: roadmapPhase.phase_number,
|
|
603
|
+
phase_name: phaseName,
|
|
604
|
+
phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
|
|
605
|
+
plans: [],
|
|
606
|
+
summaries: [],
|
|
607
|
+
incomplete_plans: [],
|
|
608
|
+
has_research: false,
|
|
609
|
+
has_context: false,
|
|
610
|
+
has_verification: false,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
368
615
|
// Fallback to ROADMAP.md if no directory exists (e.g., Plans: TBD)
|
|
369
616
|
if (!phaseInfo) {
|
|
370
617
|
const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
|
|
@@ -390,6 +637,8 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|
|
390
637
|
// Config
|
|
391
638
|
commit_docs: config.commit_docs,
|
|
392
639
|
brave_search: config.brave_search,
|
|
640
|
+
firecrawl: config.firecrawl,
|
|
641
|
+
exa_search: config.exa_search,
|
|
393
642
|
|
|
394
643
|
// Phase info
|
|
395
644
|
phase_found: !!phaseInfo,
|
|
@@ -397,23 +646,24 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|
|
397
646
|
phase_number: phaseInfo?.phase_number || null,
|
|
398
647
|
phase_name: phaseInfo?.phase_name || null,
|
|
399
648
|
phase_slug: phaseInfo?.phase_slug || null,
|
|
400
|
-
padded_phase: phaseInfo?.phase_number
|
|
649
|
+
padded_phase: phaseInfo?.phase_number ? normalizePhaseName(phaseInfo.phase_number) : null,
|
|
401
650
|
|
|
402
651
|
// Existing artifacts
|
|
403
652
|
has_research: phaseInfo?.has_research || false,
|
|
404
653
|
has_context: phaseInfo?.has_context || false,
|
|
405
654
|
has_plans: (phaseInfo?.plans?.length || 0) > 0,
|
|
406
655
|
has_verification: phaseInfo?.has_verification || false,
|
|
656
|
+
has_reviews: phaseInfo?.has_reviews || false,
|
|
407
657
|
plan_count: phaseInfo?.plans?.length || 0,
|
|
408
658
|
|
|
409
659
|
// File existence
|
|
410
|
-
roadmap_exists:
|
|
411
|
-
planning_exists:
|
|
660
|
+
roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
|
|
661
|
+
planning_exists: fs.existsSync(planningDir(cwd)),
|
|
412
662
|
|
|
413
663
|
// File paths
|
|
414
|
-
state_path: '
|
|
415
|
-
roadmap_path: '
|
|
416
|
-
requirements_path: '
|
|
664
|
+
state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
|
|
665
|
+
roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
|
|
666
|
+
requirements_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'REQUIREMENTS.md'))),
|
|
417
667
|
};
|
|
418
668
|
|
|
419
669
|
if (phaseInfo?.directory) {
|
|
@@ -436,10 +686,14 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|
|
436
686
|
if (uatFile) {
|
|
437
687
|
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
|
|
438
688
|
}
|
|
439
|
-
|
|
689
|
+
const reviewsFile = files.find(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
|
|
690
|
+
if (reviewsFile) {
|
|
691
|
+
result.reviews_path = toPosixPath(path.join(phaseInfo.directory, reviewsFile));
|
|
692
|
+
}
|
|
693
|
+
} catch { /* intentionally empty */ }
|
|
440
694
|
}
|
|
441
695
|
|
|
442
|
-
output(result, raw);
|
|
696
|
+
output(withProjectRoot(cwd, result), raw);
|
|
443
697
|
}
|
|
444
698
|
|
|
445
699
|
function cmdInitTodos(cwd, area, raw) {
|
|
@@ -447,7 +701,7 @@ function cmdInitTodos(cwd, area, raw) {
|
|
|
447
701
|
const now = new Date();
|
|
448
702
|
|
|
449
703
|
// List todos (reuse existing logic)
|
|
450
|
-
const pendingDir = path.join(cwd, '
|
|
704
|
+
const pendingDir = path.join(planningDir(cwd), 'todos', 'pending');
|
|
451
705
|
let count = 0;
|
|
452
706
|
const todos = [];
|
|
453
707
|
|
|
@@ -469,11 +723,11 @@ function cmdInitTodos(cwd, area, raw) {
|
|
|
469
723
|
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
|
470
724
|
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
471
725
|
area: todoArea,
|
|
472
|
-
path: '
|
|
726
|
+
path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'todos', 'pending', file))),
|
|
473
727
|
});
|
|
474
|
-
} catch {}
|
|
728
|
+
} catch { /* intentionally empty */ }
|
|
475
729
|
}
|
|
476
|
-
} catch {}
|
|
730
|
+
} catch { /* intentionally empty */ }
|
|
477
731
|
|
|
478
732
|
const result = {
|
|
479
733
|
// Config
|
|
@@ -489,16 +743,16 @@ function cmdInitTodos(cwd, area, raw) {
|
|
|
489
743
|
area_filter: area || null,
|
|
490
744
|
|
|
491
745
|
// Paths
|
|
492
|
-
pending_dir: '
|
|
493
|
-
completed_dir: '
|
|
746
|
+
pending_dir: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'todos', 'pending'))),
|
|
747
|
+
completed_dir: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'todos', 'completed'))),
|
|
494
748
|
|
|
495
749
|
// File existence
|
|
496
|
-
planning_exists:
|
|
497
|
-
todos_dir_exists:
|
|
498
|
-
pending_dir_exists:
|
|
750
|
+
planning_exists: fs.existsSync(planningDir(cwd)),
|
|
751
|
+
todos_dir_exists: fs.existsSync(path.join(planningDir(cwd), 'todos')),
|
|
752
|
+
pending_dir_exists: fs.existsSync(path.join(planningDir(cwd), 'todos', 'pending')),
|
|
499
753
|
};
|
|
500
754
|
|
|
501
|
-
output(result, raw);
|
|
755
|
+
output(withProjectRoot(cwd, result), raw);
|
|
502
756
|
}
|
|
503
757
|
|
|
504
758
|
function cmdInitMilestoneOp(cwd, raw) {
|
|
@@ -508,7 +762,7 @@ function cmdInitMilestoneOp(cwd, raw) {
|
|
|
508
762
|
// Count phases
|
|
509
763
|
let phaseCount = 0;
|
|
510
764
|
let completedPhases = 0;
|
|
511
|
-
const phasesDir = path.join(cwd, '
|
|
765
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
512
766
|
try {
|
|
513
767
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
514
768
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
@@ -520,18 +774,18 @@ function cmdInitMilestoneOp(cwd, raw) {
|
|
|
520
774
|
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
521
775
|
const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
522
776
|
if (hasSummary) completedPhases++;
|
|
523
|
-
} catch {}
|
|
777
|
+
} catch { /* intentionally empty */ }
|
|
524
778
|
}
|
|
525
|
-
} catch {}
|
|
779
|
+
} catch { /* intentionally empty */ }
|
|
526
780
|
|
|
527
781
|
// Check archive
|
|
528
|
-
const archiveDir = path.join(cwd, '
|
|
782
|
+
const archiveDir = path.join(planningRoot(cwd), 'archive');
|
|
529
783
|
let archivedMilestones = [];
|
|
530
784
|
try {
|
|
531
785
|
archivedMilestones = fs.readdirSync(archiveDir, { withFileTypes: true })
|
|
532
786
|
.filter(e => e.isDirectory())
|
|
533
787
|
.map(e => e.name);
|
|
534
|
-
} catch {}
|
|
788
|
+
} catch { /* intentionally empty */ }
|
|
535
789
|
|
|
536
790
|
const result = {
|
|
537
791
|
// Config
|
|
@@ -553,24 +807,24 @@ function cmdInitMilestoneOp(cwd, raw) {
|
|
|
553
807
|
|
|
554
808
|
// File existence
|
|
555
809
|
project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
|
|
556
|
-
roadmap_exists:
|
|
557
|
-
state_exists:
|
|
558
|
-
archive_exists:
|
|
559
|
-
phases_dir_exists:
|
|
810
|
+
roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
|
|
811
|
+
state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
|
|
812
|
+
archive_exists: fs.existsSync(path.join(planningRoot(cwd), 'archive')),
|
|
813
|
+
phases_dir_exists: fs.existsSync(path.join(planningDir(cwd), 'phases')),
|
|
560
814
|
};
|
|
561
815
|
|
|
562
|
-
output(result, raw);
|
|
816
|
+
output(withProjectRoot(cwd, result), raw);
|
|
563
817
|
}
|
|
564
818
|
|
|
565
819
|
function cmdInitMapCodebase(cwd, raw) {
|
|
566
820
|
const config = loadConfig(cwd);
|
|
567
821
|
|
|
568
822
|
// Check for existing codebase maps
|
|
569
|
-
const codebaseDir = path.join(cwd, '
|
|
823
|
+
const codebaseDir = path.join(planningRoot(cwd), 'codebase');
|
|
570
824
|
let existingMaps = [];
|
|
571
825
|
try {
|
|
572
826
|
existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
|
|
573
|
-
} catch {}
|
|
827
|
+
} catch { /* intentionally empty */ }
|
|
574
828
|
|
|
575
829
|
const result = {
|
|
576
830
|
// Models
|
|
@@ -580,6 +834,7 @@ function cmdInitMapCodebase(cwd, raw) {
|
|
|
580
834
|
commit_docs: config.commit_docs,
|
|
581
835
|
search_gitignored: config.search_gitignored,
|
|
582
836
|
parallelization: config.parallelization,
|
|
837
|
+
subagent_timeout: config.subagent_timeout,
|
|
583
838
|
|
|
584
839
|
// Paths
|
|
585
840
|
codebase_dir: '.planning/codebase',
|
|
@@ -593,27 +848,324 @@ function cmdInitMapCodebase(cwd, raw) {
|
|
|
593
848
|
codebase_dir_exists: pathExistsInternal(cwd, '.planning/codebase'),
|
|
594
849
|
};
|
|
595
850
|
|
|
596
|
-
output(result, raw);
|
|
851
|
+
output(withProjectRoot(cwd, result), raw);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function cmdInitManager(cwd, raw) {
|
|
855
|
+
const config = loadConfig(cwd);
|
|
856
|
+
const milestone = getMilestoneInfo(cwd);
|
|
857
|
+
|
|
858
|
+
// Use planningPaths for forward-compatibility with workstream scoping (#1268)
|
|
859
|
+
const paths = planningPaths(cwd);
|
|
860
|
+
|
|
861
|
+
// Validate prerequisites
|
|
862
|
+
if (!fs.existsSync(paths.roadmap)) {
|
|
863
|
+
error('No ROADMAP.md found. Run /gsd-new-milestone first.');
|
|
864
|
+
}
|
|
865
|
+
if (!fs.existsSync(paths.state)) {
|
|
866
|
+
error('No STATE.md found. Run /gsd-new-milestone first.');
|
|
867
|
+
}
|
|
868
|
+
const rawContent = fs.readFileSync(paths.roadmap, 'utf-8');
|
|
869
|
+
const content = extractCurrentMilestone(rawContent, cwd);
|
|
870
|
+
const phasesDir = paths.phases;
|
|
871
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
872
|
+
|
|
873
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
874
|
+
const phases = [];
|
|
875
|
+
let match;
|
|
876
|
+
|
|
877
|
+
while ((match = phasePattern.exec(content)) !== null) {
|
|
878
|
+
const phaseNum = match[1];
|
|
879
|
+
const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
|
|
880
|
+
|
|
881
|
+
const sectionStart = match.index;
|
|
882
|
+
const restOfContent = content.slice(sectionStart);
|
|
883
|
+
const nextHeader = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
|
|
884
|
+
const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
|
|
885
|
+
const section = content.slice(sectionStart, sectionEnd);
|
|
886
|
+
|
|
887
|
+
const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
888
|
+
const goal = goalMatch ? goalMatch[1].trim() : null;
|
|
889
|
+
|
|
890
|
+
const dependsMatch = section.match(/\*\*Depends on(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
891
|
+
const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
|
|
892
|
+
|
|
893
|
+
const normalized = normalizePhaseName(phaseNum);
|
|
894
|
+
let diskStatus = 'no_directory';
|
|
895
|
+
let planCount = 0;
|
|
896
|
+
let summaryCount = 0;
|
|
897
|
+
let hasContext = false;
|
|
898
|
+
let hasResearch = false;
|
|
899
|
+
let lastActivity = null;
|
|
900
|
+
let isActive = false;
|
|
901
|
+
|
|
902
|
+
try {
|
|
903
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
904
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).filter(isDirInMilestone);
|
|
905
|
+
const dirMatch = dirs.find(d => phaseTokenMatches(d, normalized));
|
|
906
|
+
|
|
907
|
+
if (dirMatch) {
|
|
908
|
+
const fullDir = path.join(phasesDir, dirMatch);
|
|
909
|
+
const phaseFiles = fs.readdirSync(fullDir);
|
|
910
|
+
planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
|
911
|
+
summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
|
912
|
+
hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
|
913
|
+
hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
914
|
+
|
|
915
|
+
if (summaryCount >= planCount && planCount > 0) diskStatus = 'complete';
|
|
916
|
+
else if (summaryCount > 0) diskStatus = 'partial';
|
|
917
|
+
else if (planCount > 0) diskStatus = 'planned';
|
|
918
|
+
else if (hasResearch) diskStatus = 'researched';
|
|
919
|
+
else if (hasContext) diskStatus = 'discussed';
|
|
920
|
+
else diskStatus = 'empty';
|
|
921
|
+
|
|
922
|
+
// Activity detection: check most recent file mtime
|
|
923
|
+
const now = Date.now();
|
|
924
|
+
let newestMtime = 0;
|
|
925
|
+
for (const f of phaseFiles) {
|
|
926
|
+
try {
|
|
927
|
+
const stat = fs.statSync(path.join(fullDir, f));
|
|
928
|
+
if (stat.mtimeMs > newestMtime) newestMtime = stat.mtimeMs;
|
|
929
|
+
} catch { /* intentionally empty */ }
|
|
930
|
+
}
|
|
931
|
+
if (newestMtime > 0) {
|
|
932
|
+
lastActivity = new Date(newestMtime).toISOString();
|
|
933
|
+
isActive = (now - newestMtime) < 300000; // 5 minutes
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
} catch { /* intentionally empty */ }
|
|
937
|
+
|
|
938
|
+
// Check ROADMAP checkbox status
|
|
939
|
+
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[:\\s]`, 'i');
|
|
940
|
+
const checkboxMatch = content.match(checkboxPattern);
|
|
941
|
+
const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
|
|
942
|
+
if (roadmapComplete && diskStatus !== 'complete') {
|
|
943
|
+
diskStatus = 'complete';
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
phases.push({
|
|
947
|
+
number: phaseNum,
|
|
948
|
+
name: phaseName,
|
|
949
|
+
goal,
|
|
950
|
+
depends_on,
|
|
951
|
+
disk_status: diskStatus,
|
|
952
|
+
has_context: hasContext,
|
|
953
|
+
has_research: hasResearch,
|
|
954
|
+
plan_count: planCount,
|
|
955
|
+
summary_count: summaryCount,
|
|
956
|
+
roadmap_complete: roadmapComplete,
|
|
957
|
+
last_activity: lastActivity,
|
|
958
|
+
is_active: isActive,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Compute display names: truncate to keep table aligned
|
|
963
|
+
const MAX_NAME_WIDTH = 20;
|
|
964
|
+
for (const phase of phases) {
|
|
965
|
+
if (phase.name.length > MAX_NAME_WIDTH) {
|
|
966
|
+
phase.display_name = phase.name.slice(0, MAX_NAME_WIDTH - 1) + '…';
|
|
967
|
+
} else {
|
|
968
|
+
phase.display_name = phase.name;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Dependency satisfaction: check if all depends_on phases are complete
|
|
973
|
+
const completedNums = new Set(phases.filter(p => p.disk_status === 'complete').map(p => p.number));
|
|
974
|
+
for (const phase of phases) {
|
|
975
|
+
if (!phase.depends_on || /^none$/i.test(phase.depends_on.trim())) {
|
|
976
|
+
phase.deps_satisfied = true;
|
|
977
|
+
} else {
|
|
978
|
+
// Parse "Phase 1, Phase 3" or "1, 3" formats
|
|
979
|
+
const depNums = phase.depends_on.match(/\d+(?:\.\d+)*/g) || [];
|
|
980
|
+
phase.deps_satisfied = depNums.every(n => completedNums.has(n));
|
|
981
|
+
phase.dep_phases = depNums;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Compact dependency display for dashboard
|
|
986
|
+
for (const phase of phases) {
|
|
987
|
+
phase.deps_display = (phase.dep_phases && phase.dep_phases.length > 0)
|
|
988
|
+
? phase.dep_phases.join(',')
|
|
989
|
+
: '—';
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Sliding window: discuss is sequential — only the first undiscussed phase is available
|
|
993
|
+
let foundNextToDiscuss = false;
|
|
994
|
+
for (const phase of phases) {
|
|
995
|
+
if (!foundNextToDiscuss && (phase.disk_status === 'empty' || phase.disk_status === 'no_directory')) {
|
|
996
|
+
phase.is_next_to_discuss = true;
|
|
997
|
+
foundNextToDiscuss = true;
|
|
998
|
+
} else {
|
|
999
|
+
phase.is_next_to_discuss = false;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Check for WAITING.json signal
|
|
1004
|
+
let waitingSignal = null;
|
|
1005
|
+
try {
|
|
1006
|
+
const waitingPath = path.join(cwd, '.planning', 'WAITING.json');
|
|
1007
|
+
if (fs.existsSync(waitingPath)) {
|
|
1008
|
+
waitingSignal = JSON.parse(fs.readFileSync(waitingPath, 'utf-8'));
|
|
1009
|
+
}
|
|
1010
|
+
} catch { /* intentionally empty */ }
|
|
1011
|
+
|
|
1012
|
+
// Compute recommended actions (execute > plan > discuss)
|
|
1013
|
+
// Skip BACKLOG phases (999.x numbering) — they are parked ideas, not active work
|
|
1014
|
+
const recommendedActions = [];
|
|
1015
|
+
for (const phase of phases) {
|
|
1016
|
+
if (phase.disk_status === 'complete') continue;
|
|
1017
|
+
if (/^999(?:\.|$)/.test(phase.number)) continue;
|
|
1018
|
+
|
|
1019
|
+
if (phase.disk_status === 'planned' && phase.deps_satisfied) {
|
|
1020
|
+
recommendedActions.push({
|
|
1021
|
+
phase: phase.number,
|
|
1022
|
+
phase_name: phase.name,
|
|
1023
|
+
action: 'execute',
|
|
1024
|
+
reason: `${phase.plan_count} plans ready, dependencies met`,
|
|
1025
|
+
command: `/gsd-execute-phase ${phase.number}`,
|
|
1026
|
+
});
|
|
1027
|
+
} else if (phase.disk_status === 'discussed' || phase.disk_status === 'researched') {
|
|
1028
|
+
recommendedActions.push({
|
|
1029
|
+
phase: phase.number,
|
|
1030
|
+
phase_name: phase.name,
|
|
1031
|
+
action: 'plan',
|
|
1032
|
+
reason: 'Context gathered, ready for planning',
|
|
1033
|
+
command: `/gsd-plan-phase ${phase.number}`,
|
|
1034
|
+
});
|
|
1035
|
+
} else if ((phase.disk_status === 'empty' || phase.disk_status === 'no_directory') && phase.is_next_to_discuss) {
|
|
1036
|
+
recommendedActions.push({
|
|
1037
|
+
phase: phase.number,
|
|
1038
|
+
phase_name: phase.name,
|
|
1039
|
+
action: 'discuss',
|
|
1040
|
+
reason: 'Unblocked, ready to gather context',
|
|
1041
|
+
command: `/gsd-discuss-phase ${phase.number}`,
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Filter recommendations: no parallel execute/plan unless phases are independent
|
|
1047
|
+
// Two phases are "independent" if neither depends on the other (directly or transitively)
|
|
1048
|
+
const phaseMap = new Map(phases.map(p => [p.number, p]));
|
|
1049
|
+
|
|
1050
|
+
function reaches(from, to, visited = new Set()) {
|
|
1051
|
+
if (visited.has(from)) return false;
|
|
1052
|
+
visited.add(from);
|
|
1053
|
+
const p = phaseMap.get(from);
|
|
1054
|
+
if (!p || !p.dep_phases || p.dep_phases.length === 0) return false;
|
|
1055
|
+
if (p.dep_phases.includes(to)) return true;
|
|
1056
|
+
return p.dep_phases.some(dep => reaches(dep, to, visited));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function hasDepRelationship(numA, numB) {
|
|
1060
|
+
return reaches(numA, numB) || reaches(numB, numA);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Detect phases with active work (file modified in last 5 min)
|
|
1064
|
+
const activeExecuting = phases.filter(p =>
|
|
1065
|
+
p.disk_status === 'partial' ||
|
|
1066
|
+
(p.disk_status === 'planned' && p.is_active)
|
|
1067
|
+
);
|
|
1068
|
+
const activePlanning = phases.filter(p =>
|
|
1069
|
+
p.is_active && (p.disk_status === 'discussed' || p.disk_status === 'researched')
|
|
1070
|
+
);
|
|
1071
|
+
|
|
1072
|
+
const filteredActions = recommendedActions.filter(action => {
|
|
1073
|
+
if (action.action === 'execute' && activeExecuting.length > 0) {
|
|
1074
|
+
// Only allow if independent of ALL actively-executing phases
|
|
1075
|
+
return activeExecuting.every(active => !hasDepRelationship(action.phase, active.number));
|
|
1076
|
+
}
|
|
1077
|
+
if (action.action === 'plan' && activePlanning.length > 0) {
|
|
1078
|
+
// Only allow if independent of ALL actively-planning phases
|
|
1079
|
+
return activePlanning.every(active => !hasDepRelationship(action.phase, active.number));
|
|
1080
|
+
}
|
|
1081
|
+
return true;
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
const completedCount = phases.filter(p => p.disk_status === 'complete').length;
|
|
1085
|
+
|
|
1086
|
+
// read manager flags from config (passthrough flags for each step)
|
|
1087
|
+
// Validate: flags must be CLI-safe (only --flags, alphanumeric, hyphens, spaces)
|
|
1088
|
+
const sanitizeFlags = (raw) => {
|
|
1089
|
+
const val = typeof raw === 'string' ? raw : '';
|
|
1090
|
+
if (!val) return '';
|
|
1091
|
+
// Allow only --flag patterns with alphanumeric/hyphen values separated by spaces
|
|
1092
|
+
const tokens = val.split(/\s+/).filter(Boolean);
|
|
1093
|
+
const safe = tokens.every(t => /^--[a-zA-Z0-9][-a-zA-Z0-9]*$/.test(t) || /^[a-zA-Z0-9][-a-zA-Z0-9_.]*$/.test(t));
|
|
1094
|
+
if (!safe) {
|
|
1095
|
+
process.stderr.write(`gsd-tools: warning: manager.flags contains invalid tokens, ignoring: ${val}\n`);
|
|
1096
|
+
return '';
|
|
1097
|
+
}
|
|
1098
|
+
return val;
|
|
1099
|
+
};
|
|
1100
|
+
const managerFlags = {
|
|
1101
|
+
discuss: sanitizeFlags(config.manager && config.manager.flags && config.manager.flags.discuss),
|
|
1102
|
+
plan: sanitizeFlags(config.manager && config.manager.flags && config.manager.flags.plan),
|
|
1103
|
+
execute: sanitizeFlags(config.manager && config.manager.flags && config.manager.flags.execute),
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
const result = {
|
|
1107
|
+
milestone_version: milestone.version,
|
|
1108
|
+
milestone_name: milestone.name,
|
|
1109
|
+
phases,
|
|
1110
|
+
phase_count: phases.length,
|
|
1111
|
+
completed_count: completedCount,
|
|
1112
|
+
in_progress_count: phases.filter(p => ['partial', 'planned', 'discussed', 'researched'].includes(p.disk_status)).length,
|
|
1113
|
+
recommended_actions: filteredActions,
|
|
1114
|
+
waiting_signal: waitingSignal,
|
|
1115
|
+
all_complete: completedCount === phases.length && phases.length > 0,
|
|
1116
|
+
project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
|
|
1117
|
+
roadmap_exists: true,
|
|
1118
|
+
state_exists: true,
|
|
1119
|
+
manager_flags: managerFlags,
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
output(withProjectRoot(cwd, result), raw);
|
|
597
1123
|
}
|
|
598
1124
|
|
|
599
1125
|
function cmdInitProgress(cwd, raw) {
|
|
600
1126
|
const config = loadConfig(cwd);
|
|
601
1127
|
const milestone = getMilestoneInfo(cwd);
|
|
602
1128
|
|
|
603
|
-
// Analyze phases
|
|
604
|
-
const phasesDir = path.join(cwd, '
|
|
1129
|
+
// Analyze phases — filter to current milestone and include ROADMAP-only phases
|
|
1130
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
605
1131
|
const phases = [];
|
|
606
1132
|
let currentPhase = null;
|
|
607
1133
|
let nextPhase = null;
|
|
608
1134
|
|
|
1135
|
+
// Build set of phases defined in ROADMAP for the current milestone
|
|
1136
|
+
const roadmapPhaseNums = new Set();
|
|
1137
|
+
const roadmapPhaseNames = new Map();
|
|
1138
|
+
try {
|
|
1139
|
+
const roadmapContent = extractCurrentMilestone(
|
|
1140
|
+
fs.readFileSync(path.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd
|
|
1141
|
+
);
|
|
1142
|
+
const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
1143
|
+
let hm;
|
|
1144
|
+
while ((hm = headingPattern.exec(roadmapContent)) !== null) {
|
|
1145
|
+
roadmapPhaseNums.add(hm[1]);
|
|
1146
|
+
roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim());
|
|
1147
|
+
}
|
|
1148
|
+
} catch { /* intentionally empty */ }
|
|
1149
|
+
|
|
1150
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
1151
|
+
const seenPhaseNums = new Set();
|
|
1152
|
+
|
|
609
1153
|
try {
|
|
610
1154
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
611
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
1155
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
1156
|
+
.filter(isDirInMilestone)
|
|
1157
|
+
.sort((a, b) => {
|
|
1158
|
+
const pa = a.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
|
1159
|
+
const pb = b.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
|
1160
|
+
if (!pa || !pb) return a.localeCompare(b);
|
|
1161
|
+
return parseInt(pa[1], 10) - parseInt(pb[1], 10);
|
|
1162
|
+
});
|
|
612
1163
|
|
|
613
1164
|
for (const dir of dirs) {
|
|
614
|
-
const match = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
|
|
1165
|
+
const match = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
615
1166
|
const phaseNumber = match ? match[1] : dir;
|
|
616
1167
|
const phaseName = match && match[2] ? match[2] : null;
|
|
1168
|
+
seenPhaseNums.add(phaseNumber.replace(/^0+/, '') || '0');
|
|
617
1169
|
|
|
618
1170
|
const phasePath = path.join(phasesDir, dir);
|
|
619
1171
|
const phaseFiles = fs.readdirSync(phasePath);
|
|
@@ -629,7 +1181,7 @@ function cmdInitProgress(cwd, raw) {
|
|
|
629
1181
|
const phaseInfo = {
|
|
630
1182
|
number: phaseNumber,
|
|
631
1183
|
name: phaseName,
|
|
632
|
-
directory: '
|
|
1184
|
+
directory: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'phases', dir))),
|
|
633
1185
|
status,
|
|
634
1186
|
plan_count: plans.length,
|
|
635
1187
|
summary_count: summaries.length,
|
|
@@ -646,15 +1198,38 @@ function cmdInitProgress(cwd, raw) {
|
|
|
646
1198
|
nextPhase = phaseInfo;
|
|
647
1199
|
}
|
|
648
1200
|
}
|
|
649
|
-
} catch {}
|
|
1201
|
+
} catch { /* intentionally empty */ }
|
|
1202
|
+
|
|
1203
|
+
// Add phases defined in ROADMAP but not yet scaffolded to disk
|
|
1204
|
+
for (const [num, name] of roadmapPhaseNames) {
|
|
1205
|
+
const stripped = num.replace(/^0+/, '') || '0';
|
|
1206
|
+
if (!seenPhaseNums.has(stripped)) {
|
|
1207
|
+
const phaseInfo = {
|
|
1208
|
+
number: num,
|
|
1209
|
+
name: name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''),
|
|
1210
|
+
directory: null,
|
|
1211
|
+
status: 'not_started',
|
|
1212
|
+
plan_count: 0,
|
|
1213
|
+
summary_count: 0,
|
|
1214
|
+
has_research: false,
|
|
1215
|
+
};
|
|
1216
|
+
phases.push(phaseInfo);
|
|
1217
|
+
if (!nextPhase && !currentPhase) {
|
|
1218
|
+
nextPhase = phaseInfo;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Re-sort phases by number after adding ROADMAP-only phases
|
|
1224
|
+
phases.sort((a, b) => parseInt(a.number, 10) - parseInt(b.number, 10));
|
|
650
1225
|
|
|
651
1226
|
// Check for paused work
|
|
652
1227
|
let pausedAt = null;
|
|
653
1228
|
try {
|
|
654
|
-
const state = fs.readFileSync(path.join(cwd, '
|
|
1229
|
+
const state = fs.readFileSync(path.join(planningDir(cwd), 'STATE.md'), 'utf-8');
|
|
655
1230
|
const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/);
|
|
656
1231
|
if (pauseMatch) pausedAt = pauseMatch[1].trim();
|
|
657
|
-
} catch {}
|
|
1232
|
+
} catch { /* intentionally empty */ }
|
|
658
1233
|
|
|
659
1234
|
const result = {
|
|
660
1235
|
// Models
|
|
@@ -682,18 +1257,248 @@ function cmdInitProgress(cwd, raw) {
|
|
|
682
1257
|
|
|
683
1258
|
// File existence
|
|
684
1259
|
project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
|
|
685
|
-
roadmap_exists:
|
|
686
|
-
state_exists:
|
|
1260
|
+
roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
|
|
1261
|
+
state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
|
|
687
1262
|
// File paths
|
|
688
|
-
state_path: '
|
|
689
|
-
roadmap_path: '
|
|
1263
|
+
state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
|
|
1264
|
+
roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
|
|
690
1265
|
project_path: '.planning/PROJECT.md',
|
|
691
|
-
config_path: '
|
|
1266
|
+
config_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'config.json'))),
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
output(withProjectRoot(cwd, result), raw);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Detect child git repos in a directory (one level deep).
|
|
1274
|
+
* Returns array of { name, path, has_uncommitted } objects.
|
|
1275
|
+
*/
|
|
1276
|
+
function detectChildRepos(dir) {
|
|
1277
|
+
const repos = [];
|
|
1278
|
+
let entries;
|
|
1279
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return repos; }
|
|
1280
|
+
for (const entry of entries) {
|
|
1281
|
+
if (!entry.isDirectory()) continue;
|
|
1282
|
+
if (entry.name.startsWith('.')) continue;
|
|
1283
|
+
const fullPath = path.join(dir, entry.name);
|
|
1284
|
+
const gitDir = path.join(fullPath, '.git');
|
|
1285
|
+
if (fs.existsSync(gitDir)) {
|
|
1286
|
+
let hasUncommitted = false;
|
|
1287
|
+
try {
|
|
1288
|
+
const status = execSync('git status --porcelain', { cwd: fullPath, encoding: 'utf8', timeout: 5000 });
|
|
1289
|
+
hasUncommitted = status.trim().length > 0;
|
|
1290
|
+
} catch { /* best-effort */ }
|
|
1291
|
+
repos.push({ name: entry.name, path: fullPath, has_uncommitted: hasUncommitted });
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return repos;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function cmdInitNewWorkspace(cwd, raw) {
|
|
1298
|
+
const homedir = process.env.HOME || require('os').homedir();
|
|
1299
|
+
const defaultBase = path.join(homedir, 'gsd-workspaces');
|
|
1300
|
+
|
|
1301
|
+
// Detect child git repos for interactive selection
|
|
1302
|
+
const childRepos = detectChildRepos(cwd);
|
|
1303
|
+
|
|
1304
|
+
// Check if git worktree is available
|
|
1305
|
+
let worktreeAvailable = false;
|
|
1306
|
+
try {
|
|
1307
|
+
execSync('git --version', { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
|
|
1308
|
+
worktreeAvailable = true;
|
|
1309
|
+
} catch { /* no git at all */ }
|
|
1310
|
+
|
|
1311
|
+
const result = {
|
|
1312
|
+
default_workspace_base: defaultBase,
|
|
1313
|
+
child_repos: childRepos,
|
|
1314
|
+
child_repo_count: childRepos.length,
|
|
1315
|
+
worktree_available: worktreeAvailable,
|
|
1316
|
+
is_git_repo: pathExistsInternal(cwd, '.git'),
|
|
1317
|
+
cwd_repo_name: path.basename(cwd),
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
output(withProjectRoot(cwd, result), raw);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function cmdInitListWorkspaces(cwd, raw) {
|
|
1324
|
+
const homedir = process.env.HOME || require('os').homedir();
|
|
1325
|
+
const defaultBase = path.join(homedir, 'gsd-workspaces');
|
|
1326
|
+
|
|
1327
|
+
const workspaces = [];
|
|
1328
|
+
if (fs.existsSync(defaultBase)) {
|
|
1329
|
+
let entries;
|
|
1330
|
+
try { entries = fs.readdirSync(defaultBase, { withFileTypes: true }); } catch { entries = []; }
|
|
1331
|
+
for (const entry of entries) {
|
|
1332
|
+
if (!entry.isDirectory()) continue;
|
|
1333
|
+
const wsPath = path.join(defaultBase, entry.name);
|
|
1334
|
+
const manifestPath = path.join(wsPath, 'WORKSPACE.md');
|
|
1335
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
1336
|
+
|
|
1337
|
+
let repoCount = 0;
|
|
1338
|
+
let hasProject = false;
|
|
1339
|
+
let strategy = 'unknown';
|
|
1340
|
+
try {
|
|
1341
|
+
const manifest = fs.readFileSync(manifestPath, 'utf8');
|
|
1342
|
+
const strategyMatch = manifest.match(/^Strategy:\s*(.+)$/m);
|
|
1343
|
+
if (strategyMatch) strategy = strategyMatch[1].trim();
|
|
1344
|
+
// Count table rows (lines starting with |, excluding header and separator)
|
|
1345
|
+
const tableRows = manifest.split('\n').filter(l => l.match(/^\|\s*\w/) && !l.includes('Repo') && !l.includes('---'));
|
|
1346
|
+
repoCount = tableRows.length;
|
|
1347
|
+
} catch { /* best-effort */ }
|
|
1348
|
+
hasProject = fs.existsSync(path.join(wsPath, '.planning', 'PROJECT.md'));
|
|
1349
|
+
|
|
1350
|
+
workspaces.push({
|
|
1351
|
+
name: entry.name,
|
|
1352
|
+
path: wsPath,
|
|
1353
|
+
repo_count: repoCount,
|
|
1354
|
+
strategy,
|
|
1355
|
+
has_project: hasProject,
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const result = {
|
|
1361
|
+
workspace_base: defaultBase,
|
|
1362
|
+
workspaces,
|
|
1363
|
+
workspace_count: workspaces.length,
|
|
692
1364
|
};
|
|
693
1365
|
|
|
694
1366
|
output(result, raw);
|
|
695
1367
|
}
|
|
696
1368
|
|
|
1369
|
+
function cmdInitRemoveWorkspace(cwd, name, raw) {
|
|
1370
|
+
const homedir = process.env.HOME || require('os').homedir();
|
|
1371
|
+
const defaultBase = path.join(homedir, 'gsd-workspaces');
|
|
1372
|
+
|
|
1373
|
+
if (!name) {
|
|
1374
|
+
error('workspace name required for init remove-workspace');
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const wsPath = path.join(defaultBase, name);
|
|
1378
|
+
const manifestPath = path.join(wsPath, 'WORKSPACE.md');
|
|
1379
|
+
|
|
1380
|
+
if (!fs.existsSync(wsPath)) {
|
|
1381
|
+
error(`Workspace not found: ${wsPath}`);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Parse manifest for repo info
|
|
1385
|
+
const repos = [];
|
|
1386
|
+
let strategy = 'unknown';
|
|
1387
|
+
if (fs.existsSync(manifestPath)) {
|
|
1388
|
+
try {
|
|
1389
|
+
const manifest = fs.readFileSync(manifestPath, 'utf8');
|
|
1390
|
+
const strategyMatch = manifest.match(/^Strategy:\s*(.+)$/m);
|
|
1391
|
+
if (strategyMatch) strategy = strategyMatch[1].trim();
|
|
1392
|
+
|
|
1393
|
+
// Parse table rows for repo names and source paths
|
|
1394
|
+
const lines = manifest.split('\n');
|
|
1395
|
+
for (const line of lines) {
|
|
1396
|
+
const match = line.match(/^\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|$/);
|
|
1397
|
+
if (match && match[1] !== 'Repo' && !match[1].includes('---')) {
|
|
1398
|
+
repos.push({ name: match[1], source: match[2], branch: match[3], strategy: match[4] });
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
} catch { /* best-effort */ }
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Check for uncommitted changes in workspace repos
|
|
1405
|
+
const dirtyRepos = [];
|
|
1406
|
+
for (const repo of repos) {
|
|
1407
|
+
const repoPath = path.join(wsPath, repo.name);
|
|
1408
|
+
if (!fs.existsSync(repoPath)) continue;
|
|
1409
|
+
try {
|
|
1410
|
+
const status = execSync('git status --porcelain', { cwd: repoPath, encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
|
|
1411
|
+
if (status.trim().length > 0) {
|
|
1412
|
+
dirtyRepos.push(repo.name);
|
|
1413
|
+
}
|
|
1414
|
+
} catch { /* best-effort */ }
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
const result = {
|
|
1418
|
+
workspace_name: name,
|
|
1419
|
+
workspace_path: wsPath,
|
|
1420
|
+
has_manifest: fs.existsSync(manifestPath),
|
|
1421
|
+
strategy,
|
|
1422
|
+
repos,
|
|
1423
|
+
repo_count: repos.length,
|
|
1424
|
+
dirty_repos: dirtyRepos,
|
|
1425
|
+
has_dirty_repos: dirtyRepos.length > 0,
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
output(result, raw);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* Build a formatted agent skills block for injection into task() prompts.
|
|
1433
|
+
*
|
|
1434
|
+
* Reads `config.agent_skills[agentType]` and validates each skill path exists
|
|
1435
|
+
* within the project root. Returns a formatted `<agent_skills>` block or empty
|
|
1436
|
+
* string if no skills are configured.
|
|
1437
|
+
*
|
|
1438
|
+
* @param {object} config - Loaded project config
|
|
1439
|
+
* @param {string} agentType - The agent type (e.g., 'gsd-executor', 'gsd-planner')
|
|
1440
|
+
* @param {string} projectRoot - Absolute path to project root (for path validation)
|
|
1441
|
+
* @returns {string} Formatted skills block or empty string
|
|
1442
|
+
*/
|
|
1443
|
+
function buildAgentSkillsBlock(config, agentType, projectRoot) {
|
|
1444
|
+
const { validatePath } = require('./security.cjs');
|
|
1445
|
+
|
|
1446
|
+
if (!config || !config.agent_skills || !agentType) return '';
|
|
1447
|
+
|
|
1448
|
+
let skillPaths = config.agent_skills[agentType];
|
|
1449
|
+
if (!skillPaths) return '';
|
|
1450
|
+
|
|
1451
|
+
// Normalize single string to array
|
|
1452
|
+
if (typeof skillPaths === 'string') skillPaths = [skillPaths];
|
|
1453
|
+
if (!Array.isArray(skillPaths) || skillPaths.length === 0) return '';
|
|
1454
|
+
|
|
1455
|
+
const validPaths = [];
|
|
1456
|
+
for (const skillPath of skillPaths) {
|
|
1457
|
+
if (typeof skillPath !== 'string') continue;
|
|
1458
|
+
|
|
1459
|
+
// Validate path safety — must resolve within project root
|
|
1460
|
+
const pathCheck = validatePath(skillPath, projectRoot);
|
|
1461
|
+
if (!pathCheck.safe) {
|
|
1462
|
+
process.stderr.write(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": ${pathCheck.error}\n`);
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// Check that the skill directory and SKILL.md exist
|
|
1467
|
+
const skillMdPath = path.join(projectRoot, skillPath, 'SKILL.md');
|
|
1468
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
1469
|
+
process.stderr.write(`[agent-skills] WARNING: skill not found at "${skillPath}/SKILL.md" — skipping\n`);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
validPaths.push(skillPath);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
if (validPaths.length === 0) return '';
|
|
1477
|
+
|
|
1478
|
+
const lines = validPaths.map(p => `- @${p}/SKILL.md`).join('\n');
|
|
1479
|
+
return `<agent_skills>\nRead these user-configured skills:\n${lines}\n</agent_skills>`;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* Command: output the agent skills block for a given agent type.
|
|
1484
|
+
* Used by workflows: SKILLS=$(node "$TOOLS" agent-skills gsd-executor 2>/dev/null)
|
|
1485
|
+
*/
|
|
1486
|
+
function cmdAgentSkills(cwd, agentType, raw) {
|
|
1487
|
+
if (!agentType) {
|
|
1488
|
+
// No agent type — output empty string silently
|
|
1489
|
+
output('', raw, '');
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const config = loadConfig(cwd);
|
|
1494
|
+
const block = buildAgentSkillsBlock(config, agentType, cwd);
|
|
1495
|
+
// Output raw text (not JSON) so workflows can embed it directly
|
|
1496
|
+
if (block) {
|
|
1497
|
+
process.stdout.write(block);
|
|
1498
|
+
}
|
|
1499
|
+
process.exit(0);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
697
1502
|
module.exports = {
|
|
698
1503
|
cmdInitExecutePhase,
|
|
699
1504
|
cmdInitPlanPhase,
|
|
@@ -707,4 +1512,11 @@ module.exports = {
|
|
|
707
1512
|
cmdInitMilestoneOp,
|
|
708
1513
|
cmdInitMapCodebase,
|
|
709
1514
|
cmdInitProgress,
|
|
1515
|
+
cmdInitManager,
|
|
1516
|
+
cmdInitNewWorkspace,
|
|
1517
|
+
cmdInitListWorkspaces,
|
|
1518
|
+
cmdInitRemoveWorkspace,
|
|
1519
|
+
detectChildRepos,
|
|
1520
|
+
buildAgentSkillsBlock,
|
|
1521
|
+
cmdAgentSkills,
|
|
710
1522
|
};
|