gsdd-cli 0.16.1 → 0.18.1

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.
Files changed (50) hide show
  1. package/README.md +111 -61
  2. package/agents/README.md +1 -1
  3. package/bin/adapters/claude.mjs +8 -1
  4. package/bin/adapters/codex.mjs +5 -1
  5. package/bin/adapters/opencode.mjs +7 -1
  6. package/bin/gsdd.mjs +25 -20
  7. package/bin/lib/evidence-contract.mjs +112 -0
  8. package/bin/lib/health-truth.mjs +29 -31
  9. package/bin/lib/health.mjs +61 -106
  10. package/bin/lib/init-flow.mjs +91 -34
  11. package/bin/lib/init-runtime.mjs +46 -25
  12. package/bin/lib/init.mjs +3 -7
  13. package/bin/lib/lifecycle-preflight.mjs +333 -0
  14. package/bin/lib/lifecycle-state.mjs +476 -0
  15. package/bin/lib/manifest.mjs +24 -20
  16. package/bin/lib/phase.mjs +81 -26
  17. package/bin/lib/provenance.mjs +295 -54
  18. package/bin/lib/rendering.mjs +70 -12
  19. package/bin/lib/runtime-freshness.mjs +264 -0
  20. package/bin/lib/session-fingerprint.mjs +106 -0
  21. package/distilled/DESIGN.md +707 -13
  22. package/distilled/EVIDENCE-INDEX.md +166 -1
  23. package/distilled/README.md +44 -28
  24. package/distilled/SKILL.md +4 -4
  25. package/distilled/templates/agents.block.md +1 -1
  26. package/distilled/workflows/audit-milestone.md +50 -0
  27. package/distilled/workflows/complete-milestone.md +38 -2
  28. package/distilled/workflows/execute.md +16 -3
  29. package/distilled/workflows/map-codebase.md +15 -4
  30. package/distilled/workflows/new-milestone.md +53 -24
  31. package/distilled/workflows/new-project.md +44 -25
  32. package/distilled/workflows/pause.md +1 -1
  33. package/distilled/workflows/plan.md +187 -74
  34. package/distilled/workflows/progress.md +135 -31
  35. package/distilled/workflows/quick.md +20 -12
  36. package/distilled/workflows/resume.md +152 -65
  37. package/distilled/workflows/verify.md +55 -20
  38. package/docs/BROWNFIELD-PROOF.md +95 -0
  39. package/docs/RUNTIME-SUPPORT.md +77 -0
  40. package/docs/USER-GUIDE.md +443 -0
  41. package/docs/VERIFICATION-DISCIPLINE.md +59 -0
  42. package/docs/claude/context-monitor.md +98 -0
  43. package/docs/proof/consumer-node-cli/README.md +37 -0
  44. package/docs/proof/consumer-node-cli/ROADMAP.md +14 -0
  45. package/docs/proof/consumer-node-cli/SPEC.md +17 -0
  46. package/docs/proof/consumer-node-cli/brief.md +9 -0
  47. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -0
  48. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -0
  49. package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -0
  50. package/package.json +26 -20
@@ -1,9 +1,15 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import { evaluateLifecycleState } from './lifecycle-state.mjs';
4
+ import {
5
+ getRuntimeFreshnessRepairGuidance,
6
+ summarizeRuntimeFreshnessIssues,
7
+ } from './runtime-freshness.mjs';
8
+ import { checkDrift } from './session-fingerprint.mjs';
3
9
 
4
- export const TRUTH_CHECK_IDS = ['W7', 'W8', 'W9', 'W10'];
10
+ export const TRUTH_CHECK_IDS = ['W7', 'W8', 'W9', 'W10', 'W11', 'W12'];
5
11
 
6
- export function runTruthChecks(planningDir, frameworkDir, actualCheckIds) {
12
+ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, options = {}) {
7
13
  const warnings = [];
8
14
  const designPath = join(frameworkDir, 'distilled', 'DESIGN.md');
9
15
  const readmePath = join(frameworkDir, 'distilled', 'README.md');
@@ -11,6 +17,7 @@ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds) {
11
17
  const gapsPath = join(frameworkDir, '.internal-research', 'gaps.md');
12
18
  const specPath = join(planningDir, 'SPEC.md');
13
19
  const roadmapPath = join(planningDir, 'ROADMAP.md');
20
+ const lifecycle = evaluateLifecycleState({ planningDir });
14
21
 
15
22
  if (existsSync(designPath)) {
16
23
  const documentedIds = extractHealthTableIds(readFileSync(designPath, 'utf-8'));
@@ -69,16 +76,7 @@ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds) {
69
76
  }
70
77
 
71
78
  if (existsSync(specPath) && existsSync(roadmapPath)) {
72
- const spec = readFileSync(specPath, 'utf-8');
73
- const roadmap = readFileSync(roadmapPath, 'utf-8');
74
- const checkedRequirements = extractCheckedRequirements(spec);
75
- const roadmapRequirements = extractRoadmapRequirements(roadmap);
76
- const mismatches = [];
77
- for (const [requirementId, roadmapStatus] of roadmapRequirements.entries()) {
78
- const specChecked = checkedRequirements.has(requirementId);
79
- if (roadmapStatus && !specChecked) mismatches.push(`${requirementId} phase complete but SPEC unchecked`);
80
- if (!roadmapStatus && specChecked) mismatches.push(`${requirementId} SPEC checked but phase incomplete`);
81
- }
79
+ const mismatches = lifecycle.requirementAlignment.mismatches;
82
80
  if (mismatches.length > 0) {
83
81
  warnings.push({
84
82
  id: 'W10',
@@ -89,6 +87,25 @@ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds) {
89
87
  }
90
88
  }
91
89
 
90
+ if (options.runtimeFreshnessReport?.issueCount > 0) {
91
+ warnings.push({
92
+ id: 'W11',
93
+ severity: 'WARN',
94
+ message: `Installed generated runtime and workflow-helper surfaces drift from current render output (${summarizeRuntimeFreshnessIssues(options.runtimeFreshnessReport)})`,
95
+ fix: getRuntimeFreshnessRepairGuidance(options.runtimeFreshnessReport),
96
+ });
97
+ }
98
+
99
+ const drift = checkDrift(planningDir);
100
+ if (drift.drifted) {
101
+ warnings.push({
102
+ id: 'W12',
103
+ severity: 'WARN',
104
+ message: `Planning state drifted since last recorded session (${drift.details.join('; ')})`,
105
+ fix: 'Review the changes, then run a lifecycle workflow to update the fingerprint',
106
+ });
107
+ }
108
+
92
109
  return warnings;
93
110
  }
94
111
 
@@ -167,22 +184,3 @@ function extractSection(content, startMarker, endMarker) {
167
184
  function normalizeContent(content) {
168
185
  return content.replace(/\r\n/g, '\n');
169
186
  }
170
-
171
- function extractCheckedRequirements(content) {
172
- const checked = new Set();
173
- for (const result of content.matchAll(/- \[x\] \*\*\[([A-Z0-9-]+)]\*\*/g)) {
174
- checked.add(result[1]);
175
- }
176
- return checked;
177
- }
178
-
179
- function extractRoadmapRequirements(content) {
180
- const mapped = new Map();
181
- for (const result of content.matchAll(/- \[([ x-])] \*\*Phase\s+\d+[a-z]?:.*?\*\*\s+—\s+\[([^\]]+)]/g)) {
182
- const requirementIds = result[2].split(',').map((id) => id.trim());
183
- for (const requirementId of requirementIds) {
184
- mapped.set(requirementId, result[1].toLowerCase() === 'x');
185
- }
186
- }
187
- return mapped;
188
- }
@@ -8,10 +8,12 @@ import { join } from 'path';
8
8
  import { readManifest, detectModifications } from './manifest.mjs';
9
9
  import { output } from './cli-utils.mjs';
10
10
  import { runTruthChecks, TRUTH_CHECK_IDS } from './health-truth.mjs';
11
+ import { evaluateLifecycleState } from './lifecycle-state.mjs';
12
+ import { evaluateRuntimeFreshness } from './runtime-freshness.mjs';
11
13
 
12
14
  /**
13
15
  * Factory function returning the health command.
14
- * ctx must provide: { frameworkVersion }
16
+ * ctx should provide: { frameworkVersion, workflows }
15
17
  */
16
18
  export function createCmdHealth(ctx) {
17
19
  return async function cmdHealth(...healthArgs) {
@@ -19,7 +21,7 @@ export function createCmdHealth(ctx) {
19
21
  const cwd = process.cwd();
20
22
  const planningDir = join(cwd, '.planning');
21
23
  const frameworkSourceMode = isFrameworkSourceRepo(cwd);
22
- const healthCheckIds = ['E1', 'E2', 'E3', 'E4', 'E5', 'E6', 'E7', 'E8', 'W1', 'W2', 'W3', 'W4', 'W5', 'W6', ...TRUTH_CHECK_IDS, 'I1', 'I2', 'I3'];
24
+ const healthCheckIds = ['E1', 'E2', 'E3', 'E4', 'E5', 'E6', 'E7', 'E8', 'W1', 'W2', 'W3', 'W4', 'W5', 'W6', ...TRUTH_CHECK_IDS, 'I1', 'I2', 'I3'];
23
25
 
24
26
  // Pre-init guard
25
27
  if (!existsSync(join(planningDir, 'config.json'))) {
@@ -41,8 +43,10 @@ export function createCmdHealth(ctx) {
41
43
  // E1: config.json missing (already handled by pre-init guard, but keep for completeness)
42
44
  // E2: config.json missing required fields
43
45
  let config = null;
46
+ let configOk = false;
44
47
  try {
45
48
  config = JSON.parse(readFileSync(join(planningDir, 'config.json'), 'utf-8'));
49
+ configOk = true;
46
50
  const requiredFields = ['researchDepth', 'modelProfile', 'initVersion'];
47
51
  const missing = requiredFields.filter((f) => !(f in config));
48
52
  if (missing.length > 0) {
@@ -53,8 +57,10 @@ export function createCmdHealth(ctx) {
53
57
  }
54
58
 
55
59
  // E3: templates/ missing
56
- const templatesDir = join(planningDir, 'templates');
57
- const hasTemplatesDir = existsSync(templatesDir);
60
+ const templatesDir = join(planningDir, 'templates');
61
+ const runtimeHelpersDir = join(planningDir, 'bin');
62
+ const hasTemplatesDir = existsSync(templatesDir);
63
+ const hasRuntimeHelpersDir = existsSync(runtimeHelpersDir);
58
64
  const rolesDir = join(templatesDir, 'roles');
59
65
  const delegatesDir = join(templatesDir, 'delegates');
60
66
  const hasRolesDir = hasTemplatesDir && existsSync(rolesDir);
@@ -117,76 +123,60 @@ export function createCmdHealth(ctx) {
117
123
  // --- WARNING checks ---
118
124
 
119
125
  // W1: generation-manifest.json missing
120
- const manifest = skipInstalledTemplateChecks ? null : readManifest(planningDir);
121
- if (!manifest && !skipInstalledTemplateChecks) {
122
- warnings.push({ id: 'W1', severity: 'WARN', message: 'generation-manifest.json missing', fix: 'Run `gsdd update --templates` to create' });
123
- }
126
+ const manifest = skipInstalledTemplateChecks ? null : readManifest(planningDir);
127
+ if (!manifest && !skipInstalledTemplateChecks) {
128
+ warnings.push({ id: 'W1', severity: 'WARN', message: 'generation-manifest.json missing', fix: 'Run `gsdd update` to create' });
129
+ }
124
130
 
125
131
  // W2 + W3: template/role hash mismatches and missing files
126
132
  if (manifest && hasTemplatesDir) {
127
- const allCategories = [
128
- { name: 'delegates', dir: delegatesDir, hashes: hasDelegatesDir ? manifest.templates?.delegates : null },
129
- { name: 'research', dir: join(templatesDir, 'research'), hashes: manifest.templates?.research },
130
- { name: 'codebase', dir: join(templatesDir, 'codebase'), hashes: manifest.templates?.codebase },
131
- { name: 'root templates', dir: templatesDir, hashes: manifest.templates?.root },
132
- { name: 'roles', dir: rolesDir, hashes: hasRolesDir ? manifest.roles : null },
133
- ];
134
-
135
- for (const cat of allCategories) {
136
- if (!cat.hashes) continue;
137
- const result = detectModifications(cat.dir, cat.hashes);
138
- if (result.modified.length > 0) {
139
- 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' });
140
- }
141
- if (result.missing.length > 0) {
142
- 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' });
143
- }
144
- }
145
- }
133
+ const allCategories = [
134
+ { name: 'delegates', dir: delegatesDir, hashes: hasDelegatesDir ? manifest.templates?.delegates : null, fixCommand: 'gsdd update --templates' },
135
+ { name: 'research', dir: join(templatesDir, 'research'), hashes: manifest.templates?.research, fixCommand: 'gsdd update --templates' },
136
+ { name: 'codebase', dir: join(templatesDir, 'codebase'), hashes: manifest.templates?.codebase, fixCommand: 'gsdd update --templates' },
137
+ { name: 'root templates', dir: templatesDir, hashes: manifest.templates?.root, fixCommand: 'gsdd update --templates' },
138
+ { name: 'roles', dir: rolesDir, hashes: hasRolesDir ? manifest.roles : null, fixCommand: 'gsdd update --templates' },
139
+ { name: 'runtime helpers', dir: planningDir, hashes: hasRuntimeHelpersDir ? manifest.runtimeHelpers : null, fixCommand: 'gsdd update' },
140
+ ];
141
+
142
+ for (const cat of allCategories) {
143
+ if (!cat.hashes) continue;
144
+ const result = detectModifications(cat.dir, cat.hashes);
145
+ if (result.modified.length > 0) {
146
+ warnings.push({ id: 'W2', severity: 'WARN', message: `${cat.name}: ${result.modified.length} file(s) modified locally (${result.modified.join(', ')})`, fix: `Intentional? Run \`${cat.fixCommand}\` to reset` });
147
+ }
148
+ if (result.missing.length > 0) {
149
+ warnings.push({ id: 'W3', severity: 'WARN', message: `${cat.name}: ${result.missing.length} file(s) missing from disk (${result.missing.join(', ')})`, fix: `Run \`${cat.fixCommand}\` to restore` });
150
+ }
151
+ }
152
+ }
146
153
 
147
154
  // W4: ROADMAP.md references phases not found in .planning/phases/
148
155
  const roadmapPath = join(planningDir, 'ROADMAP.md');
149
156
  const phasesDir = join(planningDir, 'phases');
150
157
  const roadmap = existsSync(roadmapPath) ? readFileSync(roadmapPath, 'utf-8') : null;
151
- const phaseArtifacts = existsSync(phasesDir) ? listPhaseArtifacts(phasesDir) : [];
158
+ const lifecycle = evaluateLifecycleState({ planningDir });
152
159
 
153
160
  if (roadmap && existsSync(phasesDir)) {
154
- const phaseLabels = [];
155
- let inDetails = false;
156
- for (const line of roadmap.split('\n')) {
157
- if (line.includes('<details>') && !line.includes('</details>')) {
158
- inDetails = true;
159
- continue;
160
- }
161
- if (line.includes('</details>')) {
162
- inDetails = false;
163
- continue;
164
- }
165
- if (inDetails) continue;
166
- const match = line.match(/^\s*[-*]\s*\[([x-])\]\s*\*\*Phase\s+(\d+[a-z]?)/i);
167
- if (match) phaseLabels.push(normalizePhaseLabel(match[2]));
168
- }
169
- for (const label of phaseLabels) {
170
- const hasFile = phaseArtifacts.some((artifact) => normalizePhaseLabel(artifact.phasePrefix) === label);
171
- if (!hasFile) {
172
- warnings.push({ id: 'W4', severity: 'WARN', message: `ROADMAP.md references active Phase ${label} but no files found in .planning/phases/`, fix: 'Create missing phase dirs or update ROADMAP' });
173
- }
161
+ for (const phase of lifecycle.phases.filter((entry) => entry.status !== 'not_started' && !entry.hasArtifacts)) {
162
+ warnings.push({
163
+ id: 'W4',
164
+ severity: 'WARN',
165
+ message: `ROADMAP.md references active Phase ${phase.number} but no files found in .planning/phases/`,
166
+ fix: 'Create missing phase dirs or update ROADMAP',
167
+ });
174
168
  }
175
169
  }
176
170
 
177
171
  // W5: Phase dir has PLAN but no SUMMARY (stale in-progress)
178
- if (phaseArtifacts.length > 0) {
179
- const plans = phaseArtifacts.filter((artifact) => artifact.name.includes('PLAN'));
180
- for (const plan of plans) {
181
- const prefix = plan.name.split('-PLAN')[0];
182
- const hasSummary = phaseArtifacts.some((artifact) =>
183
- artifact.dir === plan.dir &&
184
- artifact.name.startsWith(prefix) &&
185
- artifact.name.includes('SUMMARY')
186
- );
187
- if (!hasSummary) {
188
- warnings.push({ id: 'W5', severity: 'WARN', message: `${plan.displayPath} exists but no matching SUMMARY found (stale in-progress?)`, fix: 'Resume or complete the phase' });
189
- }
172
+ if (lifecycle.incompletePlans.length > 0) {
173
+ for (const plan of lifecycle.incompletePlans) {
174
+ warnings.push({
175
+ id: 'W5',
176
+ severity: 'WARN',
177
+ message: `${plan.displayPath} exists but no matching SUMMARY found (stale in-progress?)`,
178
+ fix: 'Resume or complete the phase',
179
+ });
190
180
  }
191
181
  }
192
182
 
@@ -202,7 +192,11 @@ export function createCmdHealth(ctx) {
202
192
  warnings.push({ id: 'W6', severity: 'WARN', message: 'No adapter surfaces detected', fix: 'Run `gsdd init --tools <platform>`' });
203
193
  }
204
194
 
205
- warnings.push(...runTruthChecks(planningDir, cwd, healthCheckIds));
195
+ const runtimeFreshnessReport = configOk && Array.isArray(ctx.workflows)
196
+ ? evaluateRuntimeFreshness({ cwd, workflows: ctx.workflows })
197
+ : null;
198
+
199
+ warnings.push(...runTruthChecks(planningDir, cwd, healthCheckIds, { runtimeFreshnessReport }));
206
200
 
207
201
  // --- INFO checks ---
208
202
 
@@ -212,20 +206,12 @@ export function createCmdHealth(ctx) {
212
206
  }
213
207
 
214
208
  // I2: Phase completion count
215
- if (roadmap) {
216
- const lines = roadmap.split('\n');
217
- let total = 0;
218
- let done = 0;
219
- for (const line of lines) {
220
- const match = line.match(/^\s*[-*]\s*\[([x ]|-)\]\s*\*\*Phase\s+\d+/i);
221
- if (match) {
222
- total++;
223
- if (match[1] === 'x') done++;
224
- }
225
- }
226
- if (total > 0) {
227
- info.push({ id: 'I2', severity: 'INFO', message: `Phases: ${done}/${total} completed` });
228
- }
209
+ if (lifecycle.counts.total > 0) {
210
+ info.push({
211
+ id: 'I2',
212
+ severity: 'INFO',
213
+ message: `Phases: ${lifecycle.counts.completed}/${lifecycle.counts.total} completed`,
214
+ });
229
215
  }
230
216
 
231
217
  // I3: Which adapters are installed
@@ -268,34 +254,3 @@ function isFrameworkSourceRepo(cwd) {
268
254
  return existsSync(join(cwd, 'distilled', 'templates')) && existsSync(join(cwd, 'distilled', 'workflows'));
269
255
  }
270
256
 
271
- function normalizePhaseLabel(value) {
272
- if (!value) return null;
273
- return value.toLowerCase().replace(/^0+(\d)/, '$1');
274
- }
275
-
276
- function listPhaseArtifacts(phasesDir) {
277
- const artifacts = [];
278
- for (const entry of readdirSync(phasesDir, { withFileTypes: true })) {
279
- const entryPath = join(phasesDir, entry.name);
280
- if (entry.isFile()) {
281
- artifacts.push(createPhaseArtifact('', entry.name));
282
- continue;
283
- }
284
- if (!entry.isDirectory()) continue;
285
- for (const child of readdirSync(entryPath, { withFileTypes: true })) {
286
- if (child.isFile()) {
287
- artifacts.push(createPhaseArtifact(entry.name, child.name));
288
- }
289
- }
290
- }
291
- return artifacts;
292
- }
293
-
294
- function createPhaseArtifact(dir, name) {
295
- return {
296
- dir,
297
- name,
298
- displayPath: dir ? `${dir}/${name}` : name,
299
- phasePrefix: name.match(/^(\d+[a-z]?(?:\.\d+)?)-/i)?.[1] || dir.match(/^(\d+[a-z]?(?:\.\d+)?)-/i)?.[1] || null,
300
- };
301
- }
@@ -1,7 +1,7 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync } from 'fs';
2
- import { join, isAbsolute } from 'path';
3
- import { renderSkillContent } from './rendering.mjs';
4
- import { buildManifest, writeManifest } from './manifest.mjs';
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync } from 'fs';
2
+ import { join, isAbsolute } from 'path';
3
+ import { renderPlanningCliLauncher, renderSkillContent } from './rendering.mjs';
4
+ import { buildManifest, readManifest, writeManifest } from './manifest.mjs';
5
5
  import { parseFlagValue, parseToolsFlag, parseAutoFlag } from './cli-utils.mjs';
6
6
  import { buildDefaultConfig, COST_PROFILES, RIGOR_PROFILES } from './models.mjs';
7
7
  import { installProjectTemplates, refreshTemplates } from './templates.mjs';
@@ -106,8 +106,11 @@ export function createCmdInit(ctx) {
106
106
  console.log(' - copied project brief to .planning/PROJECT_BRIEF.md');
107
107
  }
108
108
 
109
- generateOpenStandardSkills(ctx.cwd, ctx.workflows);
110
- console.log(' - generated open-standard skills (.agents/skills/gsdd-*)');
109
+ generateOpenStandardSkills(ctx.cwd, ctx.workflows);
110
+ console.log(' - generated open-standard skills (.agents/skills/gsdd-*)');
111
+
112
+ generatePlanningCliLauncher(ctx);
113
+ console.log(' - generated local workflow helper (.planning/bin/gsdd.mjs)');
111
114
 
112
115
  for (const adapter of resolveAdapters(ctx.adapters, interactiveSession.adapterTargets)) {
113
116
  adapter.generate();
@@ -144,15 +147,25 @@ export function createCmdUpdate(ctx) {
144
147
  updated = true;
145
148
  }
146
149
 
147
- if (platforms.length > 0 || existsSync(join(ctx.cwd, '.agents', 'skills'))) {
148
- if (isDry) {
149
- console.log(' - would update open-standard skills (.agents/skills/gsdd-*)');
150
- } else {
151
- generateOpenStandardSkills(ctx.cwd, ctx.workflows);
150
+ if (platforms.length > 0 || existsSync(join(ctx.cwd, '.agents', 'skills'))) {
151
+ if (isDry) {
152
+ console.log(' - would update open-standard skills (.agents/skills/gsdd-*)');
153
+ } else {
154
+ generateOpenStandardSkills(ctx.cwd, ctx.workflows);
152
155
  console.log(' - updated open-standard skills (.agents/skills/gsdd-*)');
153
- }
154
- updated = true;
155
- }
156
+ }
157
+ updated = true;
158
+ }
159
+
160
+ if (existsSync(ctx.planningDir)) {
161
+ if (isDry) {
162
+ console.log(' - would update local workflow helper (.planning/bin/gsdd.mjs)');
163
+ } else {
164
+ generatePlanningCliLauncher(ctx);
165
+ console.log(' - updated local workflow helper (.planning/bin/gsdd.mjs)');
166
+ }
167
+ updated = true;
168
+ }
156
169
 
157
170
  for (const adapter of getAdaptersToUpdate(ctx.adapters, platforms)) {
158
171
  if (isDry) {
@@ -167,26 +180,70 @@ export function createCmdUpdate(ctx) {
167
180
 
168
181
  if (!updated) {
169
182
  console.log(' - no adapters found to update (run `gsdd init` first)');
170
- } else if (isDry) {
171
- console.log('\nDry run complete. No files were written.\n');
172
- } else {
173
- if (doTemplates && existsSync(ctx.planningDir)) {
174
- const manifest = buildManifest({ planningDir: ctx.planningDir, frameworkVersion: ctx.frameworkVersion });
175
- writeManifest(ctx.planningDir, manifest);
176
- console.log(' - updated generation manifest');
177
- }
178
- console.log('\nAdapters updated.\n');
179
- }
180
- };
181
- }
182
-
183
- function generateOpenStandardSkills(cwd, workflows) {
184
- for (const workflow of workflows) {
185
- const dir = join(cwd, '.agents', 'skills', workflow.name);
186
- mkdirSync(dir, { recursive: true });
187
- writeFileSync(join(dir, 'SKILL.md'), renderSkillContent(workflow));
188
- }
189
- }
183
+ } else if (isDry) {
184
+ console.log('\nDry run complete. No files were written.\n');
185
+ } else {
186
+ if (existsSync(ctx.planningDir)) {
187
+ const manifest = buildUpdateManifest({
188
+ planningDir: ctx.planningDir,
189
+ frameworkVersion: ctx.frameworkVersion,
190
+ updateTemplates: doTemplates,
191
+ });
192
+ if (manifest) {
193
+ writeManifest(ctx.planningDir, manifest);
194
+ console.log(' - updated generation manifest');
195
+ }
196
+ }
197
+ console.log('\nAdapters updated.\n');
198
+ }
199
+ };
200
+ }
201
+
202
+ function generateOpenStandardSkills(cwd, workflows) {
203
+ for (const workflow of workflows) {
204
+ const dir = join(cwd, '.agents', 'skills', workflow.name);
205
+ mkdirSync(dir, { recursive: true });
206
+ writeFileSync(join(dir, 'SKILL.md'), renderSkillContent(workflow));
207
+ }
208
+ }
209
+
210
+ function generatePlanningCliLauncher(ctx) {
211
+ const dir = join(ctx.planningDir, 'bin');
212
+ mkdirSync(dir, { recursive: true });
213
+ writeFileSync(
214
+ join(dir, 'gsdd.mjs'),
215
+ renderPlanningCliLauncher({
216
+ packageName: ctx.packageName,
217
+ packageVersion: ctx.packageVersion,
218
+ })
219
+ );
220
+ }
221
+
222
+ function buildUpdateManifest({ planningDir, frameworkVersion, updateTemplates }) {
223
+ const existingManifest = readManifest(planningDir);
224
+ const nextManifest = buildManifest({ planningDir, frameworkVersion });
225
+
226
+ if (existingManifest && !updateTemplates) {
227
+ nextManifest.templates = existingManifest.templates ?? nextManifest.templates;
228
+ nextManifest.roles = existingManifest.roles ?? nextManifest.roles;
229
+ }
230
+
231
+ if (existingManifest && manifestsEqualIgnoringTimestamp(existingManifest, nextManifest)) {
232
+ return null;
233
+ }
234
+
235
+ return nextManifest;
236
+ }
237
+
238
+ function manifestsEqualIgnoringTimestamp(left, right) {
239
+ return JSON.stringify(stripManifestTimestamp(left)) === JSON.stringify(stripManifestTimestamp(right));
240
+ }
241
+
242
+ function stripManifestTimestamp(manifest) {
243
+ if (!manifest || typeof manifest !== 'object') return manifest;
244
+ const { generatedAt, ...rest } = manifest;
245
+ return rest;
246
+ }
190
247
 
191
248
  async function ensureConfig({ cwd, planningDir, isAuto, promptApi, preselectedConfig = null }) {
192
249
  const configFile = join(planningDir, 'config.json');
@@ -2,37 +2,37 @@ const RUNTIME_OPTIONS = [
2
2
  {
3
3
  id: 'claude',
4
4
  label: 'Claude Code',
5
- description: 'Directly validated native skills, commands, and agents',
5
+ description: 'Directly validated native skills, commands, and agents with local freshness checks',
6
6
  kind: 'native',
7
7
  },
8
8
  {
9
9
  id: 'opencode',
10
10
  label: 'OpenCode',
11
- description: 'Directly validated native slash commands and agents',
11
+ description: 'Directly validated native slash commands and agents with local freshness checks',
12
12
  kind: 'native',
13
13
  },
14
14
  {
15
15
  id: 'codex',
16
16
  label: 'Codex CLI',
17
- description: 'Directly validated portable skills plus native checker agents',
17
+ description: 'Directly validated portable skills plus native checker agents with local freshness checks',
18
18
  kind: 'native',
19
19
  },
20
20
  {
21
21
  id: 'cursor',
22
22
  label: 'Cursor',
23
- description: 'Qualified support via skills-native slash commands from .agents/skills/',
23
+ description: 'Qualified support via skills-native slash commands from .agents/skills/ with the same local freshness checks',
24
24
  kind: 'skills_native',
25
25
  },
26
26
  {
27
27
  id: 'copilot',
28
28
  label: 'GitHub Copilot',
29
- description: 'Qualified support via skills-native slash commands from .agents/skills/',
29
+ description: 'Qualified support via skills-native slash commands from .agents/skills/ with the same local freshness checks',
30
30
  kind: 'skills_native',
31
31
  },
32
32
  {
33
33
  id: 'gemini',
34
34
  label: 'Gemini CLI',
35
- description: 'Qualified support via skills-native slash commands from .agents/skills/',
35
+ description: 'Qualified support via skills-native slash commands from .agents/skills/ with the same local freshness checks',
36
36
  kind: 'skills_native',
37
37
  },
38
38
  ];
@@ -161,25 +161,31 @@ export function getPostInitRoutingLines(selectedRuntimes) {
161
161
 
162
162
  export function getHelpText() {
163
163
  return `
164
- gsdd - GSD Distilled CLI
165
- Spec-Driven Development for AI coding agents.
164
+ gsdd - Workspine CLI
165
+ Repo-native delivery spine for long-horizon AI-assisted work across coding runtimes.
166
166
 
167
167
  Usage: gsdd <command> [args]
168
168
 
169
- Commands:
170
- init [--tools <platform>] [--auto] [--brief <file>]
171
- Launch guided install wizard in TTYs, or use --tools for manual/headless setup
172
- --auto: non-interactive mode with smart defaults (requires --tools)
173
- --brief <file>: copy project brief to .planning/PROJECT_BRIEF.md
169
+ Commands:
170
+ init [--tools <platform>] [--auto] [--brief <file>]
171
+ Launch guided install wizard in TTYs, or use --tools for manual/headless setup
172
+ --auto: non-interactive mode with smart defaults (requires --tools)
173
+ --brief <file>: copy project brief to .planning/PROJECT_BRIEF.md
174
174
  update [--tools <platform>] [--templates] [--dry]
175
175
  Regenerate adapters from latest framework sources
176
176
  --templates: also refresh .planning/templates/ and roles
177
177
  --dry: preview changes without writing files
178
- health [--json] Check workspace integrity (healthy/degraded/broken)
179
- models [subcommand] Inspect or update model profile / runtime overrides
180
- find-phase [N] Show phase info as JSON (for agent consumption)
181
- verify <N> Run artifact checks for phase N
182
- scaffold phase <N> [name] Create a new phase plan file
178
+ health [--json] Check workspace integrity (healthy/degraded/broken)
179
+ models [subcommand] Inspect or update model profile / runtime overrides
180
+ find-phase [N] Show phase info as JSON (for agent consumption)
181
+ verify <N> Run artifact checks for phase N
182
+ scaffold phase <N> [name] Create a new phase plan file
183
+ file-op <copy|delete|regex-sub>
184
+ Run deterministic workspace-confined file copy/delete/text mutation
185
+ phase-status <N> <status> Update ROADMAP.md phase status ([ ] / [-] / [x])
186
+ lifecycle-preflight <surface> [phase]
187
+ Inspect deterministic lifecycle gate results for a workflow surface
188
+ help Show this summary
183
189
 
184
190
  Platforms (for --tools):
185
191
  claude Generate Claude Code skills (.claude/skills/gsdd-*), commands (.claude/commands/gsdd-*.md), and native agents (.claude/agents/gsdd-*.md)
@@ -192,13 +198,16 @@ Platforms (for --tools):
192
198
  all Generate all adapters (Claude, OpenCode, Codex, AGENTS.md, Cursor, Copilot, Gemini)
193
199
 
194
200
  Notes:
195
- - init always generates open-standard skills at .agents/skills/gsdd-* (portable workflow entrypoints)
201
+ - init always generates open-standard skills at .agents/skills/gsdd-* (portable workflow entrypoints)
202
+ - init also generates .planning/bin/gsdd.mjs so workflow-embedded helper commands do not depend on a global gsdd binary
203
+ - Workspine is the public product name; the retained package, command, workflow, and workspace contracts stay gsdd-cli, gsdd, gsdd-*, and .planning/
196
204
  - running plain \`gsdd init\` in a terminal opens the guided runtime-selection wizard
197
205
  - the wizard lets you pick runtimes first, then separately decide whether repo-wide AGENTS.md governance is worth installing
206
+ - \`gsdd health\` compares any installed generated runtime surfaces against current render output and points back to \`gsdd update\` when they drift
198
207
  - directly validated launch surfaces in this repo are Claude Code, OpenCode, and Codex CLI
199
208
  - Cursor, Copilot, and Gemini are qualified support through the shared .agents/skills/ surface plus optional governance
200
209
  - --tools remains the advanced/manual path and preserves legacy runtime aliases for backward compatibility
201
- - --tools codex generates .codex/agents/gsdd-plan-checker.toml (portable skill is the entry surface)
210
+ - --tools codex generates .codex/agents/gsdd-plan-checker.toml (portable skill is the entry surface; $gsdd-plan is plan-only until explicit $gsdd-execute)
202
211
  - root AGENTS.md is only written on init when explicitly requested via --tools agents, --tools all, or the wizard governance opt-in
203
212
 
204
213
  Examples:
@@ -219,8 +228,20 @@ Examples:
219
228
  npx gsdd-cli verify 1
220
229
  npx gsdd-cli scaffold phase 4 Payments
221
230
 
222
- Workflows (run via skills/adapters generated by init, not direct CLI):
223
- map-codebase Map or refresh codebase (.agents/skills/gsdd-map-codebase/)
224
- audit-milestone Audit a completed milestone (.agents/skills/gsdd-audit-milestone/)
225
- `;
226
- }
231
+ Workflows (run via skills/adapters generated by init, not direct CLI):
232
+ gsdd-new-project Full initializer: questioning, brownfield audit, research, spec, roadmap
233
+ gsdd-map-codebase Map or refresh brownfield codebase context
234
+ gsdd-plan Research, plan, and fresh-context plan check for a phase
235
+ gsdd-execute Execute a phase plan and write phase summaries
236
+ gsdd-verify Verify a completed phase with 3-level checks
237
+ gsdd-verify-work Conversational UAT validation for user-facing behavior
238
+ gsdd-audit-milestone Cross-phase integration, requirements coverage, and E2E audit
239
+ gsdd-complete-milestone Archive a shipped milestone and collapse roadmap state
240
+ gsdd-new-milestone Start the next milestone with goals, requirements, and phases
241
+ gsdd-plan-milestone-gaps Turn milestone-audit gaps into closure phases
242
+ gsdd-quick Bounded brownfield lane for sub-hour work
243
+ gsdd-pause Save session context to checkpoint
244
+ gsdd-resume Restore context and route to the next action
245
+ gsdd-progress Read-only status and routing surface
246
+ `;
247
+ }
package/bin/lib/init.mjs CHANGED
@@ -4,13 +4,9 @@ import { createCmdInit, createCmdUpdate } from './init-flow.mjs';
4
4
  import { createInitPromptApi, promptChoiceList } from './init-prompts.mjs';
5
5
  import { buildRuntimeChoices, detectPlatforms, getHelpText, normalizeRequestedTools } from './init-runtime.mjs';
6
6
 
7
- function cmdHelp() {
8
- console.log(`${getHelpText().trimEnd()}
9
- file-op <copy|delete|regex-sub>
10
- Run deterministic workspace-confined file copy/delete/text mutation
11
- phase-status <N> <status> Update ROADMAP.md phase status ([ ] / [-] / [x])
12
- `);
13
- }
7
+ function cmdHelp() {
8
+ console.log(getHelpText().trimEnd());
9
+ }
14
10
 
15
11
  export {
16
12
  buildRuntimeChoices,