gsdd-cli 0.25.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.
@@ -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
+ }
@@ -208,11 +208,12 @@ export function buildHealthReport(ctx, healthArgs = []) {
208
208
  // W5: Phase dir has PLAN but no SUMMARY (stale in-progress)
209
209
  if (lifecycle.incompletePlans.length > 0) {
210
210
  for (const plan of lifecycle.incompletePlans) {
211
+ const expectedSummary = `.planning/phases/${plan.dir}/${plan.baseId}-SUMMARY.md`;
211
212
  warnings.push({
212
213
  id: 'W5',
213
214
  severity: 'WARN',
214
215
  message: `${plan.displayPath} exists but no matching SUMMARY found (stale in-progress?)`,
215
- fix: 'Resume or complete the phase',
216
+ fix: `Run \`gsdd execute ${plan.phaseToken}\` to write ${expectedSummary}.`,
216
217
  });
217
218
  }
218
219
  }
@@ -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
  }
@@ -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: 'quick', label: 'quick', description: 'Faster setup with lighter planning rigor' },
60
- { value: 'balanced', label: 'balanced', description: 'Recommended default for most projects' },
61
- { value: 'thorough', label: 'thorough', description: 'Maximum research and plan review rigor' },
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,
@@ -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
@@ -210,19 +215,31 @@ Platforms (for --tools):
210
215
  gemini Generate root AGENTS.md governance block; workflows are already discovered natively from .agents/skills/ (legacy alias kept for backward compatibility)
211
216
  all Generate all adapters (Claude, OpenCode, Codex, AGENTS.md, Cursor, Copilot, Gemini)
212
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
+
213
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
214
227
  - init always generates open-standard skills at .agents/skills/gsdd-*; this is the shared workflow entry surface
215
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
216
231
  - Workspine is the public product name; the retained package, command, workflow, and workspace contracts stay gsdd-cli, gsdd, gsdd-*, and .planning/
217
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
218
233
  - the wizard lets you pick runtimes first, then separately decide whether repo-wide AGENTS.md governance is worth installing
219
- - \`npx -y gsdd-cli health\` compares any installed generated runtime surfaces against current render output and points back to \`npx -y gsdd-cli update\` when they drift
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
220
237
  - directly validated launch surfaces in this repo are Claude Code, OpenCode, and Codex CLI
221
238
  - Cursor, Copilot, and Gemini are qualified support through the shared .agents/skills/ surface plus optional governance
222
239
  - --tools remains the advanced/manual path and preserves legacy runtime aliases for backward compatibility
223
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)
224
241
  - root AGENTS.md is only written on init when explicitly requested via --tools agents, --tools all, or the wizard governance opt-in
225
- - normal user path: npx -y gsdd-cli init -> run /gsdd-* or $gsdd-* -> npx -y gsdd-cli health -> npx -y gsdd-cli update when repair or refresh is needed
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
226
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
227
244
 
228
245
  Examples:
@@ -238,7 +255,12 @@ Examples:
238
255
  npx -y gsdd-cli models clear --runtime opencode --agent plan-checker
239
256
  npx -y gsdd-cli init --tools agents
240
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
241
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
242
264
  npx -y gsdd-cli find-phase
243
265
  npx -y gsdd-cli verify 1
244
266
  npx -y gsdd-cli control-map annotate set --id canonical --write-set src/app.ts
@@ -273,6 +295,7 @@ Advanced/internal helpers (kept available, but not the primary first-run user st
273
295
  ui-proof Validate UI proof metadata and compare planned slots to observed bundles
274
296
  control-map Report computed repo/worktree/planning state; annotate only records local intent
275
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
276
299
  file-op Deterministic workspace-confined file copy/delete/text mutation
277
300
  `;
278
301
  }
@@ -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
- quick: { researchDepth: 'fast', workflow: { research: false, discuss: false, planCheck: false, verifier: true } },
20
- balanced: { researchDepth: 'balanced', workflow: { research: true, discuss: false, planCheck: true, verifier: true } },
21
- thorough: { researchDepth: 'deep', workflow: { research: true, discuss: true, planCheck: true, verifier: true } },
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) { return RIGOR_PROFILES[id] ?? RIGOR_PROFILES.balanced; }
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('balanced');
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
+ }