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
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UAT Audit — Cross-phase UAT/VERIFICATION scanner
|
|
3
|
+
*
|
|
4
|
+
* Reads all *-UAT.md and *-VERIFICATION.md files across all phases.
|
|
5
|
+
* Extracts non-passing items. Returns structured JSON for workflow consumption.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { output, error, getMilestonePhaseFilter, planningDir, toPosixPath } = require('./core.cjs');
|
|
11
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
12
|
+
const { requireSafePath, sanitizeForDisplay } = require('./security.cjs');
|
|
13
|
+
|
|
14
|
+
function cmdAuditUat(cwd, raw) {
|
|
15
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
16
|
+
if (!fs.existsSync(phasesDir)) {
|
|
17
|
+
error('No phases directory found in planning directory');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
21
|
+
const results = [];
|
|
22
|
+
|
|
23
|
+
// Scan all phase directories
|
|
24
|
+
const dirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
25
|
+
.filter(e => e.isDirectory())
|
|
26
|
+
.map(e => e.name)
|
|
27
|
+
.filter(isDirInMilestone)
|
|
28
|
+
.sort();
|
|
29
|
+
|
|
30
|
+
for (const dir of dirs) {
|
|
31
|
+
const phaseMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
|
32
|
+
const phaseNum = phaseMatch ? phaseMatch[1] : dir;
|
|
33
|
+
const phaseDir = path.join(phasesDir, dir);
|
|
34
|
+
const files = fs.readdirSync(phaseDir);
|
|
35
|
+
|
|
36
|
+
// Process UAT files
|
|
37
|
+
for (const file of files.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
|
|
38
|
+
const content = fs.readFileSync(path.join(phaseDir, file), 'utf-8');
|
|
39
|
+
const items = parseUatItems(content);
|
|
40
|
+
if (items.length > 0) {
|
|
41
|
+
results.push({
|
|
42
|
+
phase: phaseNum,
|
|
43
|
+
phase_dir: dir,
|
|
44
|
+
file,
|
|
45
|
+
file_path: toPosixPath(path.relative(cwd, path.join(phaseDir, file))),
|
|
46
|
+
type: 'uat',
|
|
47
|
+
status: (extractFrontmatter(content).status || 'unknown'),
|
|
48
|
+
items,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Process VERIFICATION files
|
|
54
|
+
for (const file of files.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
|
|
55
|
+
const content = fs.readFileSync(path.join(phaseDir, file), 'utf-8');
|
|
56
|
+
const status = extractFrontmatter(content).status || 'unknown';
|
|
57
|
+
if (status === 'human_needed' || status === 'gaps_found') {
|
|
58
|
+
const items = parseVerificationItems(content, status);
|
|
59
|
+
if (items.length > 0) {
|
|
60
|
+
results.push({
|
|
61
|
+
phase: phaseNum,
|
|
62
|
+
phase_dir: dir,
|
|
63
|
+
file,
|
|
64
|
+
file_path: toPosixPath(path.relative(cwd, path.join(phaseDir, file))),
|
|
65
|
+
type: 'verification',
|
|
66
|
+
status,
|
|
67
|
+
items,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Compute summary
|
|
75
|
+
const summary = {
|
|
76
|
+
total_files: results.length,
|
|
77
|
+
total_items: results.reduce((sum, r) => sum + r.items.length, 0),
|
|
78
|
+
by_category: {},
|
|
79
|
+
by_phase: {},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (const r of results) {
|
|
83
|
+
if (!summary.by_phase[r.phase]) summary.by_phase[r.phase] = 0;
|
|
84
|
+
for (const item of r.items) {
|
|
85
|
+
summary.by_phase[r.phase]++;
|
|
86
|
+
const cat = item.category || 'unknown';
|
|
87
|
+
summary.by_category[cat] = (summary.by_category[cat] || 0) + 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
output({ results, summary }, raw);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function cmdRenderCheckpoint(cwd, options = {}, raw) {
|
|
95
|
+
const filePath = options.file;
|
|
96
|
+
if (!filePath) {
|
|
97
|
+
error('UAT file required: use uat render-checkpoint --file <path>');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const resolvedPath = requireSafePath(filePath, cwd, 'UAT file', { allowAbsolute: true });
|
|
101
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
102
|
+
error(`UAT file not found: ${filePath}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
106
|
+
const currentTest = parseCurrentTest(content);
|
|
107
|
+
|
|
108
|
+
if (currentTest.complete) {
|
|
109
|
+
error('UAT session is already complete; no pending checkpoint to render');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const checkpoint = buildCheckpoint(currentTest);
|
|
113
|
+
output({
|
|
114
|
+
file_path: toPosixPath(path.relative(cwd, resolvedPath)),
|
|
115
|
+
test_number: currentTest.number,
|
|
116
|
+
test_name: currentTest.name,
|
|
117
|
+
checkpoint,
|
|
118
|
+
}, raw, checkpoint);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseCurrentTest(content) {
|
|
122
|
+
const currentTestMatch = content.match(/##\s*Current Test\s*(?:\n<!--[\s\S]*?-->)?\n([\s\S]*?)(?=\n##\s|$)/i);
|
|
123
|
+
if (!currentTestMatch) {
|
|
124
|
+
error('UAT file is missing a Current Test section');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const section = currentTestMatch[1].trimEnd();
|
|
128
|
+
if (!section.trim()) {
|
|
129
|
+
error('Current Test section is empty');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (/\[testing complete\]/i.test(section)) {
|
|
133
|
+
return { complete: true };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const numberMatch = section.match(/^number:\s*(\d+)\s*$/m);
|
|
137
|
+
const nameMatch = section.match(/^name:\s*(.+)\s*$/m);
|
|
138
|
+
const expectedBlockMatch = section.match(/^expected:\s*\|\n([\s\S]*?)(?=^\w[\w-]*:\s)/m)
|
|
139
|
+
|| section.match(/^expected:\s*\|\n([\s\S]+)/m);
|
|
140
|
+
const expectedInlineMatch = section.match(/^expected:\s*(.+)\s*$/m);
|
|
141
|
+
|
|
142
|
+
if (!numberMatch || !nameMatch || (!expectedBlockMatch && !expectedInlineMatch)) {
|
|
143
|
+
error('Current Test section is malformed');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let expected;
|
|
147
|
+
if (expectedBlockMatch) {
|
|
148
|
+
expected = expectedBlockMatch[1]
|
|
149
|
+
.split('\n')
|
|
150
|
+
.map(line => line.replace(/^ {2}/, ''))
|
|
151
|
+
.join('\n')
|
|
152
|
+
.trim();
|
|
153
|
+
} else {
|
|
154
|
+
expected = expectedInlineMatch[1].trim();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
complete: false,
|
|
159
|
+
number: parseInt(numberMatch[1], 10),
|
|
160
|
+
name: sanitizeForDisplay(nameMatch[1].trim()),
|
|
161
|
+
expected: sanitizeForDisplay(expected),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildCheckpoint(currentTest) {
|
|
166
|
+
return [
|
|
167
|
+
'╔══════════════════════════════════════════════════════════════╗',
|
|
168
|
+
'║ CHECKPOINT: Verification Required ║',
|
|
169
|
+
'╚══════════════════════════════════════════════════════════════╝',
|
|
170
|
+
'',
|
|
171
|
+
`**Test ${currentTest.number}: ${currentTest.name}**`,
|
|
172
|
+
'',
|
|
173
|
+
currentTest.expected,
|
|
174
|
+
'',
|
|
175
|
+
'──────────────────────────────────────────────────────────────',
|
|
176
|
+
'Type `pass` or describe what\'s wrong.',
|
|
177
|
+
'──────────────────────────────────────────────────────────────',
|
|
178
|
+
].join('\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseUatItems(content) {
|
|
182
|
+
const items = [];
|
|
183
|
+
// Match test blocks: ### N. Name\nexpected: ...\nresult: ...\n
|
|
184
|
+
const testPattern = /###\s*(\d+)\.\s*([^\n]+)\nexpected:\s*([^\n]+)\nresult:\s*(\w+)(?:\n(?:reported|reason|blocked_by):\s*[^\n]*)?/g;
|
|
185
|
+
let match;
|
|
186
|
+
while ((match = testPattern.exec(content)) !== null) {
|
|
187
|
+
const [, num, name, expected, result] = match;
|
|
188
|
+
if (result === 'pending' || result === 'skipped' || result === 'blocked') {
|
|
189
|
+
// Extract optional fields — limit to current test block (up to next ### or EOF)
|
|
190
|
+
const afterMatch = content.slice(match.index);
|
|
191
|
+
const nextHeading = afterMatch.indexOf('\n###', 1);
|
|
192
|
+
const blockText = nextHeading > 0 ? afterMatch.slice(0, nextHeading) : afterMatch;
|
|
193
|
+
const reasonMatch = blockText.match(/reason:\s*(.+)/);
|
|
194
|
+
const blockedByMatch = blockText.match(/blocked_by:\s*(.+)/);
|
|
195
|
+
|
|
196
|
+
const item = {
|
|
197
|
+
test: parseInt(num, 10),
|
|
198
|
+
name: name.trim(),
|
|
199
|
+
expected: expected.trim(),
|
|
200
|
+
result,
|
|
201
|
+
category: categorizeItem(result, reasonMatch?.[1], blockedByMatch?.[1]),
|
|
202
|
+
};
|
|
203
|
+
if (reasonMatch) item.reason = reasonMatch[1].trim();
|
|
204
|
+
if (blockedByMatch) item.blocked_by = blockedByMatch[1].trim();
|
|
205
|
+
items.push(item);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return items;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseVerificationItems(content, status) {
|
|
212
|
+
const items = [];
|
|
213
|
+
if (status === 'human_needed') {
|
|
214
|
+
// Extract from human_verification section — look for numbered items or table rows
|
|
215
|
+
const hvSection = content.match(/##\s*Human Verification.*?\n([\s\S]*?)(?=\n##\s|\n---\s|$)/i);
|
|
216
|
+
if (hvSection) {
|
|
217
|
+
const lines = hvSection[1].split('\n');
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
// Match table rows: | N | description | ... |
|
|
220
|
+
const tableMatch = line.match(/\|\s*(\d+)\s*\|\s*([^|]+)/);
|
|
221
|
+
// Match bullet items: - description
|
|
222
|
+
const bulletMatch = line.match(/^[-*]\s+(.+)/);
|
|
223
|
+
// Match numbered items: 1. description
|
|
224
|
+
const numberedMatch = line.match(/^(\d+)\.\s+(.+)/);
|
|
225
|
+
|
|
226
|
+
if (tableMatch) {
|
|
227
|
+
items.push({
|
|
228
|
+
test: parseInt(tableMatch[1], 10),
|
|
229
|
+
name: tableMatch[2].trim(),
|
|
230
|
+
result: 'human_needed',
|
|
231
|
+
category: 'human_uat',
|
|
232
|
+
});
|
|
233
|
+
} else if (numberedMatch) {
|
|
234
|
+
items.push({
|
|
235
|
+
test: parseInt(numberedMatch[1], 10),
|
|
236
|
+
name: numberedMatch[2].trim(),
|
|
237
|
+
result: 'human_needed',
|
|
238
|
+
category: 'human_uat',
|
|
239
|
+
});
|
|
240
|
+
} else if (bulletMatch && bulletMatch[1].length > 10) {
|
|
241
|
+
items.push({
|
|
242
|
+
name: bulletMatch[1].trim(),
|
|
243
|
+
result: 'human_needed',
|
|
244
|
+
category: 'human_uat',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// gaps_found items are already handled by plan-phase --gaps pipeline
|
|
251
|
+
return items;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function categorizeItem(result, reason, blockedBy) {
|
|
255
|
+
if (result === 'blocked' || blockedBy) {
|
|
256
|
+
if (blockedBy) {
|
|
257
|
+
if (/server/i.test(blockedBy)) return 'server_blocked';
|
|
258
|
+
if (/device|physical/i.test(blockedBy)) return 'device_needed';
|
|
259
|
+
if (/build|release|preview/i.test(blockedBy)) return 'build_needed';
|
|
260
|
+
if (/third.party|twilio|stripe/i.test(blockedBy)) return 'third_party';
|
|
261
|
+
}
|
|
262
|
+
return 'blocked';
|
|
263
|
+
}
|
|
264
|
+
if (result === 'skipped') {
|
|
265
|
+
if (reason) {
|
|
266
|
+
if (/server|not running|not available/i.test(reason)) return 'server_blocked';
|
|
267
|
+
if (/simulator|physical|device/i.test(reason)) return 'device_needed';
|
|
268
|
+
if (/build|release|preview/i.test(reason)) return 'build_needed';
|
|
269
|
+
}
|
|
270
|
+
return 'skipped_unresolved';
|
|
271
|
+
}
|
|
272
|
+
if (result === 'pending') return 'pending';
|
|
273
|
+
if (result === 'human_needed') return 'human_uat';
|
|
274
|
+
return 'unknown';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
cmdAuditUat,
|
|
279
|
+
cmdRenderCheckpoint,
|
|
280
|
+
parseCurrentTest,
|
|
281
|
+
buildCheckpoint,
|
|
282
|
+
};
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const { safeReadFile, loadConfig, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, planningDir, planningRoot, output, error, checkAgentsInstalled } = require('./core.cjs');
|
|
8
9
|
const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
|
|
9
10
|
const { writeStateMd } = require('./state.cjs');
|
|
10
11
|
|
|
@@ -395,8 +396,8 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
395
396
|
}
|
|
396
397
|
|
|
397
398
|
function cmdValidateConsistency(cwd, raw) {
|
|
398
|
-
const roadmapPath = path.join(cwd, '
|
|
399
|
-
const phasesDir = path.join(cwd, '
|
|
399
|
+
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
|
400
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
400
401
|
const errors = [];
|
|
401
402
|
const warnings = [];
|
|
402
403
|
|
|
@@ -407,9 +408,10 @@ function cmdValidateConsistency(cwd, raw) {
|
|
|
407
408
|
return;
|
|
408
409
|
}
|
|
409
410
|
|
|
410
|
-
const
|
|
411
|
+
const roadmapContentRaw = fs.readFileSync(roadmapPath, 'utf-8');
|
|
412
|
+
const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
|
|
411
413
|
|
|
412
|
-
// Extract phases from ROADMAP
|
|
414
|
+
// Extract phases from ROADMAP (archived milestones already stripped)
|
|
413
415
|
const roadmapPhases = new Set();
|
|
414
416
|
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
|
|
415
417
|
let m;
|
|
@@ -426,7 +428,7 @@ function cmdValidateConsistency(cwd, raw) {
|
|
|
426
428
|
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
|
427
429
|
if (dm) diskPhases.add(dm[1]);
|
|
428
430
|
}
|
|
429
|
-
} catch {}
|
|
431
|
+
} catch { /* intentionally empty */ }
|
|
430
432
|
|
|
431
433
|
// Check: phases in ROADMAP but not on disk
|
|
432
434
|
for (const p of roadmapPhases) {
|
|
@@ -443,15 +445,18 @@ function cmdValidateConsistency(cwd, raw) {
|
|
|
443
445
|
}
|
|
444
446
|
}
|
|
445
447
|
|
|
446
|
-
// Check: sequential phase numbers (integers only)
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
448
|
+
// Check: sequential phase numbers (integers only, skip in custom naming mode)
|
|
449
|
+
const config = loadConfig(cwd);
|
|
450
|
+
if (config.phase_naming !== 'custom') {
|
|
451
|
+
const integerPhases = [...diskPhases]
|
|
452
|
+
.filter(p => !p.includes('.'))
|
|
453
|
+
.map(p => parseInt(p, 10))
|
|
454
|
+
.sort((a, b) => a - b);
|
|
451
455
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
456
|
+
for (let i = 1; i < integerPhases.length; i++) {
|
|
457
|
+
if (integerPhases[i] !== integerPhases[i - 1] + 1) {
|
|
458
|
+
warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`);
|
|
459
|
+
}
|
|
455
460
|
}
|
|
456
461
|
}
|
|
457
462
|
|
|
@@ -488,7 +493,7 @@ function cmdValidateConsistency(cwd, raw) {
|
|
|
488
493
|
}
|
|
489
494
|
}
|
|
490
495
|
}
|
|
491
|
-
} catch {}
|
|
496
|
+
} catch { /* intentionally empty */ }
|
|
492
497
|
|
|
493
498
|
// Check: frontmatter in plans has required fields
|
|
494
499
|
try {
|
|
@@ -508,19 +513,33 @@ function cmdValidateConsistency(cwd, raw) {
|
|
|
508
513
|
}
|
|
509
514
|
}
|
|
510
515
|
}
|
|
511
|
-
} catch {}
|
|
516
|
+
} catch { /* intentionally empty */ }
|
|
512
517
|
|
|
513
518
|
const passed = errors.length === 0;
|
|
514
519
|
output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed');
|
|
515
520
|
}
|
|
516
521
|
|
|
517
522
|
function cmdValidateHealth(cwd, options, raw) {
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
523
|
+
// Guard: detect if CWD is the home directory (likely accidental)
|
|
524
|
+
const resolved = path.resolve(cwd);
|
|
525
|
+
if (resolved === os.homedir()) {
|
|
526
|
+
output({
|
|
527
|
+
status: 'error',
|
|
528
|
+
errors: [{ code: 'E010', message: `CWD is home directory (${resolved}) — health check would read the wrong .planning/ directory. Run from your project root instead.`, fix: 'cd into your project directory and retry' }],
|
|
529
|
+
warnings: [],
|
|
530
|
+
info: [{ code: 'I010', message: `Resolved CWD: ${resolved}` }],
|
|
531
|
+
repairable_count: 0,
|
|
532
|
+
}, raw);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const planBase = planningDir(cwd);
|
|
537
|
+
const planRoot = planningRoot(cwd);
|
|
538
|
+
const projectPath = path.join(planRoot, 'PROJECT.md');
|
|
539
|
+
const roadmapPath = path.join(planBase, 'ROADMAP.md');
|
|
540
|
+
const statePath = path.join(planBase, 'STATE.md');
|
|
541
|
+
const configPath = path.join(planRoot, 'config.json');
|
|
542
|
+
const phasesDir = path.join(planBase, 'phases');
|
|
524
543
|
|
|
525
544
|
const errors = [];
|
|
526
545
|
const warnings = [];
|
|
@@ -536,7 +555,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
536
555
|
};
|
|
537
556
|
|
|
538
557
|
// ─── Check 1: .planning/ exists ───────────────────────────────────────────
|
|
539
|
-
if (!fs.existsSync(
|
|
558
|
+
if (!fs.existsSync(planBase)) {
|
|
540
559
|
addIssue('error', 'E001', '.planning/ directory not found', 'Run /gsd-new-project to initialize');
|
|
541
560
|
output({
|
|
542
561
|
status: 'broken',
|
|
@@ -584,15 +603,19 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
584
603
|
if (m) diskPhases.add(m[1]);
|
|
585
604
|
}
|
|
586
605
|
}
|
|
587
|
-
} catch {}
|
|
606
|
+
} catch { /* intentionally empty */ }
|
|
588
607
|
// Check for invalid references
|
|
589
608
|
for (const ref of phaseRefs) {
|
|
590
609
|
const normalizedRef = String(parseInt(ref, 10)).padStart(2, '0');
|
|
591
610
|
if (!diskPhases.has(ref) && !diskPhases.has(normalizedRef) && !diskPhases.has(String(parseInt(ref, 10)))) {
|
|
592
611
|
// Only warn if phases dir has any content (not just an empty project)
|
|
593
612
|
if (diskPhases.size > 0) {
|
|
594
|
-
addIssue(
|
|
595
|
-
|
|
613
|
+
addIssue(
|
|
614
|
+
'warning',
|
|
615
|
+
'W002',
|
|
616
|
+
`STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`,
|
|
617
|
+
'Review STATE.md manually before changing it; /gsd-health --repair will not overwrite an existing STATE.md for phase mismatches'
|
|
618
|
+
);
|
|
596
619
|
}
|
|
597
620
|
}
|
|
598
621
|
}
|
|
@@ -607,7 +630,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
607
630
|
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
608
631
|
const parsed = JSON.parse(raw);
|
|
609
632
|
// Validate known fields
|
|
610
|
-
const validProfiles = ['quality', 'balanced', 'budget'];
|
|
633
|
+
const validProfiles = ['quality', 'balanced', 'budget', 'inherit'];
|
|
611
634
|
if (parsed.model_profile && !validProfiles.includes(parsed.model_profile)) {
|
|
612
635
|
addIssue('warning', 'W004', `config.json: invalid model_profile "${parsed.model_profile}"`, `Valid values: ${validProfiles.join(', ')}`);
|
|
613
636
|
}
|
|
@@ -626,7 +649,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
626
649
|
addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', 'Run /gsd-health --repair to add key', true);
|
|
627
650
|
if (!repairs.includes('addNyquistKey')) repairs.push('addNyquistKey');
|
|
628
651
|
}
|
|
629
|
-
} catch {}
|
|
652
|
+
} catch { /* intentionally empty */ }
|
|
630
653
|
}
|
|
631
654
|
|
|
632
655
|
// ─── Check 6: Phase directory naming (NN-name format) ─────────────────────
|
|
@@ -637,7 +660,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
637
660
|
addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
|
|
638
661
|
}
|
|
639
662
|
}
|
|
640
|
-
} catch {}
|
|
663
|
+
} catch { /* intentionally empty */ }
|
|
641
664
|
|
|
642
665
|
// ─── Check 7: Orphaned plans (PLAN without SUMMARY) ───────────────────────
|
|
643
666
|
try {
|
|
@@ -656,7 +679,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
656
679
|
}
|
|
657
680
|
}
|
|
658
681
|
}
|
|
659
|
-
} catch {}
|
|
682
|
+
} catch { /* intentionally empty */ }
|
|
660
683
|
|
|
661
684
|
// ─── Check 7b: Nyquist VALIDATION.md consistency ────────────────────────
|
|
662
685
|
try {
|
|
@@ -674,12 +697,31 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
674
697
|
}
|
|
675
698
|
}
|
|
676
699
|
}
|
|
677
|
-
} catch {}
|
|
700
|
+
} catch { /* intentionally empty */ }
|
|
701
|
+
|
|
702
|
+
// ─── Check 7c: Agent installation (#1371) ──────────────────────────────────
|
|
703
|
+
// Verify GSD agents are installed. Missing agents cause task(subagent_type=...)
|
|
704
|
+
// to silently fall back to general-purpose, losing specialized instructions.
|
|
705
|
+
try {
|
|
706
|
+
const agentStatus = checkAgentsInstalled();
|
|
707
|
+
if (!agentStatus.agents_installed) {
|
|
708
|
+
if (agentStatus.installed_agents.length === 0) {
|
|
709
|
+
addIssue('warning', 'W010',
|
|
710
|
+
`No GSD agents found in ${agentStatus.agents_dir} — task(subagent_type="gsd-*") will fall back to general-purpose`,
|
|
711
|
+
'Run the GSD installer: npx gsd-opencode@latest');
|
|
712
|
+
} else {
|
|
713
|
+
addIssue('warning', 'W010',
|
|
714
|
+
`Missing ${agentStatus.missing_agents.length} GSD agents: ${agentStatus.missing_agents.join(', ')} — affected workflows will fall back to general-purpose`,
|
|
715
|
+
'Run the GSD installer: npx gsd-opencode@latest');
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} catch { /* intentionally empty — agent check is non-blocking */ }
|
|
678
719
|
|
|
679
720
|
// ─── Check 8: Run existing consistency checks ─────────────────────────────
|
|
680
721
|
// Inline subset of cmdValidateConsistency
|
|
681
722
|
if (fs.existsSync(roadmapPath)) {
|
|
682
|
-
const
|
|
723
|
+
const roadmapContentRaw = fs.readFileSync(roadmapPath, 'utf-8');
|
|
724
|
+
const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
|
|
683
725
|
const roadmapPhases = new Set();
|
|
684
726
|
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
|
|
685
727
|
let m;
|
|
@@ -696,7 +738,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
696
738
|
if (dm) diskPhases.add(dm[1]);
|
|
697
739
|
}
|
|
698
740
|
}
|
|
699
|
-
} catch {}
|
|
741
|
+
} catch { /* intentionally empty */ }
|
|
700
742
|
|
|
701
743
|
// Phases in ROADMAP but not on disk
|
|
702
744
|
for (const p of roadmapPhases) {
|
|
@@ -728,10 +770,17 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
728
770
|
commit_docs: true,
|
|
729
771
|
search_gitignored: false,
|
|
730
772
|
branching_strategy: 'none',
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
773
|
+
phase_branch_template: 'gsd/phase-{phase}-{slug}',
|
|
774
|
+
milestone_branch_template: 'gsd/{milestone}-{slug}',
|
|
775
|
+
quick_branch_template: null,
|
|
776
|
+
workflow: {
|
|
777
|
+
research: true,
|
|
778
|
+
plan_check: true,
|
|
779
|
+
verifier: true,
|
|
780
|
+
nyquist_validation: true,
|
|
781
|
+
},
|
|
734
782
|
parallelization: true,
|
|
783
|
+
brave_search: false,
|
|
735
784
|
};
|
|
736
785
|
fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8');
|
|
737
786
|
repairActions.push({ action: repair, success: true, path: 'config.json' });
|
|
@@ -807,6 +856,24 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
807
856
|
}, raw);
|
|
808
857
|
}
|
|
809
858
|
|
|
859
|
+
/**
|
|
860
|
+
* Validate agent installation status (#1371).
|
|
861
|
+
* Returns detailed information about which agents are installed and which are missing.
|
|
862
|
+
*/
|
|
863
|
+
function cmdValidateAgents(cwd, raw) {
|
|
864
|
+
const { MODEL_PROFILES } = require('./model-profiles.cjs');
|
|
865
|
+
const agentStatus = checkAgentsInstalled();
|
|
866
|
+
const expected = Object.keys(MODEL_PROFILES);
|
|
867
|
+
|
|
868
|
+
output({
|
|
869
|
+
agents_dir: agentStatus.agents_dir,
|
|
870
|
+
agents_found: agentStatus.agents_installed,
|
|
871
|
+
installed: agentStatus.installed_agents,
|
|
872
|
+
missing: agentStatus.missing_agents,
|
|
873
|
+
expected,
|
|
874
|
+
}, raw);
|
|
875
|
+
}
|
|
876
|
+
|
|
810
877
|
module.exports = {
|
|
811
878
|
cmdVerifySummary,
|
|
812
879
|
cmdVerifyPlanStructure,
|
|
@@ -817,4 +884,5 @@ module.exports = {
|
|
|
817
884
|
cmdVerifyKeyLinks,
|
|
818
885
|
cmdValidateConsistency,
|
|
819
886
|
cmdValidateHealth,
|
|
887
|
+
cmdValidateAgents,
|
|
820
888
|
};
|