gsdd-cli 0.24.0 → 0.26.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/README.md +97 -546
- package/bin/adapters/claude.mjs +18 -8
- package/bin/adapters/opencode.mjs +3 -3
- package/bin/gsdd.mjs +16 -33
- package/bin/lib/closeout-report.mjs +318 -0
- package/bin/lib/control-map.mjs +698 -26
- package/bin/lib/global-install.mjs +616 -0
- package/bin/lib/global-manifest.mjs +122 -0
- package/bin/lib/health.mjs +48 -30
- package/bin/lib/init-flow.mjs +3 -0
- package/bin/lib/init-prompts.mjs +6 -4
- package/bin/lib/init-runtime.mjs +33 -3
- package/bin/lib/lifecycle-preflight.mjs +58 -1
- package/bin/lib/models.mjs +136 -5
- package/bin/lib/next.mjs +834 -0
- package/bin/lib/phase.mjs +188 -41
- package/bin/lib/rendering.mjs +15 -1
- package/bin/lib/work-context.mjs +760 -0
- package/bin/lib/workflows.mjs +27 -0
- package/distilled/DESIGN.md +17 -9
- package/distilled/README.md +32 -3
- package/distilled/templates/agents.block.md +1 -1
- package/distilled/templates/agents.md +0 -1
- package/distilled/templates/codebase/architecture.md +0 -1
- package/distilled/templates/codebase/concerns.md +0 -1
- package/distilled/templates/codebase/conventions.md +0 -1
- package/distilled/templates/codebase/stack.md +0 -1
- package/distilled/templates/roadmap.md +0 -1
- package/distilled/templates/spec.md +0 -1
- package/distilled/workflows/new-project.md +1 -1
- package/distilled/workflows/verify.md +1 -1
- package/docs/RUNTIME-SUPPORT.md +23 -3
- package/docs/USER-GUIDE.md +40 -4
- package/docs/VERIFICATION-DISCIPLINE.md +3 -1
- package/package.json +2 -2
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
export const GLOBAL_MANIFEST_FILENAME = 'workspine-file-manifest.json';
|
|
6
|
+
|
|
7
|
+
export function sha256(content) {
|
|
8
|
+
return createHash('sha256').update(content).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function fileHash(filePath) {
|
|
12
|
+
return sha256(readFileSync(filePath));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readGlobalManifest(rootDir) {
|
|
16
|
+
const manifestPath = join(rootDir, GLOBAL_MANIFEST_FILENAME);
|
|
17
|
+
if (!existsSync(manifestPath)) return null;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function writeGlobalManifest(rootDir, manifest) {
|
|
27
|
+
mkdirSync(rootDir, { recursive: true });
|
|
28
|
+
writeFileSync(join(rootDir, GLOBAL_MANIFEST_FILENAME), JSON.stringify(manifest, null, 2));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function toManifestPath(rootDir, absolutePath) {
|
|
32
|
+
return relative(rootDir, absolutePath).replace(/\\/g, '/');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function writeManifestTrackedFile({
|
|
36
|
+
rootDir,
|
|
37
|
+
relativePath,
|
|
38
|
+
content,
|
|
39
|
+
previousManifest,
|
|
40
|
+
nextFiles,
|
|
41
|
+
dryRun = false,
|
|
42
|
+
}) {
|
|
43
|
+
const absolutePath = join(rootDir, relativePath);
|
|
44
|
+
const normalizedRelativePath = relativePath.replace(/\\/g, '/');
|
|
45
|
+
const expectedHash = sha256(content);
|
|
46
|
+
const previousHash = previousManifest?.files?.[normalizedRelativePath] || null;
|
|
47
|
+
|
|
48
|
+
if (existsSync(absolutePath)) {
|
|
49
|
+
const currentHash = fileHash(absolutePath);
|
|
50
|
+
if (currentHash === expectedHash) {
|
|
51
|
+
nextFiles[normalizedRelativePath] = expectedHash;
|
|
52
|
+
return { relativePath: normalizedRelativePath, status: 'unchanged' };
|
|
53
|
+
}
|
|
54
|
+
if (!previousHash) {
|
|
55
|
+
return {
|
|
56
|
+
relativePath: normalizedRelativePath,
|
|
57
|
+
status: 'skipped_unmanaged',
|
|
58
|
+
message: 'existing file is not tracked by Workspine manifest',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (currentHash !== previousHash) {
|
|
62
|
+
return {
|
|
63
|
+
relativePath: normalizedRelativePath,
|
|
64
|
+
status: 'skipped_modified',
|
|
65
|
+
message: 'existing Workspine-managed file was modified by the user',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
nextFiles[normalizedRelativePath] = expectedHash;
|
|
71
|
+
if (!dryRun) {
|
|
72
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
73
|
+
writeFileSync(absolutePath, content);
|
|
74
|
+
}
|
|
75
|
+
return { relativePath: normalizedRelativePath, status: dryRun ? 'would_write' : 'written' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function pruneStaleManifestTrackedFiles({
|
|
79
|
+
rootDir,
|
|
80
|
+
previousManifest,
|
|
81
|
+
nextFiles,
|
|
82
|
+
dryRun = false,
|
|
83
|
+
}) {
|
|
84
|
+
if (!previousManifest?.files) return [];
|
|
85
|
+
|
|
86
|
+
const root = resolve(rootDir);
|
|
87
|
+
const results = [];
|
|
88
|
+
for (const [relativePath, previousHash] of Object.entries(previousManifest.files)) {
|
|
89
|
+
const normalizedRelativePath = relativePath.replace(/\\/g, '/');
|
|
90
|
+
if (nextFiles[normalizedRelativePath]) continue;
|
|
91
|
+
|
|
92
|
+
const absolutePath = resolve(rootDir, normalizedRelativePath);
|
|
93
|
+
if (absolutePath !== root && !absolutePath.startsWith(`${root}\\`) && !absolutePath.startsWith(`${root}/`)) {
|
|
94
|
+
results.push({
|
|
95
|
+
relativePath: normalizedRelativePath,
|
|
96
|
+
status: 'skipped_unsafe',
|
|
97
|
+
message: 'previous manifest path resolves outside the install root',
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!existsSync(absolutePath)) {
|
|
103
|
+
results.push({ relativePath: normalizedRelativePath, status: 'removed_missing' });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const currentHash = fileHash(absolutePath);
|
|
108
|
+
if (currentHash !== previousHash) {
|
|
109
|
+
results.push({
|
|
110
|
+
relativePath: normalizedRelativePath,
|
|
111
|
+
status: 'skipped_modified',
|
|
112
|
+
message: 'stale Workspine-managed file was modified by the user',
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!dryRun) rmSync(absolutePath, { force: true });
|
|
118
|
+
results.push({ relativePath: normalizedRelativePath, status: dryRun ? 'would_remove' : 'removed_stale' });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return results;
|
|
122
|
+
}
|
package/bin/lib/health.mjs
CHANGED
|
@@ -14,21 +14,19 @@ import { findUiProofBundleFiles, readUiProofBundleFile, validateUiProofBundle }
|
|
|
14
14
|
import { resolveWorkspaceContext } from './workspace-root.mjs';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
* ctx should provide: { frameworkVersion, workflows }
|
|
17
|
+
* Build the structured health report without printing or mutating workspace
|
|
18
|
+
* state. ctx should provide: { frameworkVersion, workflows }.
|
|
19
19
|
*/
|
|
20
|
-
export function
|
|
21
|
-
return async function cmdHealth(...healthArgs) {
|
|
22
|
-
const jsonMode = healthArgs.includes('--json');
|
|
20
|
+
export function buildHealthReport(ctx, healthArgs = []) {
|
|
23
21
|
const { planningDir, workspaceRoot, invalid, error } = resolveWorkspaceContext(healthArgs);
|
|
24
22
|
if (invalid) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
return {
|
|
24
|
+
status: 'broken',
|
|
25
|
+
errors: [{ id: 'E1', severity: 'ERROR', message: error, fix: 'Pass --workspace-root with a real path or remove the flag.' }],
|
|
26
|
+
warnings: [],
|
|
27
|
+
info: [],
|
|
28
|
+
humanMessage: error,
|
|
29
|
+
};
|
|
32
30
|
}
|
|
33
31
|
const cwd = workspaceRoot;
|
|
34
32
|
const frameworkSourceMode = isFrameworkSourceRepo(cwd);
|
|
@@ -36,13 +34,13 @@ export function createCmdHealth(ctx) {
|
|
|
36
34
|
|
|
37
35
|
// Pre-init guard
|
|
38
36
|
if (!existsSync(join(planningDir, 'config.json'))) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
return {
|
|
38
|
+
status: 'broken',
|
|
39
|
+
errors: [{ id: 'E1', severity: 'ERROR', message: '.planning/config.json missing', fix: 'Run `npx -y gsdd-cli init`' }],
|
|
40
|
+
warnings: [],
|
|
41
|
+
info: [],
|
|
42
|
+
humanMessage: 'Not initialized. Run `npx -y gsdd-cli init`. If `gsdd` is installed globally, `gsdd init` is also fine.',
|
|
43
|
+
};
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
const errors = [];
|
|
@@ -210,11 +208,12 @@ export function createCmdHealth(ctx) {
|
|
|
210
208
|
// W5: Phase dir has PLAN but no SUMMARY (stale in-progress)
|
|
211
209
|
if (lifecycle.incompletePlans.length > 0) {
|
|
212
210
|
for (const plan of lifecycle.incompletePlans) {
|
|
211
|
+
const expectedSummary = `.planning/phases/${plan.dir}/${plan.baseId}-SUMMARY.md`;
|
|
213
212
|
warnings.push({
|
|
214
213
|
id: 'W5',
|
|
215
214
|
severity: 'WARN',
|
|
216
215
|
message: `${plan.displayPath} exists but no matching SUMMARY found (stale in-progress?)`,
|
|
217
|
-
fix:
|
|
216
|
+
fix: `Run \`gsdd execute ${plan.phaseToken}\` to write ${expectedSummary}.`,
|
|
218
217
|
});
|
|
219
218
|
}
|
|
220
219
|
}
|
|
@@ -272,22 +271,41 @@ export function createCmdHealth(ctx) {
|
|
|
272
271
|
const hasWarnings = warnings.length > 0;
|
|
273
272
|
const status = hasErrors ? 'broken' : hasWarnings ? 'degraded' : 'healthy';
|
|
274
273
|
|
|
275
|
-
|
|
274
|
+
return { status, errors, warnings, info };
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Factory function returning the health command.
|
|
278
|
+
* ctx should provide: { frameworkVersion, workflows }
|
|
279
|
+
*/
|
|
280
|
+
export function createCmdHealth(ctx) {
|
|
281
|
+
return async function cmdHealth(...healthArgs) {
|
|
282
|
+
const jsonMode = healthArgs.includes('--json');
|
|
283
|
+
const report = buildHealthReport(ctx, healthArgs);
|
|
284
|
+
const printableReport = {
|
|
285
|
+
status: report.status,
|
|
286
|
+
errors: report.errors,
|
|
287
|
+
warnings: report.warnings,
|
|
288
|
+
info: report.info,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (report.status === 'broken') process.exitCode = 1;
|
|
276
292
|
|
|
277
293
|
if (jsonMode) {
|
|
278
|
-
output(
|
|
294
|
+
output(printableReport);
|
|
295
|
+
} else if (report.humanMessage) {
|
|
296
|
+
console.log(report.humanMessage);
|
|
279
297
|
} else {
|
|
280
|
-
console.log(`\ngsdd health
|
|
281
|
-
if (errors.length > 0) {
|
|
282
|
-
for (const e of errors) console.log(` ERROR: [${e.id}] ${e.message}\n Fix: ${e.fix}`);
|
|
298
|
+
console.log(`\ngsdd health - workspace integrity check\n`);
|
|
299
|
+
if (report.errors.length > 0) {
|
|
300
|
+
for (const e of report.errors) console.log(` ERROR: [${e.id}] ${e.message}\n Fix: ${e.fix}`);
|
|
283
301
|
}
|
|
284
|
-
if (warnings.length > 0) {
|
|
285
|
-
for (const w of warnings) console.log(` WARN: [${w.id}] ${w.message}\n Fix: ${w.fix}`);
|
|
302
|
+
if (report.warnings.length > 0) {
|
|
303
|
+
for (const w of report.warnings) console.log(` WARN: [${w.id}] ${w.message}\n Fix: ${w.fix}`);
|
|
286
304
|
}
|
|
287
|
-
if (info.length > 0) {
|
|
288
|
-
for (const i of info) console.log(` INFO: [${i.id}] ${i.message}${i.fix ? `\n Fix: ${i.fix}` : ''}`);
|
|
305
|
+
if (report.info.length > 0) {
|
|
306
|
+
for (const i of report.info) console.log(` INFO: [${i.id}] ${i.message}${i.fix ? `\n Fix: ${i.fix}` : ''}`);
|
|
289
307
|
}
|
|
290
|
-
console.log(`\n Verdict: ${status.toUpperCase()}\n`);
|
|
308
|
+
console.log(`\n Verdict: ${report.status.toUpperCase()}\n`);
|
|
291
309
|
}
|
|
292
310
|
};
|
|
293
311
|
}
|
package/bin/lib/init-flow.mjs
CHANGED
|
@@ -320,6 +320,7 @@ function ensureGitignoreEntry(cwd, entry, message) {
|
|
|
320
320
|
|
|
321
321
|
function printInitSummary(config) {
|
|
322
322
|
console.log('Config summary:');
|
|
323
|
+
console.log(` - rigorProfile: ${config.rigorProfile}`);
|
|
323
324
|
console.log(` - researchDepth: ${config.researchDepth}`);
|
|
324
325
|
console.log(` - parallelization: ${config.parallelization}`);
|
|
325
326
|
console.log(` - commitDocs: ${config.commitDocs}`);
|
|
@@ -330,6 +331,8 @@ function printInitSummary(config) {
|
|
|
330
331
|
console.log(` - workflow.discuss: ${config.workflow.discuss}`);
|
|
331
332
|
console.log(` - workflow.planCheck: ${config.workflow.planCheck}`);
|
|
332
333
|
console.log(` - workflow.verifier: ${config.workflow.verifier}`);
|
|
334
|
+
console.log(` - workflow.showCode: ${config.workflow.showCode}`);
|
|
335
|
+
console.log(` - workflow.askBeforeDecide: ${config.workflow.askBeforeDecide}`);
|
|
333
336
|
}
|
|
334
337
|
console.log('');
|
|
335
338
|
}
|
package/bin/lib/init-prompts.mjs
CHANGED
|
@@ -54,11 +54,12 @@ export async function promptForConfig(cwd, { input = process.stdin, output = pro
|
|
|
54
54
|
const rigor = await promptSingleSelect({
|
|
55
55
|
input,
|
|
56
56
|
output,
|
|
57
|
-
title: 'Rigor',
|
|
57
|
+
title: 'Rigor - how much the assistant does on its own vs. shows you and asks',
|
|
58
58
|
choices: [
|
|
59
|
-
{ value: '
|
|
60
|
-
{ value: '
|
|
61
|
-
{ value: '
|
|
59
|
+
{ value: 'low', label: 'low', description: 'Autopilot. Skips research, plan-check, and asking - it proceeds on its best guess.' },
|
|
60
|
+
{ value: 'medium', label: 'medium', description: 'Recommended default. Research + a fresh-context plan-check. No helpers, no extra asking.' },
|
|
61
|
+
{ value: 'high', label: 'high', description: 'Helpers propose options; shows you the code before writing; asks when something is genuinely unclear.' },
|
|
62
|
+
{ value: 'max', label: 'max', description: 'Most hands-on. Asks before deciding and shows the running screen. Pick low if you just want it to go.' },
|
|
62
63
|
],
|
|
63
64
|
defaultIndex: 1,
|
|
64
65
|
});
|
|
@@ -88,6 +89,7 @@ export async function promptForConfig(cwd, { input = process.stdin, output = pro
|
|
|
88
89
|
const costConfig = resolveCost(cost);
|
|
89
90
|
|
|
90
91
|
return {
|
|
92
|
+
rigorProfile: rigor,
|
|
91
93
|
...rigorConfig,
|
|
92
94
|
...costConfig,
|
|
93
95
|
commitDocs,
|
package/bin/lib/init-runtime.mjs
CHANGED
|
@@ -172,11 +172,16 @@ Commands:
|
|
|
172
172
|
Launch guided install wizard in TTYs, or use --tools for manual/headless setup
|
|
173
173
|
--auto: non-interactive mode with smart defaults (requires --tools)
|
|
174
174
|
--brief <file>: copy project brief to .planning/PROJECT_BRIEF.md
|
|
175
|
+
install --global [--tools <platform>] [--dry]
|
|
176
|
+
Install reusable Workspine skills and native runtime surfaces into agent home directories
|
|
177
|
+
In TTYs, omitting --tools opens an agent picker
|
|
175
178
|
update [--tools <platform>] [--templates] [--dry]
|
|
176
179
|
Regenerate adapters from latest framework sources
|
|
177
180
|
--templates: also refresh .planning/templates/ and roles
|
|
178
181
|
--dry: preview changes without writing files
|
|
179
182
|
health [--json] Check workspace integrity (healthy/degraded/broken)
|
|
183
|
+
next [--json] [--format auto|json|human] [--init]
|
|
184
|
+
Read \`.work\` continuity state and emit the next coherent agent action
|
|
180
185
|
models [subcommand] Inspect or update model profile / runtime overrides
|
|
181
186
|
find-phase [N] Show phase info as JSON (for agent consumption)
|
|
182
187
|
verify <N> Run artifact checks for phase N
|
|
@@ -194,6 +199,10 @@ Commands:
|
|
|
194
199
|
Compare planned UI proof slots against observed bundles
|
|
195
200
|
control-map [--json] [--with-ignored] [--annotations <path>]
|
|
196
201
|
Report computed repo/worktree/planning state and local annotations
|
|
202
|
+
control-map annotate <set|clear>
|
|
203
|
+
Maintain optional local intent annotations under .planning/.local/
|
|
204
|
+
closeout-report [--json] [--phase <N>]
|
|
205
|
+
Replay read-only closeout status from control-map, health, preflight, verify, and UI-proof signals
|
|
197
206
|
help Show this summary
|
|
198
207
|
|
|
199
208
|
Platforms (for --tools):
|
|
@@ -206,19 +215,31 @@ Platforms (for --tools):
|
|
|
206
215
|
gemini Generate root AGENTS.md governance block; workflows are already discovered natively from .agents/skills/ (legacy alias kept for backward compatibility)
|
|
207
216
|
all Generate all adapters (Claude, OpenCode, Codex, AGENTS.md, Cursor, Copilot, Gemini)
|
|
208
217
|
|
|
218
|
+
Global install targets:
|
|
219
|
+
claude Install ~/.claude skills, commands, and agents
|
|
220
|
+
opencode Install ~/.agents skills plus ~/.config/opencode commands and agents
|
|
221
|
+
codex Install ~/.agents skills plus ~/.codex agents
|
|
222
|
+
copilot Install ~/.agents skills plus ~/.copilot agent profiles
|
|
223
|
+
all Install all global targets above
|
|
224
|
+
|
|
209
225
|
Notes:
|
|
226
|
+
- use \`npx -y gsdd-cli init\` for repo-local setup; use \`npx -y gsdd-cli install --global\` when you want reusable skills in agent homes
|
|
210
227
|
- init always generates open-standard skills at .agents/skills/gsdd-*; this is the shared workflow entry surface
|
|
211
228
|
- init also generates a local .planning/bin/gsdd* helper surface for workflow-embedded lifecycle helpers; it is internal/advanced, not the normal first-run user entrypoint
|
|
229
|
+
- install --global never creates .planning/ in the current repo; it writes only selected agent-home surfaces and per-runtime Workspine manifests
|
|
230
|
+
- repair or refresh a global install by rerunning \`npx -y gsdd-cli install --global --tools <targets>\`; runtime probes stay in test harnesses
|
|
212
231
|
- Workspine is the public product name; the retained package, command, workflow, and workspace contracts stay gsdd-cli, gsdd, gsdd-*, and .planning/
|
|
213
232
|
- running \`npx -y gsdd-cli init\` in a terminal opens the guided runtime-selection wizard; bare \`gsdd init\` is equivalent only when globally installed
|
|
214
233
|
- the wizard lets you pick runtimes first, then separately decide whether repo-wide AGENTS.md governance is worth installing
|
|
215
|
-
- \`npx -y gsdd-cli health\`
|
|
234
|
+
- \`npx -y gsdd-cli health\` is for repo-local .planning/ workspaces; it compares local generated surfaces and points back to \`npx -y gsdd-cli update\` when they drift
|
|
235
|
+
- \`npx -y gsdd-cli next --init\` bootstraps the local .work continuity surface; plain \`next\` is read-only and emits a typed next-action packet
|
|
236
|
+
- \`gsdd next\` defaults to JSON when stdout is captured; use \`--format human\` for the compact supervisor card
|
|
216
237
|
- directly validated launch surfaces in this repo are Claude Code, OpenCode, and Codex CLI
|
|
217
238
|
- Cursor, Copilot, and Gemini are qualified support through the shared .agents/skills/ surface plus optional governance
|
|
218
239
|
- --tools remains the advanced/manual path and preserves legacy runtime aliases for backward compatibility
|
|
219
240
|
- --tools codex generates .codex/agents/gsdd-plan-checker.toml (portable skill is the entry surface; $gsdd-plan is plan-only until explicit $gsdd-execute)
|
|
220
241
|
- root AGENTS.md is only written on init when explicitly requested via --tools agents, --tools all, or the wizard governance opt-in
|
|
221
|
-
- normal
|
|
242
|
+
- normal repo path: npx -y gsdd-cli init -> run /gsdd-* or $gsdd-* -> npx -y gsdd-cli health -> npx -y gsdd-cli update when local repair or refresh is needed
|
|
222
243
|
- post-init, choose your starting lane honestly: new-project for greenfield or fuzzy/milestone work, quick for a concrete bounded change, map-codebase first when the repo needs deeper orientation
|
|
223
244
|
|
|
224
245
|
Examples:
|
|
@@ -234,9 +255,16 @@ Examples:
|
|
|
234
255
|
npx -y gsdd-cli models clear --runtime opencode --agent plan-checker
|
|
235
256
|
npx -y gsdd-cli init --tools agents
|
|
236
257
|
npx -y gsdd-cli init --tools all
|
|
258
|
+
npx -y gsdd-cli install --global
|
|
259
|
+
npx -y gsdd-cli install --global --tools claude,opencode,codex,copilot
|
|
237
260
|
npx -y gsdd-cli update
|
|
261
|
+
npx -y gsdd-cli next --json
|
|
262
|
+
npx -y gsdd-cli next --format human
|
|
263
|
+
npx -y gsdd-cli next --init
|
|
238
264
|
npx -y gsdd-cli find-phase
|
|
239
265
|
npx -y gsdd-cli verify 1
|
|
266
|
+
npx -y gsdd-cli control-map annotate set --id canonical --write-set src/app.ts
|
|
267
|
+
npx -y gsdd-cli control-map annotate clear --id canonical
|
|
240
268
|
npx -y gsdd-cli scaffold phase 4 Payments
|
|
241
269
|
|
|
242
270
|
Workflows (run via skills/adapters generated by init, not direct CLI):
|
|
@@ -265,7 +293,9 @@ Advanced/internal helpers (kept available, but not the primary first-run user st
|
|
|
265
293
|
session-fingerprint Rebaseline the local planning-state fingerprint after review
|
|
266
294
|
phase-status Update ROADMAP.md phase status through the local helper surface
|
|
267
295
|
ui-proof Validate UI proof metadata and compare planned slots to observed bundles
|
|
268
|
-
control-map Report computed repo/worktree/planning state
|
|
296
|
+
control-map Report computed repo/worktree/planning state; annotate only records local intent
|
|
297
|
+
closeout-report Read-only post-merge closure replay; reports blockers, warnings, and next safe action
|
|
298
|
+
next Read-only \`.work\` continuity router for the next coherent agent action
|
|
269
299
|
file-op Deterministic workspace-confined file copy/delete/text mutation
|
|
270
300
|
`;
|
|
271
301
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
2
|
+
import { join, resolve } from 'path';
|
|
3
3
|
import { output } from './cli-utils.mjs';
|
|
4
|
+
import { buildControlMap } from './control-map.mjs';
|
|
4
5
|
import {
|
|
5
6
|
DELIVERY_POSTURES,
|
|
6
7
|
EVIDENCE_KINDS,
|
|
@@ -74,12 +75,14 @@ const RELEASE_CONTRADICTION_CHECKS = Object.freeze([
|
|
|
74
75
|
]);
|
|
75
76
|
|
|
76
77
|
const RELEASE_CONTRADICTION_STATUSES = Object.freeze(['passed', 'failed', 'not_applicable']);
|
|
78
|
+
const PREFLIGHT_CONTROL_MAP_SKIP_CODES = Object.freeze(['planning_state_drift']);
|
|
77
79
|
|
|
78
80
|
export function evaluateLifecyclePreflight({
|
|
79
81
|
planningDir,
|
|
80
82
|
surface,
|
|
81
83
|
phaseNumber = null,
|
|
82
84
|
expectsMutation = 'none',
|
|
85
|
+
controlMapReport = null,
|
|
83
86
|
} = {}) {
|
|
84
87
|
if (!planningDir) {
|
|
85
88
|
throw new Error('planningDir is required');
|
|
@@ -184,6 +187,17 @@ export function evaluateLifecyclePreflight({
|
|
|
184
187
|
}
|
|
185
188
|
}
|
|
186
189
|
|
|
190
|
+
const controlMap = buildPreflightControlMap({
|
|
191
|
+
planningDir,
|
|
192
|
+
policy,
|
|
193
|
+
existingBlockerCodes: new Set(blockers.map((entry) => entry.code)),
|
|
194
|
+
controlMapReport,
|
|
195
|
+
});
|
|
196
|
+
for (const notice of controlMap.notices) {
|
|
197
|
+
if (notice.severity === 'block') blockers.push(notice);
|
|
198
|
+
else warnings.push(notice);
|
|
199
|
+
}
|
|
200
|
+
|
|
187
201
|
if (lifecycle.phaseStatusAlignment.mismatches.length > 0) {
|
|
188
202
|
warnings.push({
|
|
189
203
|
code: 'roadmap_phase_status_mismatch',
|
|
@@ -206,6 +220,7 @@ export function evaluateLifecyclePreflight({
|
|
|
206
220
|
blockers,
|
|
207
221
|
warnings,
|
|
208
222
|
planningState,
|
|
223
|
+
controlMap: controlMap.summary,
|
|
209
224
|
lifecycle: {
|
|
210
225
|
currentMilestone: lifecycle.currentMilestone,
|
|
211
226
|
currentPhase: lifecycle.currentPhase ? lifecycle.currentPhase.number : null,
|
|
@@ -215,6 +230,48 @@ export function evaluateLifecyclePreflight({
|
|
|
215
230
|
};
|
|
216
231
|
}
|
|
217
232
|
|
|
233
|
+
function buildPreflightControlMap({ planningDir, policy, existingBlockerCodes, controlMapReport = null }) {
|
|
234
|
+
const empty = {
|
|
235
|
+
summary: null,
|
|
236
|
+
notices: [],
|
|
237
|
+
};
|
|
238
|
+
if (policy.classification !== 'owned_write' || !existsSync(planningDir)) return empty;
|
|
239
|
+
|
|
240
|
+
const map = controlMapReport || buildControlMap({
|
|
241
|
+
workspaceRoot: resolve(planningDir, '..'),
|
|
242
|
+
planningDir,
|
|
243
|
+
});
|
|
244
|
+
const risks = (map.risks || []).filter((risk) => (
|
|
245
|
+
!PREFLIGHT_CONTROL_MAP_SKIP_CODES.includes(risk.code)
|
|
246
|
+
&& !(existingBlockerCodes.has(risk.code) && risk.severity !== 'block')
|
|
247
|
+
));
|
|
248
|
+
const notices = risks.map((risk) => ({
|
|
249
|
+
...controlMapNotice(risk),
|
|
250
|
+
severity: risk.severity || 'info',
|
|
251
|
+
}));
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
summary: {
|
|
255
|
+
riskCount: map.risks.length,
|
|
256
|
+
noticeCount: notices.length,
|
|
257
|
+
blockerCount: notices.filter((notice) => notice.severity === 'block').length,
|
|
258
|
+
warningCount: notices.filter((notice) => notice.severity !== 'block').length,
|
|
259
|
+
interventions: map.interventions || [],
|
|
260
|
+
},
|
|
261
|
+
notices,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function controlMapNotice(risk) {
|
|
266
|
+
return {
|
|
267
|
+
code: risk.code,
|
|
268
|
+
source: 'control-map',
|
|
269
|
+
message: risk.message,
|
|
270
|
+
artifacts: ['gsdd control-map --json'],
|
|
271
|
+
risk,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
218
275
|
function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
219
276
|
const blockers = [];
|
|
220
277
|
const phaseEntry = lifecycle.phases.find((phase) => phase.number === phaseToken);
|
package/bin/lib/models.mjs
CHANGED
|
@@ -15,21 +15,49 @@ export const DEFAULT_GIT_PROTOCOL = {
|
|
|
15
15
|
pr: 'Follow the existing repo or team review workflow. Do not assume PR creation, timing, or naming unless explicitly requested.',
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
// The rigor knob: one setting that decides how much the assistant does on its own
|
|
19
|
+
// versus how much it shows you and asks. low = autopilot; max = most hands-on.
|
|
20
|
+
// showCode + askBeforeDecide are read by the workflow markdown (=== true means on,
|
|
21
|
+
// a missing key means off), so existing projects whose config predates these flags
|
|
22
|
+
// are unaffected.
|
|
18
23
|
export const RIGOR_PROFILES = {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
low: { researchDepth: 'fast', workflow: { research: false, discuss: false, planCheck: false, verifier: true, showCode: false, askBeforeDecide: false } },
|
|
25
|
+
medium: { researchDepth: 'balanced', workflow: { research: true, discuss: false, planCheck: true, verifier: true, showCode: false, askBeforeDecide: false } },
|
|
26
|
+
high: { researchDepth: 'deep', workflow: { research: true, discuss: true, planCheck: true, verifier: true, showCode: true, askBeforeDecide: false } },
|
|
27
|
+
max: { researchDepth: 'deep', workflow: { research: true, discuss: true, planCheck: true, verifier: true, showCode: true, askBeforeDecide: true } },
|
|
22
28
|
};
|
|
23
29
|
|
|
30
|
+
// Legacy rigor names map silently to the new levels so old configs and callers keep
|
|
31
|
+
// working. medium is behaviorally identical to the old balanced default.
|
|
32
|
+
export const RIGOR_ALIASES = { quick: 'low', balanced: 'medium', thorough: 'high' };
|
|
33
|
+
|
|
34
|
+
export const RIGOR_LEVELS = ['low', 'medium', 'high', 'max'];
|
|
35
|
+
export const RIGOR_STEPS = ['plan', 'execute', 'verify'];
|
|
36
|
+
|
|
24
37
|
export const COST_PROFILES = {
|
|
25
38
|
budget: { modelProfile: 'budget', parallelization: false },
|
|
26
39
|
balanced: { modelProfile: 'balanced', parallelization: true },
|
|
27
40
|
quality: { modelProfile: 'quality', parallelization: true },
|
|
28
41
|
};
|
|
29
42
|
|
|
30
|
-
export function resolveRigor(id) {
|
|
43
|
+
export function resolveRigor(id) {
|
|
44
|
+
const key = RIGOR_ALIASES[id] ?? id;
|
|
45
|
+
return RIGOR_PROFILES[key] ?? RIGOR_PROFILES.medium;
|
|
46
|
+
}
|
|
31
47
|
export function resolveCost(id) { return COST_PROFILES[id] ?? COST_PROFILES.balanced; }
|
|
32
48
|
|
|
49
|
+
// Per-step rigor: an explicit rigorOverrides[step] wins, else the project rigorProfile,
|
|
50
|
+
// else medium. A missing override is not "off" — it just means "follow the main knob".
|
|
51
|
+
export function resolveStepRigor(config, step) {
|
|
52
|
+
const overrideName = config?.rigorOverrides?.[step];
|
|
53
|
+
const baseName = config?.rigorProfile;
|
|
54
|
+
return resolveRigor(overrideName ?? baseName ?? 'medium');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function effectiveRigorLevel(config, step) {
|
|
58
|
+
return config?.rigorOverrides?.[step] ?? config?.rigorProfile ?? 'medium';
|
|
59
|
+
}
|
|
60
|
+
|
|
33
61
|
export const VALID_MODEL_PROFILES = ['quality', 'balanced', 'budget'];
|
|
34
62
|
export const PORTABLE_AGENT_IDS = ['plan-checker', 'approach-explorer'];
|
|
35
63
|
export const MODEL_RUNTIME_IDS = ['claude', 'opencode', 'codex'];
|
|
@@ -40,9 +68,10 @@ export function normalizeModelProfile(value) {
|
|
|
40
68
|
}
|
|
41
69
|
|
|
42
70
|
export function buildDefaultConfig({ autoAdvance = false } = {}) {
|
|
43
|
-
const rigor = resolveRigor('
|
|
71
|
+
const rigor = resolveRigor('medium');
|
|
44
72
|
const cost = resolveCost('balanced');
|
|
45
73
|
const config = {
|
|
74
|
+
rigorProfile: 'medium',
|
|
46
75
|
...rigor,
|
|
47
76
|
...cost,
|
|
48
77
|
commitDocs: true,
|
|
@@ -409,3 +438,105 @@ function cmdModelsClearRuntimeOverride(args) {
|
|
|
409
438
|
console.log(` - cleared ${runtime} runtime override for ${agent}`);
|
|
410
439
|
console.log(' Run gsdd update to regenerate adapter files.');
|
|
411
440
|
}
|
|
441
|
+
|
|
442
|
+
// --- The rigor knob ---------------------------------------------------------
|
|
443
|
+
// gsdd rigor -> show the current level + per-step overrides
|
|
444
|
+
// gsdd rigor <low|medium|high|max>-> set the project-wide level
|
|
445
|
+
// gsdd rigor <plan|execute|verify> <level> -> override a single step
|
|
446
|
+
|
|
447
|
+
function describeRigorFlags(config) {
|
|
448
|
+
const w = config.workflow ?? {};
|
|
449
|
+
return {
|
|
450
|
+
researchDepth: config.researchDepth,
|
|
451
|
+
research: w.research,
|
|
452
|
+
discuss: w.discuss,
|
|
453
|
+
planCheck: w.planCheck,
|
|
454
|
+
verifier: w.verifier,
|
|
455
|
+
showCode: w.showCode,
|
|
456
|
+
askBeforeDecide: w.askBeforeDecide,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function printChangedFlags(before, after) {
|
|
461
|
+
for (const key of Object.keys(after)) {
|
|
462
|
+
if (before[key] !== after[key]) {
|
|
463
|
+
console.log(` ${key}: ${before[key]} -> ${after[key]}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function cmdRigor(...rigorArgs) {
|
|
469
|
+
const [first, second] = rigorArgs;
|
|
470
|
+
if (!first || first === 'show') return cmdRigorShow();
|
|
471
|
+
if (RIGOR_STEPS.includes(first)) return cmdRigorSetStep(first, second);
|
|
472
|
+
if (RIGOR_LEVELS.includes(first)) return cmdRigorSetProfile(first);
|
|
473
|
+
console.error(
|
|
474
|
+
`ERROR: Invalid rigor argument "${first}". Usage: gsdd rigor [show | ${RIGOR_LEVELS.join('|')} | <${RIGOR_STEPS.join('|')}> <level>]`,
|
|
475
|
+
);
|
|
476
|
+
process.exitCode = 1;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function cmdRigorShow() {
|
|
480
|
+
const config = loadProjectModelConfig(process.cwd());
|
|
481
|
+
const base = config.rigorProfile ?? 'medium';
|
|
482
|
+
output({
|
|
483
|
+
rigorProfile: base,
|
|
484
|
+
rigorOverrides: config.rigorOverrides ?? {},
|
|
485
|
+
effective: {
|
|
486
|
+
plan: effectiveRigorLevel(config, 'plan'),
|
|
487
|
+
execute: effectiveRigorLevel(config, 'execute'),
|
|
488
|
+
verify: effectiveRigorLevel(config, 'verify'),
|
|
489
|
+
},
|
|
490
|
+
workflow: config.workflow ?? resolveRigor(base).workflow,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function cmdRigorSetProfile(level) {
|
|
495
|
+
if (!isProjectInitialized()) {
|
|
496
|
+
console.error('ERROR: Project not initialized. Run gsdd init first.');
|
|
497
|
+
process.exitCode = 1;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const result = loadConfigForMutation();
|
|
501
|
+
if (!result.ok) {
|
|
502
|
+
console.error(`ERROR: .planning/config.json is malformed (${result.error}). Fix the file manually before running rigor mutations.`);
|
|
503
|
+
process.exitCode = 1;
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const before = describeRigorFlags(result.config);
|
|
508
|
+
const resolved = resolveRigor(level);
|
|
509
|
+
result.config.rigorProfile = level;
|
|
510
|
+
result.config.researchDepth = resolved.researchDepth;
|
|
511
|
+
result.config.workflow = { ...resolved.workflow };
|
|
512
|
+
const after = describeRigorFlags(result.config);
|
|
513
|
+
writeProjectConfig(result.config);
|
|
514
|
+
|
|
515
|
+
console.log(` - set rigor to ${level}`);
|
|
516
|
+
printChangedFlags(before, after);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function cmdRigorSetStep(step, level) {
|
|
520
|
+
if (!RIGOR_LEVELS.includes(level)) {
|
|
521
|
+
console.error(`ERROR: Invalid rigor level "${level}". Valid levels: ${RIGOR_LEVELS.join(', ')}`);
|
|
522
|
+
process.exitCode = 1;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (!isProjectInitialized()) {
|
|
526
|
+
console.error('ERROR: Project not initialized. Run gsdd init first.');
|
|
527
|
+
process.exitCode = 1;
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const result = loadConfigForMutation();
|
|
531
|
+
if (!result.ok) {
|
|
532
|
+
console.error(`ERROR: .planning/config.json is malformed (${result.error}). Fix the file manually before running rigor mutations.`);
|
|
533
|
+
process.exitCode = 1;
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
result.config.rigorOverrides = result.config.rigorOverrides || {};
|
|
538
|
+
const previous = result.config.rigorOverrides[step] ?? `(follows ${result.config.rigorProfile ?? 'medium'})`;
|
|
539
|
+
result.config.rigorOverrides[step] = level;
|
|
540
|
+
writeProjectConfig(result.config);
|
|
541
|
+
console.log(` - set ${step} rigor override: ${previous} -> ${level}`);
|
|
542
|
+
}
|