gsdd-cli 0.18.5 → 0.19.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 (101) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +610 -608
  3. package/agents/DISTILLATION.md +421 -421
  4. package/agents/README.md +62 -62
  5. package/agents/approach-explorer.md +370 -361
  6. package/agents/debugger.md +82 -82
  7. package/agents/executor.md +473 -394
  8. package/agents/integration-checker.md +318 -318
  9. package/agents/mapper.md +103 -103
  10. package/agents/planner.md +342 -313
  11. package/agents/researcher.md +84 -84
  12. package/agents/roadmapper.md +296 -296
  13. package/agents/synthesizer.md +236 -236
  14. package/agents/verifier.md +337 -337
  15. package/bin/adapters/agents.mjs +34 -34
  16. package/bin/adapters/claude.mjs +193 -191
  17. package/bin/adapters/codex.mjs +85 -85
  18. package/bin/adapters/index.mjs +20 -20
  19. package/bin/adapters/opencode.mjs +280 -278
  20. package/bin/gsdd.mjs +123 -116
  21. package/bin/lib/cli-utils.mjs +28 -28
  22. package/bin/lib/evidence-contract.mjs +325 -112
  23. package/bin/lib/file-ops.mjs +186 -144
  24. package/bin/lib/health-truth.mjs +196 -178
  25. package/bin/lib/health.mjs +246 -226
  26. package/bin/lib/init-flow.mjs +247 -231
  27. package/bin/lib/init-prompts.mjs +248 -247
  28. package/bin/lib/init-runtime.mjs +193 -190
  29. package/bin/lib/init.mjs +17 -17
  30. package/bin/lib/lifecycle-preflight.mjs +760 -326
  31. package/bin/lib/lifecycle-state.mjs +356 -267
  32. package/bin/lib/manifest.mjs +116 -114
  33. package/bin/lib/models.mjs +411 -411
  34. package/bin/lib/phase.mjs +365 -358
  35. package/bin/lib/plan-constants.mjs +35 -30
  36. package/bin/lib/provenance.mjs +109 -106
  37. package/bin/lib/rendering.mjs +119 -83
  38. package/bin/lib/runtime-freshness.mjs +214 -214
  39. package/bin/lib/session-fingerprint.mjs +91 -14
  40. package/bin/lib/templates.mjs +225 -224
  41. package/bin/lib/workspace-root.mjs +2 -1
  42. package/distilled/DESIGN.md +2461 -2323
  43. package/distilled/EVIDENCE-INDEX.md +418 -392
  44. package/distilled/README.md +196 -193
  45. package/distilled/SKILL.md +86 -85
  46. package/distilled/templates/agents.block.md +21 -21
  47. package/distilled/templates/agents.md +6 -6
  48. package/distilled/templates/approach.md +272 -232
  49. package/distilled/templates/auth-matrix.md +78 -78
  50. package/distilled/templates/brownfield-change/CHANGE.md +99 -0
  51. package/distilled/templates/brownfield-change/HANDOFF.md +38 -0
  52. package/distilled/templates/brownfield-change/VERIFICATION.md +56 -0
  53. package/distilled/templates/codebase/architecture.md +110 -110
  54. package/distilled/templates/codebase/concerns.md +95 -95
  55. package/distilled/templates/codebase/conventions.md +193 -193
  56. package/distilled/templates/codebase/stack.md +96 -96
  57. package/distilled/templates/delegates/approach-explorer.md +28 -25
  58. package/distilled/templates/delegates/mapper-arch.md +26 -26
  59. package/distilled/templates/delegates/mapper-concerns.md +27 -27
  60. package/distilled/templates/delegates/mapper-quality.md +28 -28
  61. package/distilled/templates/delegates/mapper-tech.md +25 -25
  62. package/distilled/templates/delegates/plan-checker.md +78 -68
  63. package/distilled/templates/delegates/researcher-architecture.md +30 -30
  64. package/distilled/templates/delegates/researcher-features.md +30 -30
  65. package/distilled/templates/delegates/researcher-pitfalls.md +30 -30
  66. package/distilled/templates/delegates/researcher-stack.md +30 -30
  67. package/distilled/templates/delegates/researcher-synthesizer.md +31 -31
  68. package/distilled/templates/research/architecture.md +57 -57
  69. package/distilled/templates/research/features.md +23 -23
  70. package/distilled/templates/research/pitfalls.md +46 -46
  71. package/distilled/templates/research/stack.md +45 -45
  72. package/distilled/templates/research/summary.md +67 -67
  73. package/distilled/templates/roadmap.md +74 -62
  74. package/distilled/templates/spec.md +110 -110
  75. package/distilled/workflows/audit-milestone.md +303 -271
  76. package/distilled/workflows/complete-milestone.md +349 -332
  77. package/distilled/workflows/execute.md +457 -450
  78. package/distilled/workflows/map-codebase.md +253 -253
  79. package/distilled/workflows/new-milestone.md +242 -238
  80. package/distilled/workflows/new-project.md +398 -398
  81. package/distilled/workflows/pause.md +160 -156
  82. package/distilled/workflows/plan-milestone-gaps.md +183 -183
  83. package/distilled/workflows/plan.md +454 -448
  84. package/distilled/workflows/progress.md +227 -223
  85. package/distilled/workflows/quick.md +351 -347
  86. package/distilled/workflows/resume.md +220 -212
  87. package/distilled/workflows/verify-work.md +260 -260
  88. package/distilled/workflows/verify.md +431 -429
  89. package/docs/BROWNFIELD-PROOF.md +95 -95
  90. package/docs/RUNTIME-SUPPORT.md +80 -69
  91. package/docs/USER-GUIDE.md +394 -386
  92. package/docs/VERIFICATION-DISCIPLINE.md +59 -59
  93. package/docs/claude/context-monitor.md +98 -98
  94. package/docs/proof/consumer-node-cli/README.md +37 -37
  95. package/docs/proof/consumer-node-cli/ROADMAP.md +14 -14
  96. package/docs/proof/consumer-node-cli/SPEC.md +17 -17
  97. package/docs/proof/consumer-node-cli/brief.md +9 -9
  98. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -34
  99. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -10
  100. package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -30
  101. package/package.json +62 -61
@@ -8,22 +8,22 @@ import {
8
8
  renderOpenCodeCommandContent,
9
9
  renderSkillContent,
10
10
  } from './rendering.mjs';
11
- import {
12
- CLAUDE_MODEL_PROFILES,
13
- renderClaudeApproachExplorer,
14
- renderClaudePlanChecker,
15
- renderClaudePlanCommand,
16
- renderClaudePlanSkill,
17
- } from '../adapters/claude.mjs';
18
- import {
19
- renderOpenCodeApproachExplorer,
20
- renderOpenCodePlanChecker,
21
- renderOpenCodePlanCommand,
22
- } from '../adapters/opencode.mjs';
23
- import {
24
- renderCodexApproachExplorer,
25
- renderCodexPlanChecker,
26
- } from '../adapters/codex.mjs';
11
+ import {
12
+ CLAUDE_MODEL_PROFILES,
13
+ renderClaudeApproachExplorer,
14
+ renderClaudePlanChecker,
15
+ renderClaudePlanCommand,
16
+ renderClaudePlanSkill,
17
+ } from '../adapters/claude.mjs';
18
+ import {
19
+ renderOpenCodeApproachExplorer,
20
+ renderOpenCodePlanChecker,
21
+ renderOpenCodePlanCommand,
22
+ } from '../adapters/opencode.mjs';
23
+ import {
24
+ renderCodexApproachExplorer,
25
+ renderCodexPlanChecker,
26
+ } from '../adapters/codex.mjs';
27
27
  import {
28
28
  getRuntimeModelOverride,
29
29
  loadProjectModelConfig,
@@ -33,121 +33,121 @@ import {
33
33
  const __filename = fileURLToPath(import.meta.url);
34
34
  const __dirname = dirname(__filename);
35
35
  const PACKAGE_JSON = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
36
-
37
- function normalizeContent(content) {
38
- return String(content).replace(/\r\n/g, '\n');
39
- }
40
-
41
- function compareGeneratedFile({ cwd, runtime, relativePath, expectedContent, repairCommand }) {
42
- const absolutePath = join(cwd, relativePath);
43
- if (!existsSync(absolutePath)) {
44
- return {
45
- runtime,
46
- relativePath,
47
- status: 'missing',
48
- repairCommand,
49
- };
50
- }
51
-
52
- const actualContent = normalizeContent(readFileSync(absolutePath, 'utf-8'));
53
- const expected = normalizeContent(expectedContent);
54
- if (actualContent === expected) {
55
- return {
56
- runtime,
57
- relativePath,
58
- status: 'clean',
59
- repairCommand,
60
- };
61
- }
62
-
63
- return {
64
- runtime,
65
- relativePath,
66
- status: 'stale',
67
- repairCommand,
68
- };
69
- }
70
-
71
- function buildClaudeEntries({ cwd, workflows }) {
72
- const checkerModelAlias = resolveRuntimeAgentModel({
73
- cwd,
74
- runtime: 'claude',
75
- agentId: 'plan-checker',
76
- profileMap: CLAUDE_MODEL_PROFILES,
77
- });
78
- const explorerModelAlias = resolveRuntimeAgentModel({
79
- cwd,
80
- runtime: 'claude',
81
- agentId: 'approach-explorer',
82
- profileMap: CLAUDE_MODEL_PROFILES,
83
- });
84
-
85
- const entries = workflows.map((workflow) => ({
86
- relativePath: `.claude/skills/${workflow.name}/SKILL.md`,
87
- expectedContent: workflow.name === 'gsdd-plan'
88
- ? renderClaudePlanSkill()
89
- : renderSkillContent(workflow),
90
- }));
91
-
92
- entries.push(
93
- {
94
- relativePath: '.claude/commands/gsdd-plan.md',
95
- expectedContent: renderClaudePlanCommand(),
96
- },
97
- {
98
- relativePath: '.claude/agents/gsdd-plan-checker.md',
99
- expectedContent: renderClaudePlanChecker(getDelegateContent('plan-checker.md'), checkerModelAlias),
100
- },
101
- {
102
- relativePath: '.claude/agents/gsdd-approach-explorer.md',
103
- expectedContent: renderClaudeApproachExplorer(getDelegateContent('approach-explorer.md'), explorerModelAlias),
104
- }
105
- );
106
-
107
- return entries;
108
- }
109
-
110
- function buildOpenCodeEntries({ cwd, workflows }) {
111
- const config = loadProjectModelConfig(cwd);
112
- const checkerModelId = getRuntimeModelOverride(config, 'opencode', 'plan-checker');
113
- const explorerModelId = getRuntimeModelOverride(config, 'opencode', 'approach-explorer');
114
-
115
- const entries = workflows.map((workflow) => ({
116
- relativePath: `.opencode/commands/${workflow.name}.md`,
117
- expectedContent: workflow.name === 'gsdd-plan'
118
- ? renderOpenCodePlanCommand()
119
- : renderOpenCodeCommandContent(workflow),
120
- }));
121
-
122
- entries.push(
123
- {
124
- relativePath: '.opencode/agents/gsdd-plan-checker.md',
125
- expectedContent: renderOpenCodePlanChecker(getDelegateContent('plan-checker.md'), checkerModelId),
126
- },
127
- {
128
- relativePath: '.opencode/agents/gsdd-approach-explorer.md',
129
- expectedContent: renderOpenCodeApproachExplorer(getDelegateContent('approach-explorer.md'), explorerModelId),
130
- }
131
- );
132
-
133
- return entries;
134
- }
135
-
36
+
37
+ function normalizeContent(content) {
38
+ return String(content).replace(/\r\n/g, '\n');
39
+ }
40
+
41
+ function compareGeneratedFile({ cwd, runtime, relativePath, expectedContent, repairCommand }) {
42
+ const absolutePath = join(cwd, relativePath);
43
+ if (!existsSync(absolutePath)) {
44
+ return {
45
+ runtime,
46
+ relativePath,
47
+ status: 'missing',
48
+ repairCommand,
49
+ };
50
+ }
51
+
52
+ const actualContent = normalizeContent(readFileSync(absolutePath, 'utf-8'));
53
+ const expected = normalizeContent(expectedContent);
54
+ if (actualContent === expected) {
55
+ return {
56
+ runtime,
57
+ relativePath,
58
+ status: 'clean',
59
+ repairCommand,
60
+ };
61
+ }
62
+
63
+ return {
64
+ runtime,
65
+ relativePath,
66
+ status: 'stale',
67
+ repairCommand,
68
+ };
69
+ }
70
+
71
+ function buildClaudeEntries({ cwd, workflows }) {
72
+ const checkerModelAlias = resolveRuntimeAgentModel({
73
+ cwd,
74
+ runtime: 'claude',
75
+ agentId: 'plan-checker',
76
+ profileMap: CLAUDE_MODEL_PROFILES,
77
+ });
78
+ const explorerModelAlias = resolveRuntimeAgentModel({
79
+ cwd,
80
+ runtime: 'claude',
81
+ agentId: 'approach-explorer',
82
+ profileMap: CLAUDE_MODEL_PROFILES,
83
+ });
84
+
85
+ const entries = workflows.map((workflow) => ({
86
+ relativePath: `.claude/skills/${workflow.name}/SKILL.md`,
87
+ expectedContent: workflow.name === 'gsdd-plan'
88
+ ? renderClaudePlanSkill()
89
+ : renderSkillContent(workflow),
90
+ }));
91
+
92
+ entries.push(
93
+ {
94
+ relativePath: '.claude/commands/gsdd-plan.md',
95
+ expectedContent: renderClaudePlanCommand(),
96
+ },
97
+ {
98
+ relativePath: '.claude/agents/gsdd-plan-checker.md',
99
+ expectedContent: renderClaudePlanChecker(getDelegateContent('plan-checker.md'), checkerModelAlias),
100
+ },
101
+ {
102
+ relativePath: '.claude/agents/gsdd-approach-explorer.md',
103
+ expectedContent: renderClaudeApproachExplorer(getDelegateContent('approach-explorer.md'), explorerModelAlias),
104
+ }
105
+ );
106
+
107
+ return entries;
108
+ }
109
+
110
+ function buildOpenCodeEntries({ cwd, workflows }) {
111
+ const config = loadProjectModelConfig(cwd);
112
+ const checkerModelId = getRuntimeModelOverride(config, 'opencode', 'plan-checker');
113
+ const explorerModelId = getRuntimeModelOverride(config, 'opencode', 'approach-explorer');
114
+
115
+ const entries = workflows.map((workflow) => ({
116
+ relativePath: `.opencode/commands/${workflow.name}.md`,
117
+ expectedContent: workflow.name === 'gsdd-plan'
118
+ ? renderOpenCodePlanCommand()
119
+ : renderOpenCodeCommandContent(workflow),
120
+ }));
121
+
122
+ entries.push(
123
+ {
124
+ relativePath: '.opencode/agents/gsdd-plan-checker.md',
125
+ expectedContent: renderOpenCodePlanChecker(getDelegateContent('plan-checker.md'), checkerModelId),
126
+ },
127
+ {
128
+ relativePath: '.opencode/agents/gsdd-approach-explorer.md',
129
+ expectedContent: renderOpenCodeApproachExplorer(getDelegateContent('approach-explorer.md'), explorerModelId),
130
+ }
131
+ );
132
+
133
+ return entries;
134
+ }
135
+
136
136
  function buildCodexEntries({ cwd }) {
137
- const config = loadProjectModelConfig(cwd);
138
- const checkerModelId = getRuntimeModelOverride(config, 'codex', 'plan-checker');
139
- const explorerModelId = getRuntimeModelOverride(config, 'codex', 'approach-explorer');
140
-
141
- return [
142
- {
143
- relativePath: '.codex/agents/gsdd-plan-checker.toml',
144
- expectedContent: renderCodexPlanChecker(getDelegateContent('plan-checker.md'), checkerModelId),
145
- },
146
- {
147
- relativePath: '.codex/agents/gsdd-approach-explorer.toml',
148
- expectedContent: renderCodexApproachExplorer(getDelegateContent('approach-explorer.md'), explorerModelId),
149
- },
150
- ];
137
+ const config = loadProjectModelConfig(cwd);
138
+ const checkerModelId = getRuntimeModelOverride(config, 'codex', 'plan-checker');
139
+ const explorerModelId = getRuntimeModelOverride(config, 'codex', 'approach-explorer');
140
+
141
+ return [
142
+ {
143
+ relativePath: '.codex/agents/gsdd-plan-checker.toml',
144
+ expectedContent: renderCodexPlanChecker(getDelegateContent('plan-checker.md'), checkerModelId),
145
+ },
146
+ {
147
+ relativePath: '.codex/agents/gsdd-approach-explorer.toml',
148
+ expectedContent: renderCodexApproachExplorer(getDelegateContent('approach-explorer.md'), explorerModelId),
149
+ },
150
+ ];
151
151
  }
152
152
 
153
153
  function buildWorkspaceHelperEntries() {
@@ -166,43 +166,43 @@ export function collectExpectedRuntimeSurfaceGroups({ cwd = process.cwd(), workf
166
166
  runtime: 'workspace-helper',
167
167
  label: 'workspace workflow helper',
168
168
  root: '.planning/bin',
169
- repairCommand: 'gsdd update',
169
+ repairCommand: 'npx -y gsdd-cli update',
170
170
  entries: buildWorkspaceHelperEntries(),
171
171
  },
172
172
  {
173
173
  runtime: 'portable',
174
174
  label: 'portable skills',
175
- root: '.agents/skills',
176
- repairCommand: 'gsdd update',
177
- entries: buildPortableSkillEntries(workflows).map((entry) => ({
178
- relativePath: entry.relativePath,
179
- expectedContent: entry.content,
180
- })),
181
- },
182
- {
183
- runtime: 'claude',
184
- label: 'Claude Code native surfaces',
185
- root: '.claude',
186
- repairCommand: 'gsdd update --tools claude',
187
- entries: buildClaudeEntries({ cwd, workflows }),
188
- },
189
- {
190
- runtime: 'opencode',
191
- label: 'OpenCode native surfaces',
192
- root: '.opencode',
193
- repairCommand: 'gsdd update --tools opencode',
194
- entries: buildOpenCodeEntries({ cwd, workflows }),
195
- },
196
- {
197
- runtime: 'codex',
198
- label: 'Codex CLI native agents',
199
- root: '.codex',
200
- repairCommand: 'gsdd update --tools codex',
201
- entries: buildCodexEntries({ cwd }),
202
- },
203
- ];
204
- }
205
-
175
+ root: '.agents/skills',
176
+ repairCommand: 'npx -y gsdd-cli update',
177
+ entries: buildPortableSkillEntries(workflows).map((entry) => ({
178
+ relativePath: entry.relativePath,
179
+ expectedContent: entry.content,
180
+ })),
181
+ },
182
+ {
183
+ runtime: 'claude',
184
+ label: 'Claude Code native surfaces',
185
+ root: '.claude',
186
+ repairCommand: 'npx -y gsdd-cli update --tools claude',
187
+ entries: buildClaudeEntries({ cwd, workflows }),
188
+ },
189
+ {
190
+ runtime: 'opencode',
191
+ label: 'OpenCode native surfaces',
192
+ root: '.opencode',
193
+ repairCommand: 'npx -y gsdd-cli update --tools opencode',
194
+ entries: buildOpenCodeEntries({ cwd, workflows }),
195
+ },
196
+ {
197
+ runtime: 'codex',
198
+ label: 'Codex CLI native agents',
199
+ root: '.codex',
200
+ repairCommand: 'npx -y gsdd-cli update --tools codex',
201
+ entries: buildCodexEntries({ cwd }),
202
+ },
203
+ ];
204
+ }
205
+
206
206
  export function evaluateRuntimeFreshness({ cwd = process.cwd(), workflows = [] }) {
207
207
  const groups = collectExpectedRuntimeSurfaceGroups({ cwd, workflows }).map((group) => {
208
208
  const installed = group.runtime === 'workspace-helper'
@@ -210,55 +210,55 @@ export function evaluateRuntimeFreshness({ cwd = process.cwd(), workflows = [] }
210
210
  : existsSync(join(cwd, group.root));
211
211
  const comparisons = installed
212
212
  ? group.entries.map((entry) => compareGeneratedFile({
213
- cwd,
214
- runtime: group.runtime,
215
- relativePath: entry.relativePath,
216
- expectedContent: entry.expectedContent,
217
- repairCommand: group.repairCommand,
218
- }))
219
- : [];
220
-
221
- const stale = comparisons.filter((entry) => entry.status === 'stale');
222
- const missing = comparisons.filter((entry) => entry.status === 'missing');
223
-
224
- return {
225
- ...group,
226
- installed,
227
- comparisons,
228
- stale,
229
- missing,
230
- issueCount: stale.length + missing.length,
231
- };
232
- });
233
-
234
- const checkedGroups = groups.filter((group) => group.installed);
235
- const issues = checkedGroups.flatMap((group) => group.comparisons.filter((entry) => entry.status !== 'clean'));
236
-
237
- return {
238
- groups,
239
- checkedGroups: checkedGroups.map((group) => group.runtime),
240
- hasInstalledRuntimeSurfaces: checkedGroups.length > 0,
241
- issueCount: issues.length,
242
- staleCount: issues.filter((entry) => entry.status === 'stale').length,
243
- missingCount: issues.filter((entry) => entry.status === 'missing').length,
244
- issues,
245
- };
246
- }
247
-
248
- export function summarizeRuntimeFreshnessIssues(report, limit = 4) {
249
- if (!report || report.issueCount === 0) return '';
250
- const listed = report.issues
251
- .slice(0, limit)
252
- .map((entry) => `${entry.relativePath} [${entry.status}]`);
253
- const remainder = report.issueCount - listed.length;
254
- return remainder > 0 ? `${listed.join(', ')} (+${remainder} more)` : listed.join(', ');
255
- }
256
-
257
- export function getRuntimeFreshnessRepairGuidance(report) {
258
- if (!report || report.issueCount === 0) return 'Run `gsdd update` to regenerate installed runtime surfaces.';
259
- const commands = [...new Set(report.issues.map((entry) => entry.repairCommand))];
260
- if (commands.length === 1) {
261
- return `Run \`${commands[0]}\` to regenerate the installed runtime surfaces.`;
262
- }
263
- return `Run \`gsdd update\` to regenerate all installed runtime surfaces, or target the affected adapters individually: ${commands.map((command) => `\`${command}\``).join(', ')}.`;
264
- }
213
+ cwd,
214
+ runtime: group.runtime,
215
+ relativePath: entry.relativePath,
216
+ expectedContent: entry.expectedContent,
217
+ repairCommand: group.repairCommand,
218
+ }))
219
+ : [];
220
+
221
+ const stale = comparisons.filter((entry) => entry.status === 'stale');
222
+ const missing = comparisons.filter((entry) => entry.status === 'missing');
223
+
224
+ return {
225
+ ...group,
226
+ installed,
227
+ comparisons,
228
+ stale,
229
+ missing,
230
+ issueCount: stale.length + missing.length,
231
+ };
232
+ });
233
+
234
+ const checkedGroups = groups.filter((group) => group.installed);
235
+ const issues = checkedGroups.flatMap((group) => group.comparisons.filter((entry) => entry.status !== 'clean'));
236
+
237
+ return {
238
+ groups,
239
+ checkedGroups: checkedGroups.map((group) => group.runtime),
240
+ hasInstalledRuntimeSurfaces: checkedGroups.length > 0,
241
+ issueCount: issues.length,
242
+ staleCount: issues.filter((entry) => entry.status === 'stale').length,
243
+ missingCount: issues.filter((entry) => entry.status === 'missing').length,
244
+ issues,
245
+ };
246
+ }
247
+
248
+ export function summarizeRuntimeFreshnessIssues(report, limit = 4) {
249
+ if (!report || report.issueCount === 0) return '';
250
+ const listed = report.issues
251
+ .slice(0, limit)
252
+ .map((entry) => `${entry.relativePath} [${entry.status}]`);
253
+ const remainder = report.issueCount - listed.length;
254
+ return remainder > 0 ? `${listed.join(', ')} (+${remainder} more)` : listed.join(', ');
255
+ }
256
+
257
+ export function getRuntimeFreshnessRepairGuidance(report) {
258
+ if (!report || report.issueCount === 0) return 'Run `npx -y gsdd-cli update` to regenerate installed runtime surfaces.';
259
+ const commands = [...new Set(report.issues.map((entry) => entry.repairCommand))];
260
+ if (commands.length === 1) {
261
+ return `Run \`${commands[0]}\` to regenerate the installed runtime surfaces.`;
262
+ }
263
+ return `Run \`npx -y gsdd-cli update\` to regenerate all installed runtime surfaces, or target the affected adapters individually: ${commands.map((command) => `\`${command}\``).join(', ')}.`;
264
+ }
@@ -11,9 +11,13 @@
11
11
  import { createHash } from 'crypto';
12
12
  import { existsSync, readFileSync, writeFileSync } from 'fs';
13
13
  import { join } from 'path';
14
+ import { output } from './cli-utils.mjs';
15
+ import { resolveWorkspaceContext } from './workspace-root.mjs';
14
16
 
15
17
  const FINGERPRINT_FILE = '.state-fingerprint.json';
16
18
  const FINGERPRINT_SOURCES = ['ROADMAP.md', 'SPEC.md', 'config.json'];
19
+ const FINGERPRINT_SCHEMA_VERSION = 2;
20
+ const FINGERPRINT_ALGORITHM = 'sha256:v2:exists-content';
17
21
 
18
22
  /**
19
23
  * Compute a SHA-256 fingerprint from the planning truth files.
@@ -23,15 +27,52 @@ const FINGERPRINT_SOURCES = ['ROADMAP.md', 'SPEC.md', 'config.json'];
23
27
  export function computeFingerprint(planningDir) {
24
28
  const hash = createHash('sha256');
25
29
  const sources = {};
30
+ const files = {};
26
31
  for (const file of FINGERPRINT_SOURCES) {
27
32
  const filePath = join(planningDir, file);
28
- const content = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
33
+ const exists = existsSync(filePath);
34
+ const content = exists ? readFileSync(filePath, 'utf-8') : '';
35
+ hash.update(`${file}:${exists ? 'exists' : 'missing'}:${content}\n`);
36
+ sources[file] = exists;
37
+ files[file] = {
38
+ exists,
39
+ hash: createHash('sha256').update(content).digest('hex'),
40
+ };
41
+ }
42
+ return { hash: hash.digest('hex'), sources, files };
43
+ }
44
+
45
+ function computeLegacyFingerprint(planningDir) {
46
+ const hash = createHash('sha256');
47
+ const sources = {};
48
+ for (const file of FINGERPRINT_SOURCES) {
49
+ const filePath = join(planningDir, file);
50
+ const exists = existsSync(filePath);
51
+ const content = exists ? readFileSync(filePath, 'utf-8') : '';
29
52
  hash.update(`${file}:${content}\n`);
30
- sources[file] = existsSync(filePath);
53
+ sources[file] = exists;
31
54
  }
32
55
  return { hash: hash.digest('hex'), sources };
33
56
  }
34
57
 
58
+ export function cmdSessionFingerprint(...args) {
59
+ const { args: normalizedArgs, planningDir, invalid, error } = resolveWorkspaceContext(args);
60
+ if (invalid) {
61
+ console.error(error);
62
+ process.exitCode = 1;
63
+ return;
64
+ }
65
+
66
+ const [action] = normalizedArgs;
67
+ if (action !== 'write') {
68
+ console.error('Usage: node .planning/bin/gsdd.mjs session-fingerprint write');
69
+ process.exitCode = 1;
70
+ return;
71
+ }
72
+
73
+ output({ operation: 'session-fingerprint write', fingerprint: writeFingerprint(planningDir) });
74
+ }
75
+
35
76
  /**
36
77
  * Read the stored fingerprint from .planning/.state-fingerprint.json.
37
78
  * Returns null if the file does not exist or is unparseable.
@@ -50,10 +91,13 @@ export function readStoredFingerprint(planningDir) {
50
91
  * Write the current fingerprint to .planning/.state-fingerprint.json.
51
92
  */
52
93
  export function writeFingerprint(planningDir) {
53
- const { hash, sources } = computeFingerprint(planningDir);
94
+ const { hash, sources, files } = computeFingerprint(planningDir);
54
95
  const data = {
96
+ schemaVersion: FINGERPRINT_SCHEMA_VERSION,
97
+ algorithm: FINGERPRINT_ALGORITHM,
55
98
  hash,
56
99
  sources,
100
+ files,
57
101
  timestamp: new Date().toISOString(),
58
102
  };
59
103
  writeFileSync(join(planningDir, FINGERPRINT_FILE), JSON.stringify(data, null, 2) + '\n');
@@ -69,27 +113,33 @@ export function writeFingerprint(planningDir) {
69
113
  */
70
114
  export function checkDrift(planningDir) {
71
115
  const stored = readStoredFingerprint(planningDir);
72
- const { hash: currentHash, sources: currentSources } = computeFingerprint(planningDir);
116
+ const { hash: currentHash, sources: currentSources, files: currentFiles } = computeFingerprint(planningDir);
73
117
 
74
118
  if (!stored) {
75
119
  return {
76
120
  drifted: false,
77
121
  noBaseline: true,
122
+ classification: 'no_baseline',
78
123
  details: ['No stored fingerprint found — first session or fingerprint was cleared.'],
79
124
  stored: null,
80
- current: { hash: currentHash, sources: currentSources },
125
+ current: { hash: currentHash, sources: currentSources, files: currentFiles },
126
+ files: [],
81
127
  };
82
128
  }
83
129
 
84
- const drifted = stored.hash !== currentHash;
130
+ const isLegacy = !stored.schemaVersion && !stored.files;
131
+ const comparison = isLegacy ? computeLegacyFingerprint(planningDir) : { hash: currentHash };
132
+ const drifted = stored.hash !== comparison.hash;
85
133
  const details = [];
134
+ const files = drifted
135
+ ? FINGERPRINT_SOURCES.map((file) => classifyFileDrift(file, stored, currentSources, currentFiles, { legacy: isLegacy }))
136
+ : FINGERPRINT_SOURCES.map((file) => ({ file, status: 'unchanged' }));
86
137
  if (drifted) {
87
- for (const file of FINGERPRINT_SOURCES) {
88
- const was = stored.sources?.[file] ?? false;
89
- const now = currentSources[file];
90
- if (was && !now) details.push(`${file} was removed`);
91
- else if (!was && now) details.push(`${file} was created`);
92
- else if (was && now) details.push(`${file} may have changed`);
138
+ for (const file of files) {
139
+ if (file.status === 'created') details.push(`${file.file} created`);
140
+ else if (file.status === 'removed') details.push(`${file.file} removed`);
141
+ else if (file.status === 'changed') details.push(`${file.file} changed`);
142
+ else if (file.status === 'unknown') details.push(`${file.file} may have changed`);
93
143
  }
94
144
  if (details.length === 0) {
95
145
  details.push('Planning state hash changed since last recorded session.');
@@ -99,8 +149,35 @@ export function checkDrift(planningDir) {
99
149
  return {
100
150
  drifted,
101
151
  noBaseline: false,
152
+ classification: drifted ? 'planning_state_drift' : 'clean',
153
+ compatibility: isLegacy ? 'legacy_v1' : null,
154
+ needsBaselineRefresh: isLegacy && !drifted,
102
155
  details,
103
- stored: { hash: stored.hash, timestamp: stored.timestamp },
104
- current: { hash: currentHash, sources: currentSources },
156
+ files,
157
+ stored: {
158
+ hash: stored.hash,
159
+ timestamp: stored.timestamp,
160
+ schemaVersion: stored.schemaVersion ?? null,
161
+ algorithm: stored.algorithm ?? null,
162
+ files: stored.files ?? null,
163
+ },
164
+ current: { hash: currentHash, sources: currentSources, files: currentFiles },
165
+ };
166
+ }
167
+
168
+ function classifyFileDrift(file, stored, currentSources, currentFiles, { legacy = false } = {}) {
169
+ const was = stored.sources?.[file] ?? false;
170
+ const now = currentSources[file];
171
+
172
+ if (was && !now) return { file, status: 'removed' };
173
+ if (!was && now) return { file, status: 'created' };
174
+ if (!was && !now) return { file, status: 'unchanged' };
175
+ if (legacy) return { file, status: 'unknown' };
176
+
177
+ const storedFile = stored.files?.[file];
178
+ if (!storedFile?.hash) return { file, status: 'unknown' };
179
+ return {
180
+ file,
181
+ status: storedFile.hash === currentFiles[file].hash ? 'unchanged' : 'changed',
105
182
  };
106
183
  }