gsdd-cli 0.1.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/LICENSE +21 -0
- package/README.md +528 -0
- package/agents/DISTILLATION.md +306 -0
- package/agents/README.md +53 -0
- package/agents/debugger.md +82 -0
- package/agents/executor.md +394 -0
- package/agents/integration-checker.md +318 -0
- package/agents/mapper.md +103 -0
- package/agents/planner.md +296 -0
- package/agents/researcher.md +84 -0
- package/agents/roadmapper.md +296 -0
- package/agents/synthesizer.md +236 -0
- package/agents/verifier.md +337 -0
- package/bin/adapters/agents.mjs +33 -0
- package/bin/adapters/claude.mjs +145 -0
- package/bin/adapters/codex.mjs +58 -0
- package/bin/adapters/index.mjs +20 -0
- package/bin/adapters/opencode.mjs +237 -0
- package/bin/gsdd.mjs +102 -0
- package/bin/lib/cli-utils.mjs +28 -0
- package/bin/lib/health.mjs +248 -0
- package/bin/lib/init.mjs +379 -0
- package/bin/lib/manifest.mjs +134 -0
- package/bin/lib/models.mjs +379 -0
- package/bin/lib/phase.mjs +237 -0
- package/bin/lib/rendering.mjs +95 -0
- package/bin/lib/templates.mjs +207 -0
- package/distilled/DESIGN.md +1286 -0
- package/distilled/README.md +169 -0
- package/distilled/SKILL.md +85 -0
- package/distilled/templates/agents.block.md +90 -0
- package/distilled/templates/agents.md +13 -0
- package/distilled/templates/auth-matrix.md +78 -0
- package/distilled/templates/codebase/architecture.md +110 -0
- package/distilled/templates/codebase/concerns.md +95 -0
- package/distilled/templates/codebase/conventions.md +193 -0
- package/distilled/templates/codebase/stack.md +96 -0
- package/distilled/templates/delegates/mapper-arch.md +26 -0
- package/distilled/templates/delegates/mapper-concerns.md +27 -0
- package/distilled/templates/delegates/mapper-quality.md +28 -0
- package/distilled/templates/delegates/mapper-tech.md +25 -0
- package/distilled/templates/delegates/plan-checker.md +55 -0
- package/distilled/templates/delegates/researcher-architecture.md +30 -0
- package/distilled/templates/delegates/researcher-features.md +30 -0
- package/distilled/templates/delegates/researcher-pitfalls.md +30 -0
- package/distilled/templates/delegates/researcher-stack.md +30 -0
- package/distilled/templates/delegates/researcher-synthesizer.md +31 -0
- package/distilled/templates/research/architecture.md +57 -0
- package/distilled/templates/research/features.md +23 -0
- package/distilled/templates/research/pitfalls.md +46 -0
- package/distilled/templates/research/stack.md +45 -0
- package/distilled/templates/research/summary.md +67 -0
- package/distilled/templates/roadmap.md +62 -0
- package/distilled/templates/spec.md +110 -0
- package/distilled/workflows/audit-milestone.md +220 -0
- package/distilled/workflows/execute.md +270 -0
- package/distilled/workflows/map-codebase.md +246 -0
- package/distilled/workflows/new-project.md +418 -0
- package/distilled/workflows/pause.md +121 -0
- package/distilled/workflows/plan.md +383 -0
- package/distilled/workflows/progress.md +199 -0
- package/distilled/workflows/quick.md +187 -0
- package/distilled/workflows/resume.md +152 -0
- package/distilled/workflows/verify.md +307 -0
- package/package.json +45 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// health.mjs — Workspace integrity diagnostics
|
|
2
|
+
//
|
|
3
|
+
// IMPORTANT: No module-scope process.cwd() — ESM caching means sub-modules
|
|
4
|
+
// evaluate once, so CWD must be computed inside function bodies.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { readManifest, detectModifications } from './manifest.mjs';
|
|
9
|
+
import { output } from './cli-utils.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Factory function returning the health command.
|
|
13
|
+
* ctx must provide: { frameworkVersion }
|
|
14
|
+
*/
|
|
15
|
+
export function createCmdHealth(ctx) {
|
|
16
|
+
return async function cmdHealth(...healthArgs) {
|
|
17
|
+
const jsonMode = healthArgs.includes('--json');
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
const planningDir = join(cwd, '.planning');
|
|
20
|
+
|
|
21
|
+
// Pre-init guard
|
|
22
|
+
if (!existsSync(join(planningDir, 'config.json'))) {
|
|
23
|
+
if (jsonMode) {
|
|
24
|
+
output({ status: 'broken', errors: [{ id: 'E1', severity: 'ERROR', message: '.planning/config.json missing', fix: 'Run `gsdd init`' }], warnings: [], info: [] });
|
|
25
|
+
} else {
|
|
26
|
+
console.log('Not initialized. Run `gsdd init`.');
|
|
27
|
+
}
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const errors = [];
|
|
33
|
+
const warnings = [];
|
|
34
|
+
const info = [];
|
|
35
|
+
|
|
36
|
+
// --- ERROR checks ---
|
|
37
|
+
|
|
38
|
+
// E1: config.json missing (already handled by pre-init guard, but keep for completeness)
|
|
39
|
+
// E2: config.json missing required fields
|
|
40
|
+
let config = null;
|
|
41
|
+
try {
|
|
42
|
+
config = JSON.parse(readFileSync(join(planningDir, 'config.json'), 'utf-8'));
|
|
43
|
+
const requiredFields = ['researchDepth', 'modelProfile', 'initVersion'];
|
|
44
|
+
const missing = requiredFields.filter((f) => !(f in config));
|
|
45
|
+
if (missing.length > 0) {
|
|
46
|
+
errors.push({ id: 'E2', severity: 'ERROR', message: `config.json missing required fields: ${missing.join(', ')}`, fix: 'Run `gsdd init` to regenerate' });
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
errors.push({ id: 'E1', severity: 'ERROR', message: '.planning/config.json is unparseable', fix: 'Run `gsdd init`' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// E3: templates/ missing
|
|
53
|
+
const templatesDir = join(planningDir, 'templates');
|
|
54
|
+
const hasTemplatesDir = existsSync(templatesDir);
|
|
55
|
+
const rolesDir = join(templatesDir, 'roles');
|
|
56
|
+
const delegatesDir = join(templatesDir, 'delegates');
|
|
57
|
+
const hasRolesDir = hasTemplatesDir && existsSync(rolesDir);
|
|
58
|
+
const hasDelegatesDir = hasTemplatesDir && existsSync(delegatesDir);
|
|
59
|
+
|
|
60
|
+
if (!hasTemplatesDir) {
|
|
61
|
+
errors.push({ id: 'E3', severity: 'ERROR', message: '.planning/templates/ missing', fix: 'Run `gsdd update --templates`' });
|
|
62
|
+
} else {
|
|
63
|
+
// E4: roles/ missing or empty
|
|
64
|
+
if (!hasRolesDir) {
|
|
65
|
+
errors.push({ id: 'E4', severity: 'ERROR', message: '.planning/templates/roles/ missing', fix: 'Run `gsdd update --templates`' });
|
|
66
|
+
} else {
|
|
67
|
+
const roleFiles = readdirSync(rolesDir).filter((f) => f.endsWith('.md'));
|
|
68
|
+
if (roleFiles.length === 0) {
|
|
69
|
+
errors.push({ id: 'E4', severity: 'ERROR', message: '.planning/templates/roles/ has 0 role files', fix: 'Run `gsdd update --templates`' });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// E5: delegates/ missing or empty
|
|
74
|
+
if (!hasDelegatesDir) {
|
|
75
|
+
errors.push({ id: 'E5', severity: 'ERROR', message: '.planning/templates/delegates/ missing', fix: 'Run `gsdd update --templates`' });
|
|
76
|
+
} else {
|
|
77
|
+
const delegateFiles = readdirSync(delegatesDir).filter((f) => f.endsWith('.md'));
|
|
78
|
+
if (delegateFiles.length === 0) {
|
|
79
|
+
errors.push({ id: 'E5', severity: 'ERROR', message: '.planning/templates/delegates/ has 0 delegate files', fix: 'Run `gsdd update --templates`' });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- WARNING checks ---
|
|
85
|
+
|
|
86
|
+
// W1: generation-manifest.json missing
|
|
87
|
+
const manifest = readManifest(planningDir);
|
|
88
|
+
if (!manifest) {
|
|
89
|
+
warnings.push({ id: 'W1', severity: 'WARN', message: 'generation-manifest.json missing', fix: 'Run `gsdd update --templates` to create' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// W2 + W3: template/role hash mismatches and missing files
|
|
93
|
+
if (manifest && hasTemplatesDir) {
|
|
94
|
+
const allCategories = [
|
|
95
|
+
{ name: 'delegates', dir: delegatesDir, hashes: hasDelegatesDir ? manifest.templates?.delegates : null },
|
|
96
|
+
{ name: 'research', dir: join(templatesDir, 'research'), hashes: manifest.templates?.research },
|
|
97
|
+
{ name: 'codebase', dir: join(templatesDir, 'codebase'), hashes: manifest.templates?.codebase },
|
|
98
|
+
{ name: 'root templates', dir: templatesDir, hashes: manifest.templates?.root },
|
|
99
|
+
{ name: 'roles', dir: rolesDir, hashes: hasRolesDir ? manifest.roles : null },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
for (const cat of allCategories) {
|
|
103
|
+
if (!cat.hashes) continue;
|
|
104
|
+
const result = detectModifications(cat.dir, cat.hashes);
|
|
105
|
+
if (result.modified.length > 0) {
|
|
106
|
+
warnings.push({ id: 'W2', severity: 'WARN', message: `${cat.name}: ${result.modified.length} file(s) modified locally (${result.modified.join(', ')})`, fix: 'Intentional? Run `gsdd update --templates` to reset' });
|
|
107
|
+
}
|
|
108
|
+
if (result.missing.length > 0) {
|
|
109
|
+
warnings.push({ id: 'W3', severity: 'WARN', message: `${cat.name}: ${result.missing.length} file(s) missing from disk (${result.missing.join(', ')})`, fix: 'Run `gsdd update --templates` to restore' });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// W4: ROADMAP.md references phases not found in .planning/phases/
|
|
115
|
+
const roadmapPath = join(planningDir, 'ROADMAP.md');
|
|
116
|
+
const phasesDir = join(planningDir, 'phases');
|
|
117
|
+
const roadmap = existsSync(roadmapPath) ? readFileSync(roadmapPath, 'utf-8') : null;
|
|
118
|
+
const phaseArtifacts = existsSync(phasesDir) ? listPhaseArtifacts(phasesDir) : [];
|
|
119
|
+
|
|
120
|
+
if (roadmap && existsSync(phasesDir)) {
|
|
121
|
+
const phaseNums = [];
|
|
122
|
+
for (const line of roadmap.split('\n')) {
|
|
123
|
+
const match = line.match(/^\s*[-*]\s*\[[ x-]\]\s*\*\*Phase\s+(\d+)/i);
|
|
124
|
+
if (match) phaseNums.push(parseInt(match[1], 10));
|
|
125
|
+
}
|
|
126
|
+
for (const num of phaseNums) {
|
|
127
|
+
const padded = String(num).padStart(2, '0');
|
|
128
|
+
const hasFile = phaseArtifacts.some((artifact) => artifact.phasePrefix === padded || artifact.phasePrefix === String(num));
|
|
129
|
+
if (!hasFile) {
|
|
130
|
+
warnings.push({ id: 'W4', severity: 'WARN', message: `ROADMAP.md references Phase ${num} but no files found in .planning/phases/`, fix: 'Create missing phase dirs or update ROADMAP' });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// W5: Phase dir has PLAN but no SUMMARY (stale in-progress)
|
|
136
|
+
if (phaseArtifacts.length > 0) {
|
|
137
|
+
const plans = phaseArtifacts.filter((artifact) => artifact.name.includes('PLAN'));
|
|
138
|
+
for (const plan of plans) {
|
|
139
|
+
const prefix = plan.name.split('-PLAN')[0];
|
|
140
|
+
const hasSummary = phaseArtifacts.some((artifact) =>
|
|
141
|
+
artifact.dir === plan.dir &&
|
|
142
|
+
artifact.name.startsWith(prefix) &&
|
|
143
|
+
artifact.name.includes('SUMMARY')
|
|
144
|
+
);
|
|
145
|
+
if (!hasSummary) {
|
|
146
|
+
warnings.push({ id: 'W5', severity: 'WARN', message: `${plan.displayPath} exists but no matching SUMMARY found (stale in-progress?)`, fix: 'Resume or complete the phase' });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// W6: No adapter surfaces detected
|
|
152
|
+
const adapterPaths = [
|
|
153
|
+
join(cwd, '.agents', 'skills'),
|
|
154
|
+
join(cwd, '.claude'),
|
|
155
|
+
join(cwd, '.opencode'),
|
|
156
|
+
join(cwd, '.codex'),
|
|
157
|
+
];
|
|
158
|
+
const hasAnyAdapter = adapterPaths.some((p) => existsSync(p));
|
|
159
|
+
if (!hasAnyAdapter) {
|
|
160
|
+
warnings.push({ id: 'W6', severity: 'WARN', message: 'No adapter surfaces detected', fix: 'Run `gsdd init --tools <platform>`' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- INFO checks ---
|
|
164
|
+
|
|
165
|
+
// I1: generation manifest was produced by a different framework version
|
|
166
|
+
if (manifest && manifest.frameworkVersion && manifest.frameworkVersion !== ctx.frameworkVersion) {
|
|
167
|
+
info.push({ id: 'I1', severity: 'INFO', message: `Generation manifest frameworkVersion (${manifest.frameworkVersion}) differs from current framework version (${ctx.frameworkVersion})`, fix: 'Run `gsdd update --templates`' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// I2: Phase completion count
|
|
171
|
+
if (roadmap) {
|
|
172
|
+
const lines = roadmap.split('\n');
|
|
173
|
+
let total = 0;
|
|
174
|
+
let done = 0;
|
|
175
|
+
for (const line of lines) {
|
|
176
|
+
const match = line.match(/^\s*[-*]\s*\[([x ]|-)\]\s*\*\*Phase\s+\d+/i);
|
|
177
|
+
if (match) {
|
|
178
|
+
total++;
|
|
179
|
+
if (match[1] === 'x') done++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (total > 0) {
|
|
183
|
+
info.push({ id: 'I2', severity: 'INFO', message: `Phases: ${done}/${total} completed` });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// I3: Which adapters are installed
|
|
188
|
+
const installedAdapters = [];
|
|
189
|
+
if (existsSync(join(cwd, '.agents', 'skills'))) installedAdapters.push('open-standard-skills');
|
|
190
|
+
if (existsSync(join(cwd, '.claude'))) installedAdapters.push('claude');
|
|
191
|
+
if (existsSync(join(cwd, '.opencode'))) installedAdapters.push('opencode');
|
|
192
|
+
if (existsSync(join(cwd, '.codex'))) installedAdapters.push('codex');
|
|
193
|
+
if (existsSync(join(cwd, 'AGENTS.md'))) installedAdapters.push('agents');
|
|
194
|
+
if (installedAdapters.length > 0) {
|
|
195
|
+
info.push({ id: 'I3', severity: 'INFO', message: `Adapters installed: ${installedAdapters.join(', ')}` });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- Verdict ---
|
|
199
|
+
const hasErrors = errors.length > 0;
|
|
200
|
+
const hasWarnings = warnings.length > 0;
|
|
201
|
+
const status = hasErrors ? 'broken' : hasWarnings ? 'degraded' : 'healthy';
|
|
202
|
+
|
|
203
|
+
if (hasErrors) process.exitCode = 1;
|
|
204
|
+
|
|
205
|
+
if (jsonMode) {
|
|
206
|
+
output({ status, errors, warnings, info });
|
|
207
|
+
} else {
|
|
208
|
+
console.log(`\ngsdd health — workspace integrity check\n`);
|
|
209
|
+
if (errors.length > 0) {
|
|
210
|
+
for (const e of errors) console.log(` ERROR: [${e.id}] ${e.message}\n Fix: ${e.fix}`);
|
|
211
|
+
}
|
|
212
|
+
if (warnings.length > 0) {
|
|
213
|
+
for (const w of warnings) console.log(` WARN: [${w.id}] ${w.message}\n Fix: ${w.fix}`);
|
|
214
|
+
}
|
|
215
|
+
if (info.length > 0) {
|
|
216
|
+
for (const i of info) console.log(` INFO: [${i.id}] ${i.message}${i.fix ? `\n Fix: ${i.fix}` : ''}`);
|
|
217
|
+
}
|
|
218
|
+
console.log(`\n Verdict: ${status.toUpperCase()}\n`);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function listPhaseArtifacts(phasesDir) {
|
|
224
|
+
const artifacts = [];
|
|
225
|
+
for (const entry of readdirSync(phasesDir, { withFileTypes: true })) {
|
|
226
|
+
const entryPath = join(phasesDir, entry.name);
|
|
227
|
+
if (entry.isFile()) {
|
|
228
|
+
artifacts.push(createPhaseArtifact('', entry.name));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (!entry.isDirectory()) continue;
|
|
232
|
+
for (const child of readdirSync(entryPath, { withFileTypes: true })) {
|
|
233
|
+
if (child.isFile()) {
|
|
234
|
+
artifacts.push(createPhaseArtifact(entry.name, child.name));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return artifacts;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function createPhaseArtifact(dir, name) {
|
|
242
|
+
return {
|
|
243
|
+
dir,
|
|
244
|
+
name,
|
|
245
|
+
displayPath: dir ? `${dir}/${name}` : name,
|
|
246
|
+
phasePrefix: name.match(/^(\d+(?:\.\d+)?)-/)?.[1] || dir.match(/^(\d+(?:\.\d+)?)-/)?.[1] || null,
|
|
247
|
+
};
|
|
248
|
+
}
|
package/bin/lib/init.mjs
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// init.mjs - CLI bootstrap, update, and help command implementations
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync } from 'fs';
|
|
4
|
+
import { join, isAbsolute } from 'path';
|
|
5
|
+
import * as readline from 'readline';
|
|
6
|
+
import { renderSkillContent } from './rendering.mjs';
|
|
7
|
+
import { buildManifest, writeManifest } from './manifest.mjs';
|
|
8
|
+
import { parseFlagValue, parseToolsFlag, parseAutoFlag } from './cli-utils.mjs';
|
|
9
|
+
import { DEFAULT_GIT_PROTOCOL, buildDefaultConfig, normalizeModelProfile } from './models.mjs';
|
|
10
|
+
import { installProjectTemplates, refreshTemplates } from './templates.mjs';
|
|
11
|
+
|
|
12
|
+
export function createCmdInit(ctx) {
|
|
13
|
+
return async function cmdInit(...initArgs) {
|
|
14
|
+
console.log('gsdd init - setting up SDD workflow\n');
|
|
15
|
+
|
|
16
|
+
const isAuto = parseAutoFlag(initArgs);
|
|
17
|
+
const toolsFlag = parseFlagValue(initArgs, '--tools');
|
|
18
|
+
const briefFlag = parseFlagValue(initArgs, '--brief');
|
|
19
|
+
let briefSource = null;
|
|
20
|
+
|
|
21
|
+
if (toolsFlag.invalid) {
|
|
22
|
+
console.error('ERROR: --tools requires a value. Example: gsdd init --tools claude');
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (briefFlag.invalid) {
|
|
28
|
+
console.error('ERROR: --brief requires a file path. Example: gsdd init --brief project-idea.md');
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (briefFlag.value) {
|
|
34
|
+
briefSource = isAbsolute(briefFlag.value) ? briefFlag.value : join(ctx.cwd, briefFlag.value);
|
|
35
|
+
if (!existsSync(briefSource)) {
|
|
36
|
+
console.error(`ERROR: Brief file not found: ${briefFlag.value}`);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const parsedTools = parseToolsFlag(initArgs);
|
|
43
|
+
if (isAuto && parsedTools.length === 0) {
|
|
44
|
+
console.error('ERROR: --auto requires --tools <platform>. Example: gsdd init --auto --tools claude');
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const existed = existsSync(ctx.planningDir);
|
|
50
|
+
mkdirSync(join(ctx.planningDir, 'phases'), { recursive: true });
|
|
51
|
+
mkdirSync(join(ctx.planningDir, 'research'), { recursive: true });
|
|
52
|
+
console.log(existed
|
|
53
|
+
? ' - .planning/ already exists (ensured subdirectories)'
|
|
54
|
+
: ' - created .planning/ directory structure');
|
|
55
|
+
|
|
56
|
+
installProjectTemplates(ctx);
|
|
57
|
+
await ensureConfig(ctx.cwd, ctx.planningDir, isAuto);
|
|
58
|
+
|
|
59
|
+
if (briefSource) {
|
|
60
|
+
cpSync(briefSource, join(ctx.planningDir, 'PROJECT_BRIEF.md'));
|
|
61
|
+
console.log(' - copied project brief to .planning/PROJECT_BRIEF.md');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
generateOpenStandardSkills(ctx.cwd, ctx.workflows);
|
|
65
|
+
console.log(' - generated open-standard skills (.agents/skills/gsdd-*)');
|
|
66
|
+
|
|
67
|
+
const requestedTools = normalizeRequestedTools(parsedTools);
|
|
68
|
+
const platforms = parsedTools.length > 0 ? requestedTools : detectPlatforms(ctx.adapters);
|
|
69
|
+
for (const adapter of resolveAdapters(ctx.adapters, platforms)) {
|
|
70
|
+
adapter.generate();
|
|
71
|
+
console.log(` - ${adapter.summary('generated')}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const manifest = buildManifest({ planningDir: ctx.planningDir, frameworkVersion: ctx.frameworkVersion });
|
|
75
|
+
writeManifest(ctx.planningDir, manifest);
|
|
76
|
+
console.log(' - wrote generation manifest');
|
|
77
|
+
|
|
78
|
+
console.log('\nSDD initialized.');
|
|
79
|
+
console.log('Next: run the new-project workflow to produce SPEC.md and ROADMAP.md:\n');
|
|
80
|
+
|
|
81
|
+
if (platforms.includes('claude'))
|
|
82
|
+
console.log(' Claude Code: /gsdd-new-project');
|
|
83
|
+
if (platforms.includes('opencode'))
|
|
84
|
+
console.log(' OpenCode: /gsdd-new-project');
|
|
85
|
+
if (platforms.includes('codex'))
|
|
86
|
+
console.log(' Codex CLI: $gsdd-new-project');
|
|
87
|
+
|
|
88
|
+
// Always show the portable fallback
|
|
89
|
+
console.log(' Any tool: open .agents/skills/gsdd-new-project/SKILL.md\n');
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createCmdUpdate(ctx) {
|
|
94
|
+
return function cmdUpdate(...updateArgs) {
|
|
95
|
+
const isDry = updateArgs.includes('--dry');
|
|
96
|
+
const doTemplates = updateArgs.includes('--templates');
|
|
97
|
+
|
|
98
|
+
console.log(`gsdd update - regenerating adapter files${isDry ? ' (dry run)' : ''}\n`);
|
|
99
|
+
|
|
100
|
+
const parsedTools = parseToolsFlag(updateArgs);
|
|
101
|
+
const requestedTools = normalizeRequestedTools(parsedTools);
|
|
102
|
+
const platforms = parsedTools.length > 0 ? requestedTools : detectPlatforms(ctx.adapters);
|
|
103
|
+
|
|
104
|
+
let updated = false;
|
|
105
|
+
|
|
106
|
+
if (doTemplates) {
|
|
107
|
+
refreshTemplates({ ...ctx, isDry });
|
|
108
|
+
updated = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (platforms.length > 0 || existsSync(join(ctx.cwd, '.agents', 'skills'))) {
|
|
112
|
+
if (isDry) {
|
|
113
|
+
console.log(' - would update open-standard skills (.agents/skills/gsdd-*)');
|
|
114
|
+
} else {
|
|
115
|
+
generateOpenStandardSkills(ctx.cwd, ctx.workflows);
|
|
116
|
+
console.log(' - updated open-standard skills (.agents/skills/gsdd-*)');
|
|
117
|
+
}
|
|
118
|
+
updated = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const adapter of getAdaptersToUpdate(ctx.adapters, platforms)) {
|
|
122
|
+
if (isDry) {
|
|
123
|
+
console.log(` - would update ${adapter.name} adapter`);
|
|
124
|
+
} else {
|
|
125
|
+
adapter.generate();
|
|
126
|
+
console.log(` - ${adapter.summary('updated')}`);
|
|
127
|
+
}
|
|
128
|
+
updated = true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!updated) {
|
|
132
|
+
console.log(' - no adapters found to update (run `gsdd init` first)');
|
|
133
|
+
} else if (isDry) {
|
|
134
|
+
console.log('\nDry run complete. No files were written.\n');
|
|
135
|
+
} else {
|
|
136
|
+
if (doTemplates && existsSync(ctx.planningDir)) {
|
|
137
|
+
const manifest = buildManifest({ planningDir: ctx.planningDir, frameworkVersion: ctx.frameworkVersion });
|
|
138
|
+
writeManifest(ctx.planningDir, manifest);
|
|
139
|
+
console.log(' - updated generation manifest');
|
|
140
|
+
}
|
|
141
|
+
console.log('\nAdapters updated.\n');
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function cmdHelp() {
|
|
147
|
+
console.log(`
|
|
148
|
+
gsdd - GSD Distilled CLI
|
|
149
|
+
Spec-Driven Development for AI coding agents.
|
|
150
|
+
|
|
151
|
+
Usage: gsdd <command> [args]
|
|
152
|
+
|
|
153
|
+
Commands:
|
|
154
|
+
init [--tools <platform>] [--auto] [--brief <file>]
|
|
155
|
+
Set up SDD + generate adapters
|
|
156
|
+
--auto: non-interactive mode with smart defaults (requires --tools)
|
|
157
|
+
--brief <file>: copy project brief to .planning/PROJECT_BRIEF.md
|
|
158
|
+
update [--tools <platform>] [--templates] [--dry]
|
|
159
|
+
Regenerate adapters from latest framework sources
|
|
160
|
+
--templates: also refresh .planning/templates/ and roles
|
|
161
|
+
--dry: preview changes without writing files
|
|
162
|
+
health [--json] Check workspace integrity (healthy/degraded/broken)
|
|
163
|
+
models [subcommand] Inspect or update model profile / runtime overrides
|
|
164
|
+
find-phase [N] Show phase info as JSON (for agent consumption)
|
|
165
|
+
verify <N> Run artifact checks for phase N
|
|
166
|
+
scaffold phase <N> [name] Create a new phase plan file
|
|
167
|
+
|
|
168
|
+
Platforms (for --tools):
|
|
169
|
+
claude Generate Claude Code skills (.claude/skills/gsdd-*), commands (.claude/commands/gsdd-*.md), and native agents (.claude/agents/gsdd-*.md)
|
|
170
|
+
opencode Generate OpenCode local slash commands (.opencode/commands/gsdd-*.md) + native agents (.opencode/agents/gsdd-*.md)
|
|
171
|
+
codex Generate Codex CLI native plan-checker agent (.codex/agents/gsdd-plan-checker.toml)
|
|
172
|
+
agents Generate/Update root AGENTS.md (bounded GSDD block)
|
|
173
|
+
cursor Same as 'agents'
|
|
174
|
+
copilot Same as 'agents'
|
|
175
|
+
gemini Same as 'agents'
|
|
176
|
+
all Generate all adapters (Claude, OpenCode, Codex, AGENTS-based surfaces)
|
|
177
|
+
|
|
178
|
+
Notes:
|
|
179
|
+
- init always generates open-standard skills at .agents/skills/gsdd-* (portable workflow entrypoints)
|
|
180
|
+
- --tools claude also generates native commands at .claude/commands/gsdd-*.md and native agents at .claude/agents/gsdd-*.md
|
|
181
|
+
- --tools opencode also generates native agents at .opencode/agents/gsdd-*.md
|
|
182
|
+
- --tools codex generates .codex/agents/gsdd-plan-checker.toml (portable skill is the entry surface)
|
|
183
|
+
- root AGENTS.md is only written on init when explicitly requested via --tools agents (or all)
|
|
184
|
+
|
|
185
|
+
Examples:
|
|
186
|
+
npx gsdd-cli init
|
|
187
|
+
npx gsdd-cli init --tools claude
|
|
188
|
+
npx gsdd-cli init --auto --tools claude --brief project-idea.md
|
|
189
|
+
npx gsdd-cli init --auto --tools all
|
|
190
|
+
npx gsdd-cli models show
|
|
191
|
+
npx gsdd-cli models profile quality
|
|
192
|
+
npx gsdd-cli models agent-profile --agent plan-checker --profile quality
|
|
193
|
+
npx gsdd-cli models set --runtime opencode --agent plan-checker --model anthropic/claude-opus-4-6
|
|
194
|
+
npx gsdd-cli models clear --runtime opencode --agent plan-checker
|
|
195
|
+
npx gsdd-cli init --tools agents
|
|
196
|
+
npx gsdd-cli init --tools all
|
|
197
|
+
npx gsdd-cli update
|
|
198
|
+
npx gsdd-cli find-phase
|
|
199
|
+
npx gsdd-cli verify 1
|
|
200
|
+
npx gsdd-cli scaffold phase 4 Payments
|
|
201
|
+
|
|
202
|
+
Workflows (run via skills/adapters generated by init, not direct CLI):
|
|
203
|
+
map-codebase Map or refresh codebase (.agents/skills/gsdd-map-codebase/)
|
|
204
|
+
audit-milestone Audit a completed milestone (.agents/skills/gsdd-audit-milestone/)
|
|
205
|
+
`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function detectPlatforms(adapters) {
|
|
209
|
+
return Object.values(adapters)
|
|
210
|
+
.filter((adapter, index, arr) => arr.findIndex((other) => other.id === adapter.id) === index)
|
|
211
|
+
.filter((adapter) => adapter.detect())
|
|
212
|
+
.map((adapter) => adapter.name);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function normalizeRequestedTools(requestedTools) {
|
|
216
|
+
return requestedTools;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function generateOpenStandardSkills(cwd, workflows) {
|
|
220
|
+
for (const workflow of workflows) {
|
|
221
|
+
const dir = join(cwd, '.agents', 'skills', workflow.name);
|
|
222
|
+
mkdirSync(dir, { recursive: true });
|
|
223
|
+
writeFileSync(join(dir, 'SKILL.md'), renderSkillContent(workflow));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function resolveAdapters(adapters, platformNames) {
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
const resolved = [];
|
|
230
|
+
|
|
231
|
+
for (const platformName of platformNames) {
|
|
232
|
+
const adapter = adapters[platformName];
|
|
233
|
+
if (!adapter || seen.has(adapter.id)) continue;
|
|
234
|
+
seen.add(adapter.id);
|
|
235
|
+
resolved.push(adapter);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return resolved;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getAdaptersToUpdate(adapters, platformNames) {
|
|
242
|
+
const requested = new Set(platformNames);
|
|
243
|
+
const seen = new Set();
|
|
244
|
+
const installed = [];
|
|
245
|
+
|
|
246
|
+
for (const [platformName, adapter] of Object.entries(adapters)) {
|
|
247
|
+
if (seen.has(adapter.id)) continue;
|
|
248
|
+
if (!requested.has(platformName) && !adapter.isInstalled()) continue;
|
|
249
|
+
seen.add(adapter.id);
|
|
250
|
+
installed.push(adapter);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return installed;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function ensureConfig(cwd, planningDir, isAuto) {
|
|
257
|
+
const configFile = join(planningDir, 'config.json');
|
|
258
|
+
if (existsSync(configFile)) {
|
|
259
|
+
console.log(' - .planning/config.json already exists');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (isAuto) {
|
|
264
|
+
console.log(' - auto mode: writing config.json with defaults');
|
|
265
|
+
writeFileSync(configFile, JSON.stringify(buildDefaultConfig({ autoAdvance: true }), null, 2));
|
|
266
|
+
console.log(' - saved .planning/config.json (auto mode - autoAdvance enabled)\n');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!process.stdin.isTTY) {
|
|
271
|
+
console.log(' - non-interactive mode detected: writing config.json with defaults');
|
|
272
|
+
writeFileSync(configFile, JSON.stringify(buildDefaultConfig(), null, 2));
|
|
273
|
+
console.log(' - saved .planning/config.json (defaults - re-run gsdd init in a terminal to customize)\n');
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const config = await promptForConfig(cwd);
|
|
278
|
+
writeFileSync(configFile, JSON.stringify(config, null, 2));
|
|
279
|
+
console.log('\n - saved .planning/config.json\n');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function promptForConfig(cwd) {
|
|
283
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
284
|
+
const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
console.log(" Let's configure the planning strategy for this project:\n");
|
|
288
|
+
|
|
289
|
+
console.log(' Research depth:');
|
|
290
|
+
console.log(' - balanced: SOTA research per phase (recommended)');
|
|
291
|
+
console.log(' - fast: skip deep domain research, plan from existing knowledge');
|
|
292
|
+
console.log(' - deep: exhaustive web sweeps + parallel researchers for every feature');
|
|
293
|
+
let researchDepth = await askQuestion(' Depth [balanced/fast/deep] (default: balanced): ');
|
|
294
|
+
researchDepth = researchDepth.trim().toLowerCase();
|
|
295
|
+
if (!['balanced', 'fast', 'deep'].includes(researchDepth)) researchDepth = 'balanced';
|
|
296
|
+
|
|
297
|
+
console.log('\n Parallelization (run independent agents simultaneously):');
|
|
298
|
+
console.log(' - yes: faster, uses more tokens (recommended for non-trivial projects)');
|
|
299
|
+
console.log(' - no: sequential, lower token usage');
|
|
300
|
+
let parallelInput = await askQuestion(' Parallelize agents? [yes/no] (default: yes): ');
|
|
301
|
+
const parallelization = parallelInput.trim().toLowerCase() !== 'no';
|
|
302
|
+
|
|
303
|
+
console.log('\n Version control for planning docs:');
|
|
304
|
+
console.log(' - yes: .planning/ tracked in git (recommended - enables history + recovery)');
|
|
305
|
+
console.log(' - no: .planning/ added to .gitignore (local only)');
|
|
306
|
+
let commitInput = await askQuestion(' Commit planning docs to git? [yes/no] (default: yes): ');
|
|
307
|
+
const commitDocs = commitInput.trim().toLowerCase() !== 'no';
|
|
308
|
+
|
|
309
|
+
console.log('\n AI model profile for planning agents:');
|
|
310
|
+
console.log(' - balanced: capable model for most agents (recommended)');
|
|
311
|
+
console.log(' - quality: most capable model for research/roadmap (higher cost)');
|
|
312
|
+
console.log(' - budget: fastest/cheapest model (lower quality for complex tasks)');
|
|
313
|
+
let modelProfile = await askQuestion(' Model profile [balanced/quality/budget] (default: balanced): ');
|
|
314
|
+
modelProfile = normalizeModelProfile(modelProfile.trim().toLowerCase());
|
|
315
|
+
|
|
316
|
+
console.log('\n Workflow agents (each adds quality but costs tokens/time):');
|
|
317
|
+
let researchInput = await askQuestion(' Research before planning each phase? [yes/no] (default: yes): ');
|
|
318
|
+
const workflowResearch = researchInput.trim().toLowerCase() !== 'no';
|
|
319
|
+
|
|
320
|
+
let planCheckInput = await askQuestion(' Verify plans achieve their goals before executing? [yes/no] (default: yes): ');
|
|
321
|
+
const workflowPlanCheck = planCheckInput.trim().toLowerCase() !== 'no';
|
|
322
|
+
|
|
323
|
+
let verifierInput = await askQuestion(' Verify phase deliverables after execution? [yes/no] (default: yes): ');
|
|
324
|
+
const workflowVerifier = verifierInput.trim().toLowerCase() !== 'no';
|
|
325
|
+
|
|
326
|
+
console.log('\n Version Control Protocol (Advisory)');
|
|
327
|
+
console.log(' This captures preferred guidance. Repo/user conventions still win over framework defaults.');
|
|
328
|
+
|
|
329
|
+
let branchStrategy = await askQuestion(' Branching guidance (default: follow existing repo conventions; use feature branches for significant changes): ');
|
|
330
|
+
branchStrategy = branchStrategy.trim() || DEFAULT_GIT_PROTOCOL.branch;
|
|
331
|
+
|
|
332
|
+
let commitStrategy = await askQuestion(' Commit guidance (default: logical grouping, no phase/plan/task IDs unless requested): ');
|
|
333
|
+
commitStrategy = commitStrategy.trim() || DEFAULT_GIT_PROTOCOL.commit;
|
|
334
|
+
|
|
335
|
+
let prStrategy = await askQuestion(' PR guidance (default: follow existing repo review workflow): ');
|
|
336
|
+
prStrategy = prStrategy.trim() || DEFAULT_GIT_PROTOCOL.pr;
|
|
337
|
+
|
|
338
|
+
if (!commitDocs) {
|
|
339
|
+
ensureGitignoreEntry(cwd);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
researchDepth,
|
|
344
|
+
parallelization,
|
|
345
|
+
commitDocs,
|
|
346
|
+
modelProfile,
|
|
347
|
+
workflow: {
|
|
348
|
+
research: workflowResearch,
|
|
349
|
+
planCheck: workflowPlanCheck,
|
|
350
|
+
verifier: workflowVerifier,
|
|
351
|
+
},
|
|
352
|
+
gitProtocol: {
|
|
353
|
+
branch: branchStrategy,
|
|
354
|
+
commit: commitStrategy,
|
|
355
|
+
pr: prStrategy,
|
|
356
|
+
},
|
|
357
|
+
initVersion: 'v1.1',
|
|
358
|
+
};
|
|
359
|
+
} finally {
|
|
360
|
+
rl.close();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function ensureGitignoreEntry(cwd) {
|
|
365
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
366
|
+
const ignoreEntry = '\n# GSDD planning docs (local only)\n.planning/\n';
|
|
367
|
+
|
|
368
|
+
if (existsSync(gitignorePath)) {
|
|
369
|
+
const existing = readFileSync(gitignorePath, 'utf-8');
|
|
370
|
+
if (!existing.includes('.planning/')) {
|
|
371
|
+
writeFileSync(gitignorePath, existing + ignoreEntry);
|
|
372
|
+
console.log(' - added .planning/ to .gitignore');
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
writeFileSync(gitignorePath, ignoreEntry.trimStart());
|
|
378
|
+
console.log(' - created .gitignore with .planning/ entry');
|
|
379
|
+
}
|