gsdd-cli 0.16.1 → 0.18.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.
Files changed (47) hide show
  1. package/README.md +109 -60
  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 +6 -4
  7. package/bin/lib/evidence-contract.mjs +112 -0
  8. package/bin/lib/health-truth.mjs +29 -31
  9. package/bin/lib/health.mjs +32 -80
  10. package/bin/lib/init-runtime.mjs +44 -24
  11. package/bin/lib/init.mjs +3 -7
  12. package/bin/lib/lifecycle-preflight.mjs +333 -0
  13. package/bin/lib/lifecycle-state.mjs +293 -0
  14. package/bin/lib/phase.mjs +81 -26
  15. package/bin/lib/provenance.mjs +20 -1
  16. package/bin/lib/rendering.mjs +8 -0
  17. package/bin/lib/runtime-freshness.mjs +239 -0
  18. package/bin/lib/session-fingerprint.mjs +106 -0
  19. package/distilled/DESIGN.md +398 -12
  20. package/distilled/EVIDENCE-INDEX.md +105 -0
  21. package/distilled/README.md +44 -28
  22. package/distilled/SKILL.md +4 -4
  23. package/distilled/templates/agents.block.md +1 -1
  24. package/distilled/workflows/audit-milestone.md +50 -0
  25. package/distilled/workflows/complete-milestone.md +38 -2
  26. package/distilled/workflows/execute.md +13 -0
  27. package/distilled/workflows/map-codebase.md +14 -3
  28. package/distilled/workflows/new-milestone.md +13 -0
  29. package/distilled/workflows/new-project.md +3 -1
  30. package/distilled/workflows/plan.md +3 -1
  31. package/distilled/workflows/progress.md +68 -13
  32. package/distilled/workflows/quick.md +15 -8
  33. package/distilled/workflows/resume.md +14 -1
  34. package/distilled/workflows/verify.md +52 -17
  35. package/docs/BROWNFIELD-PROOF.md +95 -0
  36. package/docs/RUNTIME-SUPPORT.md +77 -0
  37. package/docs/USER-GUIDE.md +439 -0
  38. package/docs/VERIFICATION-DISCIPLINE.md +59 -0
  39. package/docs/claude/context-monitor.md +98 -0
  40. package/docs/proof/consumer-node-cli/README.md +37 -0
  41. package/docs/proof/consumer-node-cli/ROADMAP.md +14 -0
  42. package/docs/proof/consumer-node-cli/SPEC.md +17 -0
  43. package/docs/proof/consumer-node-cli/brief.md +9 -0
  44. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -0
  45. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -0
  46. package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -0
  47. package/package.json +34 -27
@@ -0,0 +1,239 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import {
4
+ buildPortableSkillEntries,
5
+ getDelegateContent,
6
+ renderOpenCodeCommandContent,
7
+ renderSkillContent,
8
+ } from './rendering.mjs';
9
+ import {
10
+ CLAUDE_MODEL_PROFILES,
11
+ renderClaudeApproachExplorer,
12
+ renderClaudePlanChecker,
13
+ renderClaudePlanCommand,
14
+ renderClaudePlanSkill,
15
+ } from '../adapters/claude.mjs';
16
+ import {
17
+ renderOpenCodeApproachExplorer,
18
+ renderOpenCodePlanChecker,
19
+ renderOpenCodePlanCommand,
20
+ } from '../adapters/opencode.mjs';
21
+ import {
22
+ renderCodexApproachExplorer,
23
+ renderCodexPlanChecker,
24
+ } from '../adapters/codex.mjs';
25
+ import {
26
+ getRuntimeModelOverride,
27
+ loadProjectModelConfig,
28
+ resolveRuntimeAgentModel,
29
+ } from './models.mjs';
30
+
31
+ function normalizeContent(content) {
32
+ return String(content).replace(/\r\n/g, '\n');
33
+ }
34
+
35
+ function compareGeneratedFile({ cwd, runtime, relativePath, expectedContent, repairCommand }) {
36
+ const absolutePath = join(cwd, relativePath);
37
+ if (!existsSync(absolutePath)) {
38
+ return {
39
+ runtime,
40
+ relativePath,
41
+ status: 'missing',
42
+ repairCommand,
43
+ };
44
+ }
45
+
46
+ const actualContent = normalizeContent(readFileSync(absolutePath, 'utf-8'));
47
+ const expected = normalizeContent(expectedContent);
48
+ if (actualContent === expected) {
49
+ return {
50
+ runtime,
51
+ relativePath,
52
+ status: 'clean',
53
+ repairCommand,
54
+ };
55
+ }
56
+
57
+ return {
58
+ runtime,
59
+ relativePath,
60
+ status: 'stale',
61
+ repairCommand,
62
+ };
63
+ }
64
+
65
+ function buildClaudeEntries({ cwd, workflows }) {
66
+ const checkerModelAlias = resolveRuntimeAgentModel({
67
+ cwd,
68
+ runtime: 'claude',
69
+ agentId: 'plan-checker',
70
+ profileMap: CLAUDE_MODEL_PROFILES,
71
+ });
72
+ const explorerModelAlias = resolveRuntimeAgentModel({
73
+ cwd,
74
+ runtime: 'claude',
75
+ agentId: 'approach-explorer',
76
+ profileMap: CLAUDE_MODEL_PROFILES,
77
+ });
78
+
79
+ const entries = workflows.map((workflow) => ({
80
+ relativePath: `.claude/skills/${workflow.name}/SKILL.md`,
81
+ expectedContent: workflow.name === 'gsdd-plan'
82
+ ? renderClaudePlanSkill()
83
+ : renderSkillContent(workflow),
84
+ }));
85
+
86
+ entries.push(
87
+ {
88
+ relativePath: '.claude/commands/gsdd-plan.md',
89
+ expectedContent: renderClaudePlanCommand(),
90
+ },
91
+ {
92
+ relativePath: '.claude/agents/gsdd-plan-checker.md',
93
+ expectedContent: renderClaudePlanChecker(getDelegateContent('plan-checker.md'), checkerModelAlias),
94
+ },
95
+ {
96
+ relativePath: '.claude/agents/gsdd-approach-explorer.md',
97
+ expectedContent: renderClaudeApproachExplorer(getDelegateContent('approach-explorer.md'), explorerModelAlias),
98
+ }
99
+ );
100
+
101
+ return entries;
102
+ }
103
+
104
+ function buildOpenCodeEntries({ cwd, workflows }) {
105
+ const config = loadProjectModelConfig(cwd);
106
+ const checkerModelId = getRuntimeModelOverride(config, 'opencode', 'plan-checker');
107
+ const explorerModelId = getRuntimeModelOverride(config, 'opencode', 'approach-explorer');
108
+
109
+ const entries = workflows.map((workflow) => ({
110
+ relativePath: `.opencode/commands/${workflow.name}.md`,
111
+ expectedContent: workflow.name === 'gsdd-plan'
112
+ ? renderOpenCodePlanCommand()
113
+ : renderOpenCodeCommandContent(workflow),
114
+ }));
115
+
116
+ entries.push(
117
+ {
118
+ relativePath: '.opencode/agents/gsdd-plan-checker.md',
119
+ expectedContent: renderOpenCodePlanChecker(getDelegateContent('plan-checker.md'), checkerModelId),
120
+ },
121
+ {
122
+ relativePath: '.opencode/agents/gsdd-approach-explorer.md',
123
+ expectedContent: renderOpenCodeApproachExplorer(getDelegateContent('approach-explorer.md'), explorerModelId),
124
+ }
125
+ );
126
+
127
+ return entries;
128
+ }
129
+
130
+ function buildCodexEntries({ cwd }) {
131
+ const config = loadProjectModelConfig(cwd);
132
+ const checkerModelId = getRuntimeModelOverride(config, 'codex', 'plan-checker');
133
+ const explorerModelId = getRuntimeModelOverride(config, 'codex', 'approach-explorer');
134
+
135
+ return [
136
+ {
137
+ relativePath: '.codex/agents/gsdd-plan-checker.toml',
138
+ expectedContent: renderCodexPlanChecker(getDelegateContent('plan-checker.md'), checkerModelId),
139
+ },
140
+ {
141
+ relativePath: '.codex/agents/gsdd-approach-explorer.toml',
142
+ expectedContent: renderCodexApproachExplorer(getDelegateContent('approach-explorer.md'), explorerModelId),
143
+ },
144
+ ];
145
+ }
146
+
147
+ export function collectExpectedRuntimeSurfaceGroups({ cwd = process.cwd(), workflows }) {
148
+ return [
149
+ {
150
+ runtime: 'portable',
151
+ label: 'portable skills',
152
+ root: '.agents/skills',
153
+ repairCommand: 'gsdd update',
154
+ entries: buildPortableSkillEntries(workflows).map((entry) => ({
155
+ relativePath: entry.relativePath,
156
+ expectedContent: entry.content,
157
+ })),
158
+ },
159
+ {
160
+ runtime: 'claude',
161
+ label: 'Claude Code native surfaces',
162
+ root: '.claude',
163
+ repairCommand: 'gsdd update --tools claude',
164
+ entries: buildClaudeEntries({ cwd, workflows }),
165
+ },
166
+ {
167
+ runtime: 'opencode',
168
+ label: 'OpenCode native surfaces',
169
+ root: '.opencode',
170
+ repairCommand: 'gsdd update --tools opencode',
171
+ entries: buildOpenCodeEntries({ cwd, workflows }),
172
+ },
173
+ {
174
+ runtime: 'codex',
175
+ label: 'Codex CLI native agents',
176
+ root: '.codex',
177
+ repairCommand: 'gsdd update --tools codex',
178
+ entries: buildCodexEntries({ cwd }),
179
+ },
180
+ ];
181
+ }
182
+
183
+ export function evaluateRuntimeFreshness({ cwd = process.cwd(), workflows = [] }) {
184
+ const groups = collectExpectedRuntimeSurfaceGroups({ cwd, workflows }).map((group) => {
185
+ const installed = existsSync(join(cwd, group.root));
186
+ const comparisons = installed
187
+ ? group.entries.map((entry) => compareGeneratedFile({
188
+ cwd,
189
+ runtime: group.runtime,
190
+ relativePath: entry.relativePath,
191
+ expectedContent: entry.expectedContent,
192
+ repairCommand: group.repairCommand,
193
+ }))
194
+ : [];
195
+
196
+ const stale = comparisons.filter((entry) => entry.status === 'stale');
197
+ const missing = comparisons.filter((entry) => entry.status === 'missing');
198
+
199
+ return {
200
+ ...group,
201
+ installed,
202
+ comparisons,
203
+ stale,
204
+ missing,
205
+ issueCount: stale.length + missing.length,
206
+ };
207
+ });
208
+
209
+ const checkedGroups = groups.filter((group) => group.installed);
210
+ const issues = checkedGroups.flatMap((group) => group.comparisons.filter((entry) => entry.status !== 'clean'));
211
+
212
+ return {
213
+ groups,
214
+ checkedGroups: checkedGroups.map((group) => group.runtime),
215
+ hasInstalledRuntimeSurfaces: checkedGroups.length > 0,
216
+ issueCount: issues.length,
217
+ staleCount: issues.filter((entry) => entry.status === 'stale').length,
218
+ missingCount: issues.filter((entry) => entry.status === 'missing').length,
219
+ issues,
220
+ };
221
+ }
222
+
223
+ export function summarizeRuntimeFreshnessIssues(report, limit = 4) {
224
+ if (!report || report.issueCount === 0) return '';
225
+ const listed = report.issues
226
+ .slice(0, limit)
227
+ .map((entry) => `${entry.relativePath} [${entry.status}]`);
228
+ const remainder = report.issueCount - listed.length;
229
+ return remainder > 0 ? `${listed.join(', ')} (+${remainder} more)` : listed.join(', ');
230
+ }
231
+
232
+ export function getRuntimeFreshnessRepairGuidance(report) {
233
+ if (!report || report.issueCount === 0) return 'Run `gsdd update` to regenerate installed runtime surfaces.';
234
+ const commands = [...new Set(report.issues.map((entry) => entry.repairCommand))];
235
+ if (commands.length === 1) {
236
+ return `Run \`${commands[0]}\` to regenerate the installed runtime surfaces.`;
237
+ }
238
+ return `Run \`gsdd update\` to regenerate all installed runtime surfaces, or target the affected adapters individually: ${commands.map((command) => `\`${command}\``).join(', ')}.`;
239
+ }
@@ -0,0 +1,106 @@
1
+ // session-fingerprint.mjs — Planning state drift detection
2
+ //
3
+ // Computes a SHA-256 fingerprint from the combined contents of ROADMAP.md,
4
+ // SPEC.md, and config.json. When the fingerprint stored in
5
+ // .planning/.state-fingerprint.json no longer matches the live files, the
6
+ // preflight and health systems can warn that planning state drifted since
7
+ // the last recorded session.
8
+ //
9
+ // The fingerprint file is session-local and gitignored by convention.
10
+
11
+ import { createHash } from 'crypto';
12
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
13
+ import { join } from 'path';
14
+
15
+ const FINGERPRINT_FILE = '.state-fingerprint.json';
16
+ const FINGERPRINT_SOURCES = ['ROADMAP.md', 'SPEC.md', 'config.json'];
17
+
18
+ /**
19
+ * Compute a SHA-256 fingerprint from the planning truth files.
20
+ * Missing files contribute an empty string (so a newly created file
21
+ * registers as drift).
22
+ */
23
+ export function computeFingerprint(planningDir) {
24
+ const hash = createHash('sha256');
25
+ const sources = {};
26
+ for (const file of FINGERPRINT_SOURCES) {
27
+ const filePath = join(planningDir, file);
28
+ const content = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
29
+ hash.update(`${file}:${content}\n`);
30
+ sources[file] = existsSync(filePath);
31
+ }
32
+ return { hash: hash.digest('hex'), sources };
33
+ }
34
+
35
+ /**
36
+ * Read the stored fingerprint from .planning/.state-fingerprint.json.
37
+ * Returns null if the file does not exist or is unparseable.
38
+ */
39
+ export function readStoredFingerprint(planningDir) {
40
+ const filePath = join(planningDir, FINGERPRINT_FILE);
41
+ if (!existsSync(filePath)) return null;
42
+ try {
43
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Write the current fingerprint to .planning/.state-fingerprint.json.
51
+ */
52
+ export function writeFingerprint(planningDir) {
53
+ const { hash, sources } = computeFingerprint(planningDir);
54
+ const data = {
55
+ hash,
56
+ sources,
57
+ timestamp: new Date().toISOString(),
58
+ };
59
+ writeFileSync(join(planningDir, FINGERPRINT_FILE), JSON.stringify(data, null, 2) + '\n');
60
+ return data;
61
+ }
62
+
63
+ /**
64
+ * Check whether the current planning state has drifted from the stored
65
+ * fingerprint. Returns { drifted, details, stored, current }.
66
+ *
67
+ * If no stored fingerprint exists, returns drifted: false with a note
68
+ * that no baseline was found (first session after adoption).
69
+ */
70
+ export function checkDrift(planningDir) {
71
+ const stored = readStoredFingerprint(planningDir);
72
+ const { hash: currentHash, sources: currentSources } = computeFingerprint(planningDir);
73
+
74
+ if (!stored) {
75
+ return {
76
+ drifted: false,
77
+ noBaseline: true,
78
+ details: ['No stored fingerprint found — first session or fingerprint was cleared.'],
79
+ stored: null,
80
+ current: { hash: currentHash, sources: currentSources },
81
+ };
82
+ }
83
+
84
+ const drifted = stored.hash !== currentHash;
85
+ const details = [];
86
+ 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`);
93
+ }
94
+ if (details.length === 0) {
95
+ details.push('Planning state hash changed since last recorded session.');
96
+ }
97
+ }
98
+
99
+ return {
100
+ drifted,
101
+ noBaseline: false,
102
+ details,
103
+ stored: { hash: stored.hash, timestamp: stored.timestamp },
104
+ current: { hash: currentHash, sources: currentSources },
105
+ };
106
+ }