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.
- package/LICENSE +21 -21
- package/README.md +610 -608
- package/agents/DISTILLATION.md +421 -421
- package/agents/README.md +62 -62
- package/agents/approach-explorer.md +370 -361
- package/agents/debugger.md +82 -82
- package/agents/executor.md +473 -394
- package/agents/integration-checker.md +318 -318
- package/agents/mapper.md +103 -103
- package/agents/planner.md +342 -313
- package/agents/researcher.md +84 -84
- package/agents/roadmapper.md +296 -296
- package/agents/synthesizer.md +236 -236
- package/agents/verifier.md +337 -337
- package/bin/adapters/agents.mjs +34 -34
- package/bin/adapters/claude.mjs +193 -191
- package/bin/adapters/codex.mjs +85 -85
- package/bin/adapters/index.mjs +20 -20
- package/bin/adapters/opencode.mjs +280 -278
- package/bin/gsdd.mjs +123 -116
- package/bin/lib/cli-utils.mjs +28 -28
- package/bin/lib/evidence-contract.mjs +325 -112
- package/bin/lib/file-ops.mjs +186 -144
- package/bin/lib/health-truth.mjs +196 -178
- package/bin/lib/health.mjs +246 -226
- package/bin/lib/init-flow.mjs +247 -231
- package/bin/lib/init-prompts.mjs +248 -247
- package/bin/lib/init-runtime.mjs +193 -190
- package/bin/lib/init.mjs +17 -17
- package/bin/lib/lifecycle-preflight.mjs +760 -326
- package/bin/lib/lifecycle-state.mjs +356 -267
- package/bin/lib/manifest.mjs +116 -114
- package/bin/lib/models.mjs +411 -411
- package/bin/lib/phase.mjs +365 -358
- package/bin/lib/plan-constants.mjs +35 -30
- package/bin/lib/provenance.mjs +109 -106
- package/bin/lib/rendering.mjs +119 -83
- package/bin/lib/runtime-freshness.mjs +214 -214
- package/bin/lib/session-fingerprint.mjs +91 -14
- package/bin/lib/templates.mjs +225 -224
- package/bin/lib/workspace-root.mjs +2 -1
- package/distilled/DESIGN.md +2461 -2323
- package/distilled/EVIDENCE-INDEX.md +418 -392
- package/distilled/README.md +196 -193
- package/distilled/SKILL.md +86 -85
- package/distilled/templates/agents.block.md +21 -21
- package/distilled/templates/agents.md +6 -6
- package/distilled/templates/approach.md +272 -232
- package/distilled/templates/auth-matrix.md +78 -78
- package/distilled/templates/brownfield-change/CHANGE.md +99 -0
- package/distilled/templates/brownfield-change/HANDOFF.md +38 -0
- package/distilled/templates/brownfield-change/VERIFICATION.md +56 -0
- package/distilled/templates/codebase/architecture.md +110 -110
- package/distilled/templates/codebase/concerns.md +95 -95
- package/distilled/templates/codebase/conventions.md +193 -193
- package/distilled/templates/codebase/stack.md +96 -96
- package/distilled/templates/delegates/approach-explorer.md +28 -25
- package/distilled/templates/delegates/mapper-arch.md +26 -26
- package/distilled/templates/delegates/mapper-concerns.md +27 -27
- package/distilled/templates/delegates/mapper-quality.md +28 -28
- package/distilled/templates/delegates/mapper-tech.md +25 -25
- package/distilled/templates/delegates/plan-checker.md +78 -68
- package/distilled/templates/delegates/researcher-architecture.md +30 -30
- package/distilled/templates/delegates/researcher-features.md +30 -30
- package/distilled/templates/delegates/researcher-pitfalls.md +30 -30
- package/distilled/templates/delegates/researcher-stack.md +30 -30
- package/distilled/templates/delegates/researcher-synthesizer.md +31 -31
- package/distilled/templates/research/architecture.md +57 -57
- package/distilled/templates/research/features.md +23 -23
- package/distilled/templates/research/pitfalls.md +46 -46
- package/distilled/templates/research/stack.md +45 -45
- package/distilled/templates/research/summary.md +67 -67
- package/distilled/templates/roadmap.md +74 -62
- package/distilled/templates/spec.md +110 -110
- package/distilled/workflows/audit-milestone.md +303 -271
- package/distilled/workflows/complete-milestone.md +349 -332
- package/distilled/workflows/execute.md +457 -450
- package/distilled/workflows/map-codebase.md +253 -253
- package/distilled/workflows/new-milestone.md +242 -238
- package/distilled/workflows/new-project.md +398 -398
- package/distilled/workflows/pause.md +160 -156
- package/distilled/workflows/plan-milestone-gaps.md +183 -183
- package/distilled/workflows/plan.md +454 -448
- package/distilled/workflows/progress.md +227 -223
- package/distilled/workflows/quick.md +351 -347
- package/distilled/workflows/resume.md +220 -212
- package/distilled/workflows/verify-work.md +260 -260
- package/distilled/workflows/verify.md +431 -429
- package/docs/BROWNFIELD-PROOF.md +95 -95
- package/docs/RUNTIME-SUPPORT.md +80 -69
- package/docs/USER-GUIDE.md +394 -386
- package/docs/VERIFICATION-DISCIPLINE.md +59 -59
- package/docs/claude/context-monitor.md +98 -98
- package/docs/proof/consumer-node-cli/README.md +37 -37
- package/docs/proof/consumer-node-cli/ROADMAP.md +14 -14
- package/docs/proof/consumer-node-cli/SPEC.md +17 -17
- package/docs/proof/consumer-node-cli/brief.md +9 -9
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -34
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -10
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -30
- 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
|
|
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] =
|
|
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
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
else if (
|
|
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
|
-
|
|
104
|
-
|
|
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
|
}
|