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