gsdd-cli 0.19.0 → 0.19.2

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.
@@ -1,6 +1,11 @@
1
1
  const EVIDENCE_KINDS = Object.freeze(['code', 'test', 'runtime', 'delivery', 'human']);
2
2
  const DELIVERY_POSTURES = Object.freeze(['repo_only', 'delivery_sensitive']);
3
3
  const CLOSURE_SURFACES = Object.freeze(['verify', 'audit-milestone', 'complete-milestone']);
4
+ const RELEASE_CLAIM_POSTURES = Object.freeze([
5
+ 'repo_closeout',
6
+ 'runtime_validated_closeout',
7
+ 'delivery_supported_closeout',
8
+ ]);
4
9
 
5
10
  const LEGACY_EVIDENCE_ALIASES = Object.freeze({
6
11
  code: 'code',
@@ -22,8 +27,8 @@ const EVIDENCE_MATRIX = Object.freeze({
22
27
  blockedSoloKinds: Object.freeze(['human', 'delivery']),
23
28
  }),
24
29
  delivery_sensitive: Object.freeze({
25
- requiredKinds: Object.freeze(['code', 'runtime']),
26
- recommendedKinds: Object.freeze(['test', 'delivery', 'human']),
30
+ requiredKinds: Object.freeze(['code', 'runtime', 'delivery']),
31
+ recommendedKinds: Object.freeze(['test', 'human']),
27
32
  blockedSoloKinds: Object.freeze(['code', 'human']),
28
33
  }),
29
34
  }),
@@ -53,7 +58,45 @@ const EVIDENCE_MATRIX = Object.freeze({
53
58
  }),
54
59
  });
55
60
 
56
- export { CLOSURE_SURFACES, DELIVERY_POSTURES, EVIDENCE_KINDS };
61
+ const CONTRADICTION_CATEGORIES = Object.freeze([
62
+ 'evidence',
63
+ 'public_surface',
64
+ 'runtime',
65
+ 'delivery',
66
+ 'planning_drift',
67
+ 'generated_surface',
68
+ ]);
69
+
70
+ const CONTRADICTION_STATUSES = Object.freeze(['passed', 'failed', 'not_applicable']);
71
+
72
+ const RELEASE_CLAIM_MATRIX = Object.freeze({
73
+ repo_closeout: Object.freeze({
74
+ deliveryPosture: 'repo_only',
75
+ requiredClaimKinds: Object.freeze([]),
76
+ allowedClaim: 'Repo-local milestone or phase closeout is supported by planning and repository artifacts only.',
77
+ invalidClaim: 'Do not imply runtime validation, delivery, publication, or public support from repo-local closeout alone.',
78
+ }),
79
+ runtime_validated_closeout: Object.freeze({
80
+ deliveryPosture: 'repo_only',
81
+ requiredClaimKinds: Object.freeze(['runtime']),
82
+ allowedClaim: 'Runtime behavior or a runtime surface was directly executed and observed for the named runtime or surface.',
83
+ invalidClaim: 'Do not generalize validation from one runtime or generated surface to another.',
84
+ }),
85
+ delivery_supported_closeout: Object.freeze({
86
+ deliveryPosture: 'delivery_sensitive',
87
+ requiredClaimKinds: Object.freeze([]),
88
+ allowedClaim: 'Externally consumed release, support, install, or delivery claims are supported by the delivery-sensitive evidence bar.',
89
+ invalidClaim: 'Do not imply merge, package, tag, GitHub Release, publication, generated-surface freshness, or public support without matching delivery evidence.',
90
+ }),
91
+ });
92
+
93
+ const CONTRADICTION_BLOCKERS_BY_POSTURE = Object.freeze({
94
+ repo_closeout: Object.freeze(['evidence', 'public_surface', 'planning_drift']),
95
+ runtime_validated_closeout: Object.freeze(['evidence', 'runtime', 'generated_surface', 'planning_drift']),
96
+ delivery_supported_closeout: CONTRADICTION_CATEGORIES,
97
+ });
98
+
99
+ export { CLOSURE_SURFACES, DELIVERY_POSTURES, EVIDENCE_KINDS, RELEASE_CLAIM_POSTURES };
57
100
 
58
101
  export function normalizeEvidenceKind(kind) {
59
102
  if (!kind) {
@@ -78,6 +121,11 @@ export function isClosureSurface(surface) {
78
121
  return CLOSURE_SURFACES.includes(surface);
79
122
  }
80
123
 
124
+ export function normalizeReleaseClaimPosture(posture) {
125
+ if (!posture) return 'repo_closeout';
126
+ return RELEASE_CLAIM_POSTURES.includes(posture) ? posture : null;
127
+ }
128
+
81
129
  export function getEvidenceContract(surface, deliveryPosture) {
82
130
  const matrix = EVIDENCE_MATRIX[surface];
83
131
  if (!matrix) {
@@ -108,5 +156,170 @@ export function describeEvidenceSurface(surface) {
108
156
  surface,
109
157
  supportedKinds: [...EVIDENCE_KINDS],
110
158
  deliveryPostures: DELIVERY_POSTURES.map((deliveryPosture) => getEvidenceContract(surface, deliveryPosture)),
159
+ releaseClaimPostures: RELEASE_CLAIM_POSTURES.map((releaseClaimPosture) => getReleaseClaimContract(surface, releaseClaimPosture)),
160
+ };
161
+ }
162
+
163
+ function uniqueKinds(kinds) {
164
+ return [...new Set(kinds)];
165
+ }
166
+
167
+ function getDowngradePosture(observedKinds) {
168
+ if (observedKinds.includes('runtime')) {
169
+ return 'runtime_validated_closeout';
170
+ }
171
+ return 'repo_closeout';
172
+ }
173
+
174
+ export function getReleaseClaimContract(surface, releaseClaimPosture = 'repo_closeout') {
175
+ const posture = normalizeReleaseClaimPosture(releaseClaimPosture);
176
+ if (!posture) {
177
+ throw new Error(`Unsupported release claim posture: ${releaseClaimPosture}`);
178
+ }
179
+ const claim = RELEASE_CLAIM_MATRIX[posture];
180
+ const evidence = getEvidenceContract(surface, claim.deliveryPosture);
181
+ const requiredKinds = uniqueKinds([...evidence.requiredKinds, ...claim.requiredClaimKinds]);
182
+
183
+ return {
184
+ surface,
185
+ releaseClaimPosture: posture,
186
+ deliveryPosture: claim.deliveryPosture,
187
+ supportedKinds: [...EVIDENCE_KINDS],
188
+ requiredKinds,
189
+ requiredClaimKinds: [...claim.requiredClaimKinds],
190
+ allowedClaim: claim.allowedClaim,
191
+ invalidClaim: claim.invalidClaim,
192
+ waiverRule: 'Waivers may only narrow the release claim posture or defer an unsupported claim; they never satisfy missing required evidence for the stronger claim.',
193
+ deferralRule: 'Deferrals must name the unsupported claim, missing evidence kinds, and later workflow or milestone candidate when known.',
194
+ contradictionCategories: [...CONTRADICTION_CATEGORIES],
195
+ };
196
+ }
197
+
198
+ export function evaluateReleaseClaimPosture({
199
+ surface,
200
+ releaseClaimPosture = 'repo_closeout',
201
+ observedKinds = [],
202
+ waivedKinds = [],
203
+ } = {}) {
204
+ const contract = getReleaseClaimContract(surface, releaseClaimPosture);
205
+ const observed = normalizeEvidenceKinds(observedKinds);
206
+ const waived = normalizeEvidenceKinds(waivedKinds);
207
+ const missingKinds = contract.requiredKinds.filter((kind) => !observed.includes(kind));
208
+ const invalidWaivers = waived.filter((kind) => missingKinds.includes(kind));
209
+ const hasUnsupportedStrongClaim = contract.releaseClaimPosture !== 'repo_closeout' && missingKinds.length > 0;
210
+
211
+ return {
212
+ surface: contract.surface,
213
+ releaseClaimPosture: contract.releaseClaimPosture,
214
+ deliveryPosture: contract.deliveryPosture,
215
+ requiredKinds: [...contract.requiredKinds],
216
+ observedKinds: observed,
217
+ missingKinds,
218
+ invalidWaivers,
219
+ status: missingKinds.length === 0 && invalidWaivers.length === 0 ? 'supported' : 'unsupported',
220
+ disposition: hasUnsupportedStrongClaim ? 'downgrade_or_defer' : missingKinds.length > 0 ? 'block_or_defer' : 'proceed',
221
+ downgradeTo: hasUnsupportedStrongClaim ? getDowngradePosture(observed) : null,
222
+ deferredClaims: hasUnsupportedStrongClaim
223
+ ? [{ claim: contract.releaseClaimPosture, missingKinds }]
224
+ : [],
225
+ };
226
+ }
227
+
228
+ export function evaluateReleaseClaimCloseoutContract({
229
+ surface,
230
+ deliveryPosture = null,
231
+ releaseClaimPosture = 'repo_closeout',
232
+ observedKinds = [],
233
+ waivedKinds = [],
234
+ unsupportedClaims = [],
235
+ deferrals = [],
236
+ contradictionChecks = {},
237
+ } = {}) {
238
+ const posture = evaluateReleaseClaimPosture({
239
+ surface,
240
+ releaseClaimPosture,
241
+ observedKinds,
242
+ waivedKinds,
243
+ });
244
+ const missingContradictionChecks = CONTRADICTION_CATEGORIES.filter((name) => !(name in contradictionChecks));
245
+ const failedContradictionChecks = Object.entries(contradictionChecks)
246
+ .filter(([, status]) => status === 'failed')
247
+ .map(([name]) => name);
248
+ const unknownContradictionChecks = Object.keys(contradictionChecks)
249
+ .filter((name) => !CONTRADICTION_CATEGORIES.includes(name));
250
+ const invalidContradictionChecks = Object.entries(contradictionChecks)
251
+ .filter(([, status]) => !CONTRADICTION_STATUSES.includes(status))
252
+ .map(([name]) => name);
253
+ const blockingContradictionChecks = failedContradictionChecks.filter((name) =>
254
+ CONTRADICTION_BLOCKERS_BY_POSTURE[posture.releaseClaimPosture].includes(name)
255
+ );
256
+ const unresolvedUnsupportedClaims = unsupportedClaims.filter((claim) =>
257
+ !deferrals.some((deferral) => namesUnsupportedClaim(deferral, claim))
258
+ );
259
+ const blockers = [];
260
+
261
+ if (deliveryPosture && deliveryPosture !== posture.deliveryPosture) {
262
+ blockers.push({
263
+ code: 'incompatible_release_claim_posture',
264
+ details: [`${deliveryPosture} cannot support ${posture.releaseClaimPosture}; expected ${posture.deliveryPosture}`],
265
+ });
266
+ }
267
+
268
+ if (posture.missingKinds.length > 0) {
269
+ blockers.push({ code: 'missing_required_release_evidence', details: posture.missingKinds });
270
+ }
271
+ if (posture.invalidWaivers.length > 0) {
272
+ blockers.push({ code: 'invalid_release_waivers', details: posture.invalidWaivers });
273
+ }
274
+ if (unresolvedUnsupportedClaims.length > 0) {
275
+ blockers.push({ code: 'unsupported_release_claims', details: unresolvedUnsupportedClaims });
276
+ }
277
+ if (missingContradictionChecks.length > 0) {
278
+ blockers.push({ code: 'missing_release_contradiction_checks', details: missingContradictionChecks });
279
+ }
280
+ if (unknownContradictionChecks.length > 0) {
281
+ blockers.push({ code: 'unknown_release_contradiction_checks', details: unknownContradictionChecks });
282
+ }
283
+ if (invalidContradictionChecks.length > 0) {
284
+ blockers.push({ code: 'invalid_release_contradiction_checks', details: invalidContradictionChecks });
285
+ }
286
+ if (blockingContradictionChecks.length > 0) {
287
+ blockers.push({ code: 'failed_release_contradiction_checks', details: blockingContradictionChecks });
288
+ }
289
+
290
+ return {
291
+ ...posture,
292
+ unsupportedClaims: [...unsupportedClaims],
293
+ deferrals: [...deferrals],
294
+ failedContradictionChecks: blockingContradictionChecks,
295
+ allFailedContradictionChecks: failedContradictionChecks,
296
+ missingContradictionChecks,
297
+ unknownContradictionChecks,
298
+ invalidContradictionChecks,
299
+ unresolvedUnsupportedClaims,
300
+ blockers,
301
+ status: blockers.length === 0 ? 'supported' : 'unsupported',
111
302
  };
112
303
  }
304
+
305
+ function namesUnsupportedClaim(deferral, claim) {
306
+ const normalizedDeferral = normalizeClaimText(deferral);
307
+ const normalizedClaim = normalizeClaimText(claim);
308
+ if (!normalizedDeferral || !normalizedClaim) return false;
309
+ return normalizedDeferral.includes(normalizedClaim) && isStructuredDeferral(normalizedDeferral);
310
+ }
311
+
312
+ function isStructuredDeferral(normalizedDeferral) {
313
+ const namesEvidenceKind = EVIDENCE_KINDS.some((kind) => normalizedDeferral.includes(kind));
314
+ const namesLaterTarget = /\b(later|next|future|workflow|milestone|phase|gsdd)\b/.test(normalizedDeferral);
315
+ return namesEvidenceKind && namesLaterTarget;
316
+ }
317
+
318
+ function normalizeClaimText(value) {
319
+ return String(value || '')
320
+ .trim()
321
+ .toLowerCase()
322
+ .replace(/[^a-z0-9]+/g, ' ')
323
+ .replace(/\s+/g, ' ')
324
+ .trim();
325
+ }
@@ -56,8 +56,8 @@ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, option
56
56
  warnings.push({
57
57
  id: 'W8',
58
58
  severity: 'WARN',
59
- message: `distilled/README.md workflow inventory is out of sync (${issues.join('; ')})`,
60
- fix: 'Update distilled/README.md workflow status table and framework file tree to match distilled/workflows/',
59
+ message: `distilled/README.md workflow surface/status inventory is out of sync (${issues.join('; ')})`,
60
+ fix: 'Update distilled/README.md workflow inventory table and framework file tree to match distilled/workflows/',
61
61
  });
62
62
  }
63
63
  }
@@ -94,7 +94,7 @@ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, option
94
94
  warnings.push({
95
95
  id: 'W11',
96
96
  severity: 'WARN',
97
- message: `Installed generated runtime and workflow-helper surfaces drift from current render output (${summarizeRuntimeFreshnessIssues(options.runtimeFreshnessReport)})`,
97
+ message: `Renderer-backed generated runtime and workflow-helper surfaces drift from current render output (${summarizeRuntimeFreshnessIssues(options.runtimeFreshnessReport)})`,
98
98
  fix: getRuntimeFreshnessRepairGuidance(options.runtimeFreshnessReport),
99
99
  });
100
100
  }
@@ -105,8 +105,8 @@ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, option
105
105
  id: 'W12',
106
106
  severity: 'WARN',
107
107
  message: `Planning state drifted since last recorded session (${drift.details.join('; ')})`,
108
- fix: 'Review the changes, then run a lifecycle workflow to update the fingerprint',
109
- });
108
+ fix: 'Review the changed planning files. If the drift is intentional, rebaseline with `node .planning/bin/gsdd.mjs session-fingerprint write`, then rerun the blocked lifecycle preflight.',
109
+ });
110
110
  }
111
111
 
112
112
  return warnings;
@@ -119,19 +119,34 @@ function extractHealthTableIds(content) {
119
119
  }
120
120
 
121
121
  function extractReadmeStatusEntries(content) {
122
- const section = extractSection(content, '## Current Status', 'Architecture notes:');
122
+ const workflowSurface = extractSection(content, '## Workflow Surface', 'Architecture notes:');
123
+ const currentStatus = extractSection(content, '## Current Status', 'Architecture notes:');
124
+ const section = workflowSurface || currentStatus;
123
125
  if (!section) return [];
124
126
  return [...normalizeContent(section).matchAll(/\|\s*`([^`]+\.md)`\s*\|/g)].map((result) => result[1]);
125
127
  }
126
128
 
127
129
  function extractReadmeWorkflowTreeEntries(content) {
128
130
  const section = extractSection(content, '## Files In This Framework', '## ');
129
- const match = normalizeContent(section || '').match(/```[\s\S]*?workflows\/\n([\s\S]*?)\n\s*templates\//);
131
+ const match = normalizeContent(section || '').match(/```[^\n]*\n([\s\S]*?)\n```/);
130
132
  if (!match) return [];
131
- return match[1]
132
- .split('\n')
133
- .map((line) => line.trim())
134
- .filter((line) => line.endsWith('.md'));
133
+ const entries = [];
134
+ let inWorkflows = false;
135
+ for (const rawLine of match[1].split('\n')) {
136
+ const line = rawLine.trim();
137
+ if (!line) continue;
138
+ if (!inWorkflows) {
139
+ if (/\bworkflows\/$/.test(line)) inWorkflows = true;
140
+ continue;
141
+ }
142
+ if (/\.md\b/.test(line)) {
143
+ const file = line.match(/([^\s/`|├└─]+\.md)\b/);
144
+ if (file) entries.push(file[1]);
145
+ continue;
146
+ }
147
+ if (/\b[^\s/]+\/$/.test(line)) break;
148
+ }
149
+ return entries;
135
150
  }
136
151
 
137
152
  function extractRepoLocalPaths(content) {
@@ -164,10 +164,10 @@ export function createCmdHealth(ctx) {
164
164
  if (!cat.hashes) continue;
165
165
  const result = detectModifications(cat.dir, cat.hashes);
166
166
  if (result.modified.length > 0) {
167
- 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` });
167
+ warnings.push({ id: 'W2', severity: 'WARN', message: `${cat.name}: ${result.modified.length} manifest-tracked installed file(s) modified locally (${result.modified.join(', ')})`, fix: `Intentional? Run \`${cat.fixCommand}\` to reset` });
168
168
  }
169
169
  if (result.missing.length > 0) {
170
- 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` });
170
+ warnings.push({ id: 'W3', severity: 'WARN', message: `${cat.name}: ${result.missing.length} manifest-tracked installed file(s) missing from disk (${result.missing.join(', ')})`, fix: `Run \`${cat.fixCommand}\` to restore` });
171
171
  }
172
172
  }
173
173
  }
@@ -179,7 +179,7 @@ export function createCmdHealth(ctx) {
179
179
  const lifecycle = evaluateLifecycleState({ planningDir });
180
180
 
181
181
  if (roadmap && existsSync(phasesDir)) {
182
- for (const phase of lifecycle.phases.filter((entry) => entry.status !== 'not_started' && !entry.hasArtifacts)) {
182
+ for (const phase of lifecycle.phases.filter((entry) => entry.status !== 'not_started' && !entry.hasLifecycleArtifacts)) {
183
183
  warnings.push({
184
184
  id: 'W4',
185
185
  severity: 'WARN',
@@ -201,16 +201,9 @@ export function createCmdHealth(ctx) {
201
201
  }
202
202
  }
203
203
 
204
- // W6: No adapter surfaces detected
205
- const adapterPaths = [
206
- join(cwd, '.agents', 'skills'),
207
- join(cwd, '.claude'),
208
- join(cwd, '.opencode'),
209
- join(cwd, '.codex'),
210
- ];
211
- const hasAnyAdapter = adapterPaths.some((p) => existsSync(p));
212
- if (!hasAnyAdapter) {
213
- warnings.push({ id: 'W6', severity: 'WARN', message: 'No adapter surfaces detected', fix: 'Run `npx -y gsdd-cli init --tools <platform>`' });
204
+ // W6: No generated workflow adapter surfaces detected
205
+ if (!hasAnyGeneratedWorkflowSurface(cwd)) {
206
+ warnings.push({ id: 'W6', severity: 'WARN', message: 'No generated workflow adapter surfaces detected', fix: 'Run `npx -y gsdd-cli init --tools <platform>`' });
214
207
  }
215
208
 
216
209
  const runtimeFreshnessReport = configOk && Array.isArray(ctx.workflows)
@@ -245,15 +238,15 @@ export function createCmdHealth(ctx) {
245
238
  });
246
239
  }
247
240
 
248
- // I3: Which adapters are installed
249
- const installedAdapters = [];
250
- if (existsSync(join(cwd, '.agents', 'skills'))) installedAdapters.push('open-standard-skills');
251
- if (existsSync(join(cwd, '.claude'))) installedAdapters.push('claude');
252
- if (existsSync(join(cwd, '.opencode'))) installedAdapters.push('opencode');
253
- if (existsSync(join(cwd, '.codex'))) installedAdapters.push('codex');
254
- if (existsSync(join(cwd, 'AGENTS.md'))) installedAdapters.push('agents');
255
- if (installedAdapters.length > 0) {
256
- info.push({ id: 'I3', severity: 'INFO', message: `Adapters installed: ${installedAdapters.join(', ')}` });
241
+ // I3: Which runtime/governance surfaces are installed
242
+ const installedSurfaces = [];
243
+ if (hasGeneratedSkillSurface(cwd)) installedSurfaces.push('open-standard-skills');
244
+ if (hasGeneratedClaudeSurface(cwd)) installedSurfaces.push('claude');
245
+ if (hasGeneratedOpenCodeSurface(cwd)) installedSurfaces.push('opencode');
246
+ if (hasGeneratedCodexSurface(cwd)) installedSurfaces.push('codex');
247
+ if (existsSync(join(cwd, 'AGENTS.md'))) installedSurfaces.push('root AGENTS.md governance-only');
248
+ if (installedSurfaces.length > 0) {
249
+ info.push({ id: 'I3', severity: 'INFO', message: `Installed runtime/governance surfaces: ${installedSurfaces.join(', ')}` });
257
250
  }
258
251
 
259
252
  // --- Verdict ---
@@ -281,6 +274,78 @@ export function createCmdHealth(ctx) {
281
274
  };
282
275
  }
283
276
 
277
+ function hasAnyGeneratedWorkflowSurface(cwd) {
278
+ return hasGeneratedSkillSurface(cwd)
279
+ || hasGeneratedClaudeEntrySurface(cwd)
280
+ || hasGeneratedOpenCodeEntrySurface(cwd);
281
+ }
282
+
283
+ function hasGeneratedSkillSurface(cwd) {
284
+ return hasGeneratedSkillDirectory(join(cwd, '.agents', 'skills'));
285
+ }
286
+
287
+ function hasGeneratedClaudeSurface(cwd) {
288
+ return hasGeneratedClaudeEntrySurface(cwd)
289
+ || hasGeneratedMarkdownFile(join(cwd, '.claude', 'agents'));
290
+ }
291
+
292
+ function hasGeneratedClaudeEntrySurface(cwd) {
293
+ return hasGeneratedSkillDirectory(join(cwd, '.claude', 'skills'))
294
+ || hasGeneratedMarkdownFile(join(cwd, '.claude', 'commands'));
295
+ }
296
+
297
+ function hasGeneratedOpenCodeSurface(cwd) {
298
+ return hasGeneratedOpenCodeEntrySurface(cwd)
299
+ || hasGeneratedMarkdownFile(join(cwd, '.opencode', 'agents'));
300
+ }
301
+
302
+ function hasGeneratedOpenCodeEntrySurface(cwd) {
303
+ return hasGeneratedMarkdownFile(join(cwd, '.opencode', 'commands'))
304
+ }
305
+
306
+ function hasGeneratedCodexSurface(cwd) {
307
+ return hasGeneratedTomlFile(join(cwd, '.codex', 'agents'));
308
+ }
309
+
310
+ function hasGeneratedSkillDirectory(dir) {
311
+ try {
312
+ return readdirSync(dir, { withFileTypes: true }).some((entry) => {
313
+ return entry.isDirectory()
314
+ && entry.name.startsWith('gsdd-')
315
+ && existsSync(join(dir, entry.name, 'SKILL.md'));
316
+ });
317
+ } catch {
318
+ return false;
319
+ }
320
+ }
321
+
322
+ function hasGeneratedMarkdownFile(dir) {
323
+ try {
324
+ return readdirSync(dir, { withFileTypes: true }).some((entry) => {
325
+ return entry.isFile() && entry.name.startsWith('gsdd-') && entry.name.endsWith('.md');
326
+ });
327
+ } catch {
328
+ return false;
329
+ }
330
+ }
331
+
332
+ function hasGeneratedTomlFile(dir) {
333
+ try {
334
+ return readdirSync(dir, { withFileTypes: true }).some((entry) => {
335
+ return entry.isFile() && entry.name.startsWith('gsdd-') && entry.name.endsWith('.toml');
336
+ });
337
+ } catch {
338
+ return false;
339
+ }
340
+ }
341
+
284
342
  function isFrameworkSourceRepo(cwd) {
285
- return existsSync(join(cwd, 'distilled', 'templates')) && existsSync(join(cwd, 'distilled', 'workflows'));
343
+ if (!existsSync(join(cwd, 'distilled', 'templates')) || !existsSync(join(cwd, 'distilled', 'workflows'))) return false;
344
+ if (!existsSync(join(cwd, 'bin', 'gsdd.mjs')) || !existsSync(join(cwd, 'package.json'))) return false;
345
+ try {
346
+ const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
347
+ return pkg.name === 'gsdd-cli';
348
+ } catch {
349
+ return false;
350
+ }
286
351
  }
@@ -186,6 +186,7 @@ Commands:
186
186
  phase-status <N> <status> Update ROADMAP.md phase status ([ ] / [-] / [x])
187
187
  lifecycle-preflight <surface> [phase]
188
188
  Inspect deterministic lifecycle gate results for a workflow surface
189
+ session-fingerprint write Rebaseline planning-state drift after reviewing changed planning files
189
190
  help Show this summary
190
191
 
191
192
  Platforms (for --tools):
@@ -254,6 +255,7 @@ Starting lanes after init:
254
255
 
255
256
  Advanced/internal helpers (kept available, but not the primary first-run user story):
256
257
  lifecycle-preflight Inspect deterministic lifecycle gate results for a workflow surface
258
+ session-fingerprint Rebaseline the local planning-state fingerprint after review
257
259
  phase-status Update ROADMAP.md phase status through the local helper surface
258
260
  file-op Deterministic workspace-confined file copy/delete/text mutation
259
261
  `;