gsdd-cli 0.2.0 → 0.16.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/README.md +67 -34
- package/agents/DISTILLATION.md +15 -13
- package/agents/planner.md +2 -0
- package/bin/adapters/agents.mjs +1 -0
- package/bin/adapters/claude.mjs +12 -3
- package/bin/adapters/codex.mjs +4 -0
- package/bin/adapters/opencode.mjs +13 -4
- package/bin/gsdd.mjs +64 -39
- package/bin/lib/cli-utils.mjs +1 -1
- package/bin/lib/file-ops.mjs +161 -0
- package/bin/lib/health-truth.mjs +188 -0
- package/bin/lib/health.mjs +65 -12
- package/bin/lib/init-flow.mjs +267 -0
- package/bin/lib/init-prompts.mjs +247 -0
- package/bin/lib/init-runtime.mjs +226 -0
- package/bin/lib/init.mjs +19 -378
- package/bin/lib/models.mjs +19 -4
- package/bin/lib/phase.mjs +100 -14
- package/bin/lib/plan-constants.mjs +30 -0
- package/bin/lib/provenance.mjs +146 -0
- package/bin/lib/templates.mjs +17 -0
- package/distilled/DESIGN.md +625 -41
- package/distilled/EVIDENCE-INDEX.md +297 -0
- package/distilled/README.md +49 -21
- package/distilled/SKILL.md +89 -85
- package/distilled/templates/agents.block.md +13 -82
- package/distilled/templates/agents.md +0 -7
- package/distilled/templates/delegates/plan-checker.md +11 -4
- package/distilled/workflows/audit-milestone.md +7 -5
- package/distilled/workflows/complete-milestone.md +297 -0
- package/distilled/workflows/execute.md +188 -19
- package/distilled/workflows/map-codebase.md +14 -7
- package/distilled/workflows/new-milestone.md +249 -0
- package/distilled/workflows/new-project.md +28 -24
- package/distilled/workflows/pause.md +42 -6
- package/distilled/workflows/plan-milestone-gaps.md +183 -0
- package/distilled/workflows/plan.md +78 -13
- package/distilled/workflows/progress.md +42 -19
- package/distilled/workflows/quick.md +171 -8
- package/distilled/workflows/resume.md +121 -11
- package/distilled/workflows/verify-work.md +260 -0
- package/distilled/workflows/verify.md +124 -33
- package/package.json +9 -7
package/bin/lib/cli-utils.mjs
CHANGED
|
@@ -15,7 +15,7 @@ export function parseFlagValue(flagArgs, flagName) {
|
|
|
15
15
|
export function parseToolsFlag(flagArgs) {
|
|
16
16
|
const { value } = parseFlagValue(flagArgs, '--tools');
|
|
17
17
|
if (!value) return [];
|
|
18
|
-
if (value === 'all') return ['claude', 'opencode', 'codex', 'agents'];
|
|
18
|
+
if (value === 'all') return ['claude', 'opencode', 'codex', 'agents', 'cursor', 'copilot', 'gemini'];
|
|
19
19
|
return value.split(',').map((v) => v.trim()).filter(Boolean);
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from 'path';
|
|
3
|
+
import { output, parseFlagValue } from './cli-utils.mjs';
|
|
4
|
+
|
|
5
|
+
class FileOpError extends Error {}
|
|
6
|
+
|
|
7
|
+
function fail(message) {
|
|
8
|
+
console.error(message);
|
|
9
|
+
throw new FileOpError(message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolveWorkspacePath(cwd, target) {
|
|
13
|
+
const workspaceRoot = resolve(cwd);
|
|
14
|
+
const resolved = resolve(workspaceRoot, target);
|
|
15
|
+
const rel = relative(workspaceRoot, resolved);
|
|
16
|
+
|
|
17
|
+
if (rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))) {
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fail(`Path must stay inside the workspace: ${target}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getMissingBehavior(args) {
|
|
25
|
+
const parsed = parseFlagValue(args, '--missing');
|
|
26
|
+
if (!parsed.present) return 'error';
|
|
27
|
+
if (parsed.invalid) fail('Usage: --missing <error|ok>');
|
|
28
|
+
if (!['error', 'ok'].includes(parsed.value)) fail('Usage: --missing <error|ok>');
|
|
29
|
+
return parsed.value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cmdCopy(cwd, args) {
|
|
33
|
+
const [sourceArg, destinationArg, ...flags] = args;
|
|
34
|
+
if (!sourceArg || !destinationArg) {
|
|
35
|
+
fail('Usage: gsdd file-op copy <source> <destination> [--missing <error|ok>]');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const missingBehavior = getMissingBehavior(flags);
|
|
39
|
+
const source = resolveWorkspacePath(cwd, sourceArg);
|
|
40
|
+
const destination = resolveWorkspacePath(cwd, destinationArg);
|
|
41
|
+
|
|
42
|
+
if (!existsSync(source)) {
|
|
43
|
+
if (missingBehavior === 'ok') {
|
|
44
|
+
output({ operation: 'copy', source: sourceArg, destination: destinationArg, changed: false, reason: 'missing_source' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
fail(`Source file does not exist: ${sourceArg}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (statSync(source).isDirectory()) {
|
|
51
|
+
fail(`Copy only supports files in this phase: ${sourceArg}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
55
|
+
cpSync(source, destination, { force: true });
|
|
56
|
+
output({ operation: 'copy', source: sourceArg, destination: destinationArg, changed: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cmdDelete(cwd, args) {
|
|
60
|
+
const [targetArg, ...flags] = args;
|
|
61
|
+
if (!targetArg) {
|
|
62
|
+
fail('Usage: gsdd file-op delete <target> [--missing <error|ok>]');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const missingBehavior = getMissingBehavior(flags);
|
|
66
|
+
const target = resolveWorkspacePath(cwd, targetArg);
|
|
67
|
+
|
|
68
|
+
if (!existsSync(target)) {
|
|
69
|
+
if (missingBehavior === 'ok') {
|
|
70
|
+
output({ operation: 'delete', target: targetArg, changed: false, reason: 'missing_target' });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
fail(`Target file does not exist: ${targetArg}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (statSync(target).isDirectory()) {
|
|
77
|
+
fail(`Delete only supports files in this phase: ${targetArg}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
unlinkSync(target);
|
|
81
|
+
output({ operation: 'delete', target: targetArg, changed: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function cmdRegexSub(cwd, args) {
|
|
85
|
+
const [targetArg, pattern, replacement, ...flags] = args;
|
|
86
|
+
if (!targetArg || pattern === undefined || replacement === undefined) {
|
|
87
|
+
fail('Usage: gsdd file-op regex-sub <target> <pattern> <replacement> [--flags <flags>] [--missing <error|ok>]');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const regexFlags = parseFlagValue(flags, '--flags');
|
|
91
|
+
if (regexFlags.present && regexFlags.invalid) {
|
|
92
|
+
fail('Usage: --flags <regex-flags>');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const missingBehavior = getMissingBehavior(flags);
|
|
96
|
+
const target = resolveWorkspacePath(cwd, targetArg);
|
|
97
|
+
|
|
98
|
+
if (!existsSync(target)) {
|
|
99
|
+
if (missingBehavior === 'ok') {
|
|
100
|
+
output({ operation: 'regex-sub', target: targetArg, changed: false, reason: 'missing_target' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
fail(`Target file does not exist: ${targetArg}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (statSync(target).isDirectory()) {
|
|
107
|
+
fail(`regex-sub only supports files in this phase: ${targetArg}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let regex;
|
|
111
|
+
try {
|
|
112
|
+
regex = new RegExp(pattern, regexFlags.value || 'g');
|
|
113
|
+
} catch (error) {
|
|
114
|
+
fail(`Invalid regex pattern: ${error.message}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const source = readFileSync(target, 'utf-8');
|
|
118
|
+
let replacementCount = 0;
|
|
119
|
+
if (regex.global) {
|
|
120
|
+
const matches = source.match(regex);
|
|
121
|
+
replacementCount = matches ? matches.length : 0;
|
|
122
|
+
} else {
|
|
123
|
+
replacementCount = regex.test(source) ? 1 : 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (replacementCount === 0) {
|
|
127
|
+
fail(`Pattern did not match any text in ${targetArg}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const updated = source.replace(regex, replacement);
|
|
131
|
+
const changed = updated !== source;
|
|
132
|
+
writeFileSync(target, updated);
|
|
133
|
+
output({ operation: 'regex-sub', target: targetArg, changed, replacements: replacementCount });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function cmdFileOp(...args) {
|
|
137
|
+
const cwd = process.cwd();
|
|
138
|
+
const [operation, ...rest] = args;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
switch (operation) {
|
|
142
|
+
case 'copy':
|
|
143
|
+
cmdCopy(cwd, rest);
|
|
144
|
+
return;
|
|
145
|
+
case 'delete':
|
|
146
|
+
cmdDelete(cwd, rest);
|
|
147
|
+
return;
|
|
148
|
+
case 'regex-sub':
|
|
149
|
+
cmdRegexSub(cwd, rest);
|
|
150
|
+
return;
|
|
151
|
+
default:
|
|
152
|
+
fail('Usage: gsdd file-op <copy|delete|regex-sub> ...');
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof FileOpError) {
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
export const TRUTH_CHECK_IDS = ['W7', 'W8', 'W9', 'W10'];
|
|
5
|
+
|
|
6
|
+
export function runTruthChecks(planningDir, frameworkDir, actualCheckIds) {
|
|
7
|
+
const warnings = [];
|
|
8
|
+
const designPath = join(frameworkDir, 'distilled', 'DESIGN.md');
|
|
9
|
+
const readmePath = join(frameworkDir, 'distilled', 'README.md');
|
|
10
|
+
const workflowsDir = join(frameworkDir, 'distilled', 'workflows');
|
|
11
|
+
const gapsPath = join(frameworkDir, '.internal-research', 'gaps.md');
|
|
12
|
+
const specPath = join(planningDir, 'SPEC.md');
|
|
13
|
+
const roadmapPath = join(planningDir, 'ROADMAP.md');
|
|
14
|
+
|
|
15
|
+
if (existsSync(designPath)) {
|
|
16
|
+
const documentedIds = extractHealthTableIds(readFileSync(designPath, 'utf-8'));
|
|
17
|
+
const missingFromDesign = actualCheckIds.filter((id) => !documentedIds.includes(id));
|
|
18
|
+
const extraInDesign = documentedIds.filter((id) => !actualCheckIds.includes(id));
|
|
19
|
+
if (missingFromDesign.length > 0 || extraInDesign.length > 0) {
|
|
20
|
+
const parts = [];
|
|
21
|
+
if (missingFromDesign.length > 0) parts.push(`missing in DESIGN.md: ${missingFromDesign.join(', ')}`);
|
|
22
|
+
if (extraInDesign.length > 0) parts.push(`extra in DESIGN.md: ${extraInDesign.join(', ')}`);
|
|
23
|
+
warnings.push({
|
|
24
|
+
id: 'W7',
|
|
25
|
+
severity: 'WARN',
|
|
26
|
+
message: `DESIGN.md health check table is out of sync (${parts.join('; ')})`,
|
|
27
|
+
fix: 'Update distilled/DESIGN.md section 20 to match the implemented health checks',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (existsSync(readmePath) && existsSync(workflowsDir)) {
|
|
33
|
+
const readme = readFileSync(readmePath, 'utf-8');
|
|
34
|
+
const workflowFiles = readdirSync(workflowsDir).filter((entry) => entry.endsWith('.md')).sort();
|
|
35
|
+
const statusEntries = extractReadmeStatusEntries(readme);
|
|
36
|
+
const treeEntries = extractReadmeWorkflowTreeEntries(readme);
|
|
37
|
+
const issues = [];
|
|
38
|
+
if (statusEntries.length !== workflowFiles.length) {
|
|
39
|
+
issues.push(`status table ${statusEntries.length} != workflows dir ${workflowFiles.length}`);
|
|
40
|
+
}
|
|
41
|
+
if (treeEntries.length !== workflowFiles.length) {
|
|
42
|
+
issues.push(`framework tree ${treeEntries.length} != workflows dir ${workflowFiles.length}`);
|
|
43
|
+
}
|
|
44
|
+
const missingStatus = workflowFiles.filter((name) => !statusEntries.includes(name));
|
|
45
|
+
const missingTree = workflowFiles.filter((name) => !treeEntries.includes(name));
|
|
46
|
+
if (missingStatus.length > 0) issues.push(`missing from status table: ${missingStatus.join(', ')}`);
|
|
47
|
+
if (missingTree.length > 0) issues.push(`missing from framework tree: ${missingTree.join(', ')}`);
|
|
48
|
+
if (issues.length > 0) {
|
|
49
|
+
warnings.push({
|
|
50
|
+
id: 'W8',
|
|
51
|
+
severity: 'WARN',
|
|
52
|
+
message: `distilled/README.md workflow inventory is out of sync (${issues.join('; ')})`,
|
|
53
|
+
fix: 'Update distilled/README.md workflow status table and framework file tree to match distilled/workflows/',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (existsSync(gapsPath)) {
|
|
59
|
+
const gapRefs = extractRepoLocalPaths(readFileSync(gapsPath, 'utf-8'));
|
|
60
|
+
const missingRefs = gapRefs.filter((ref) => !existsSync(join(frameworkDir, ref)));
|
|
61
|
+
if (missingRefs.length > 0) {
|
|
62
|
+
warnings.push({
|
|
63
|
+
id: 'W9',
|
|
64
|
+
severity: 'WARN',
|
|
65
|
+
message: `gaps.md references missing repo-local paths (${missingRefs.join(', ')})`,
|
|
66
|
+
fix: 'Annotate stale gap references as resolved or update them to current repo truth',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (existsSync(specPath) && existsSync(roadmapPath)) {
|
|
72
|
+
const spec = readFileSync(specPath, 'utf-8');
|
|
73
|
+
const roadmap = readFileSync(roadmapPath, 'utf-8');
|
|
74
|
+
const checkedRequirements = extractCheckedRequirements(spec);
|
|
75
|
+
const roadmapRequirements = extractRoadmapRequirements(roadmap);
|
|
76
|
+
const mismatches = [];
|
|
77
|
+
for (const [requirementId, roadmapStatus] of roadmapRequirements.entries()) {
|
|
78
|
+
const specChecked = checkedRequirements.has(requirementId);
|
|
79
|
+
if (roadmapStatus && !specChecked) mismatches.push(`${requirementId} phase complete but SPEC unchecked`);
|
|
80
|
+
if (!roadmapStatus && specChecked) mismatches.push(`${requirementId} SPEC checked but phase incomplete`);
|
|
81
|
+
}
|
|
82
|
+
if (mismatches.length > 0) {
|
|
83
|
+
warnings.push({
|
|
84
|
+
id: 'W10',
|
|
85
|
+
severity: 'WARN',
|
|
86
|
+
message: `ROADMAP/SPEC requirement status drift (${mismatches.join('; ')})`,
|
|
87
|
+
fix: 'Reconcile .planning/ROADMAP.md phase completion markers with .planning/SPEC.md requirement checkboxes',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return warnings;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extractHealthTableIds(content) {
|
|
96
|
+
const section = extractSection(content, '## 20. Workspace Health Diagnostics', '## 21.');
|
|
97
|
+
if (!section) return [];
|
|
98
|
+
return [...normalizeContent(section).matchAll(/^\|\s*([EWI]\d+)\s*\|/gm)].map((result) => result[1]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function extractReadmeStatusEntries(content) {
|
|
102
|
+
const section = extractSection(content, '## Current Status', 'Architecture notes:');
|
|
103
|
+
if (!section) return [];
|
|
104
|
+
return [...normalizeContent(section).matchAll(/\|\s*`([^`]+\.md)`\s*\|/g)].map((result) => result[1]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function extractReadmeWorkflowTreeEntries(content) {
|
|
108
|
+
const section = extractSection(content, '## Files In This Framework', '## ');
|
|
109
|
+
const match = normalizeContent(section || '').match(/```[\s\S]*?workflows\/\n([\s\S]*?)\n\s*templates\//);
|
|
110
|
+
if (!match) return [];
|
|
111
|
+
return match[1]
|
|
112
|
+
.split('\n')
|
|
113
|
+
.map((line) => line.trim())
|
|
114
|
+
.filter((line) => line.endsWith('.md'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function extractRepoLocalPaths(content) {
|
|
118
|
+
const allowedRoots = [
|
|
119
|
+
'AGENTS.md',
|
|
120
|
+
'README.md',
|
|
121
|
+
'CHANGELOG.md',
|
|
122
|
+
'SPEC.md',
|
|
123
|
+
'package.json',
|
|
124
|
+
'.planning/',
|
|
125
|
+
'.internal-research/',
|
|
126
|
+
'.agents/',
|
|
127
|
+
'.claude/',
|
|
128
|
+
'.opencode/',
|
|
129
|
+
'.codex/',
|
|
130
|
+
'.worktrees/',
|
|
131
|
+
'bin/',
|
|
132
|
+
'distilled/',
|
|
133
|
+
'tests/',
|
|
134
|
+
'fixtures/',
|
|
135
|
+
'agents/',
|
|
136
|
+
];
|
|
137
|
+
const refs = new Set();
|
|
138
|
+
for (const result of content.matchAll(/`([^`]+)`/g)) {
|
|
139
|
+
const value = result[1].trim();
|
|
140
|
+
if (!looksLikeRepoLocalPath(value, allowedRoots)) continue;
|
|
141
|
+
refs.add(value.replace(/\\/g, '/').replace(/\/$/, ''));
|
|
142
|
+
}
|
|
143
|
+
return [...refs];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function looksLikeRepoLocalPath(value, allowedRoots) {
|
|
147
|
+
if (value.includes(' ')) return false;
|
|
148
|
+
if (value.startsWith('/')) return false;
|
|
149
|
+
if (value.startsWith('$')) return false;
|
|
150
|
+
if (value.startsWith('..')) return false;
|
|
151
|
+
if (value.includes('://')) return false;
|
|
152
|
+
if (value.startsWith('feat/') || value.startsWith('fix/') || value.startsWith('pr') || value.startsWith('origin/')) return false;
|
|
153
|
+
if (allowedRoots.some((root) => value === root || value.startsWith(root))) return true;
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function extractSection(content, startMarker, endMarker) {
|
|
158
|
+
const normalized = normalizeContent(content);
|
|
159
|
+
const start = normalized.indexOf(startMarker);
|
|
160
|
+
if (start === -1) return '';
|
|
161
|
+
const rest = normalized.slice(start);
|
|
162
|
+
if (!endMarker) return rest;
|
|
163
|
+
const end = rest.indexOf(endMarker, startMarker.length);
|
|
164
|
+
return end === -1 ? rest : rest.slice(0, end);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeContent(content) {
|
|
168
|
+
return content.replace(/\r\n/g, '\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function extractCheckedRequirements(content) {
|
|
172
|
+
const checked = new Set();
|
|
173
|
+
for (const result of content.matchAll(/- \[x\] \*\*\[([A-Z0-9-]+)]\*\*/g)) {
|
|
174
|
+
checked.add(result[1]);
|
|
175
|
+
}
|
|
176
|
+
return checked;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function extractRoadmapRequirements(content) {
|
|
180
|
+
const mapped = new Map();
|
|
181
|
+
for (const result of content.matchAll(/- \[([ x-])] \*\*Phase\s+\d+[a-z]?:.*?\*\*\s+—\s+\[([^\]]+)]/g)) {
|
|
182
|
+
const requirementIds = result[2].split(',').map((id) => id.trim());
|
|
183
|
+
for (const requirementId of requirementIds) {
|
|
184
|
+
mapped.set(requirementId, result[1].toLowerCase() === 'x');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return mapped;
|
|
188
|
+
}
|
package/bin/lib/health.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { readManifest, detectModifications } from './manifest.mjs';
|
|
9
9
|
import { output } from './cli-utils.mjs';
|
|
10
|
+
import { runTruthChecks, TRUTH_CHECK_IDS } from './health-truth.mjs';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Factory function returning the health command.
|
|
@@ -17,6 +18,8 @@ export function createCmdHealth(ctx) {
|
|
|
17
18
|
const jsonMode = healthArgs.includes('--json');
|
|
18
19
|
const cwd = process.cwd();
|
|
19
20
|
const planningDir = join(cwd, '.planning');
|
|
21
|
+
const frameworkSourceMode = isFrameworkSourceRepo(cwd);
|
|
22
|
+
const healthCheckIds = ['E1', 'E2', 'E3', 'E4', 'E5', 'E6', 'E7', 'E8', 'W1', 'W2', 'W3', 'W4', 'W5', 'W6', ...TRUTH_CHECK_IDS, 'I1', 'I2', 'I3'];
|
|
20
23
|
|
|
21
24
|
// Pre-init guard
|
|
22
25
|
if (!existsSync(join(planningDir, 'config.json'))) {
|
|
@@ -56,10 +59,11 @@ export function createCmdHealth(ctx) {
|
|
|
56
59
|
const delegatesDir = join(templatesDir, 'delegates');
|
|
57
60
|
const hasRolesDir = hasTemplatesDir && existsSync(rolesDir);
|
|
58
61
|
const hasDelegatesDir = hasTemplatesDir && existsSync(delegatesDir);
|
|
62
|
+
const skipInstalledTemplateChecks = !hasTemplatesDir && frameworkSourceMode;
|
|
59
63
|
|
|
60
|
-
if (!hasTemplatesDir) {
|
|
64
|
+
if (!hasTemplatesDir && !skipInstalledTemplateChecks) {
|
|
61
65
|
errors.push({ id: 'E3', severity: 'ERROR', message: '.planning/templates/ missing', fix: 'Run `gsdd update --templates`' });
|
|
62
|
-
} else {
|
|
66
|
+
} else if (hasTemplatesDir) {
|
|
63
67
|
// E4: roles/ missing or empty
|
|
64
68
|
if (!hasRolesDir) {
|
|
65
69
|
errors.push({ id: 'E4', severity: 'ERROR', message: '.planning/templates/roles/ missing', fix: 'Run `gsdd update --templates`' });
|
|
@@ -79,13 +83,42 @@ export function createCmdHealth(ctx) {
|
|
|
79
83
|
errors.push({ id: 'E5', severity: 'ERROR', message: '.planning/templates/delegates/ has 0 delegate files', fix: 'Run `gsdd update --templates`' });
|
|
80
84
|
}
|
|
81
85
|
}
|
|
86
|
+
|
|
87
|
+
// E6: research/ missing or empty
|
|
88
|
+
const researchDir = join(templatesDir, 'research');
|
|
89
|
+
if (!existsSync(researchDir)) {
|
|
90
|
+
errors.push({ id: 'E6', severity: 'ERROR', message: '.planning/templates/research/ missing', fix: 'Run `gsdd update --templates`' });
|
|
91
|
+
} else {
|
|
92
|
+
const researchFiles = readdirSync(researchDir).filter((f) => f.endsWith('.md'));
|
|
93
|
+
if (researchFiles.length === 0) {
|
|
94
|
+
errors.push({ id: 'E6', severity: 'ERROR', message: '.planning/templates/research/ has 0 template files', fix: 'Run `gsdd update --templates`' });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// E7: codebase/ missing or empty
|
|
99
|
+
const codebaseDir = join(templatesDir, 'codebase');
|
|
100
|
+
if (!existsSync(codebaseDir)) {
|
|
101
|
+
errors.push({ id: 'E7', severity: 'ERROR', message: '.planning/templates/codebase/ missing', fix: 'Run `gsdd update --templates`' });
|
|
102
|
+
} else {
|
|
103
|
+
const codebaseFiles = readdirSync(codebaseDir).filter((f) => f.endsWith('.md'));
|
|
104
|
+
if (codebaseFiles.length === 0) {
|
|
105
|
+
errors.push({ id: 'E7', severity: 'ERROR', message: '.planning/templates/codebase/ has 0 template files', fix: 'Run `gsdd update --templates`' });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// E8: critical root template files missing
|
|
110
|
+
const requiredRootFiles = ['spec.md', 'roadmap.md', 'auth-matrix.md'];
|
|
111
|
+
const missingRoot = requiredRootFiles.filter((f) => !existsSync(join(templatesDir, f)));
|
|
112
|
+
if (missingRoot.length > 0) {
|
|
113
|
+
errors.push({ id: 'E8', severity: 'ERROR', message: `.planning/templates/ missing critical root files: ${missingRoot.join(', ')}`, fix: 'Run `gsdd update --templates`' });
|
|
114
|
+
}
|
|
82
115
|
}
|
|
83
116
|
|
|
84
117
|
// --- WARNING checks ---
|
|
85
118
|
|
|
86
119
|
// W1: generation-manifest.json missing
|
|
87
|
-
const manifest = readManifest(planningDir);
|
|
88
|
-
if (!manifest) {
|
|
120
|
+
const manifest = skipInstalledTemplateChecks ? null : readManifest(planningDir);
|
|
121
|
+
if (!manifest && !skipInstalledTemplateChecks) {
|
|
89
122
|
warnings.push({ id: 'W1', severity: 'WARN', message: 'generation-manifest.json missing', fix: 'Run `gsdd update --templates` to create' });
|
|
90
123
|
}
|
|
91
124
|
|
|
@@ -118,16 +151,25 @@ export function createCmdHealth(ctx) {
|
|
|
118
151
|
const phaseArtifacts = existsSync(phasesDir) ? listPhaseArtifacts(phasesDir) : [];
|
|
119
152
|
|
|
120
153
|
if (roadmap && existsSync(phasesDir)) {
|
|
121
|
-
const
|
|
154
|
+
const phaseLabels = [];
|
|
155
|
+
let inDetails = false;
|
|
122
156
|
for (const line of roadmap.split('\n')) {
|
|
123
|
-
|
|
124
|
-
|
|
157
|
+
if (line.includes('<details>') && !line.includes('</details>')) {
|
|
158
|
+
inDetails = true;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (line.includes('</details>')) {
|
|
162
|
+
inDetails = false;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (inDetails) continue;
|
|
166
|
+
const match = line.match(/^\s*[-*]\s*\[([x-])\]\s*\*\*Phase\s+(\d+[a-z]?)/i);
|
|
167
|
+
if (match) phaseLabels.push(normalizePhaseLabel(match[2]));
|
|
125
168
|
}
|
|
126
|
-
for (const
|
|
127
|
-
const
|
|
128
|
-
const hasFile = phaseArtifacts.some((artifact) => artifact.phasePrefix === padded || artifact.phasePrefix === String(num));
|
|
169
|
+
for (const label of phaseLabels) {
|
|
170
|
+
const hasFile = phaseArtifacts.some((artifact) => normalizePhaseLabel(artifact.phasePrefix) === label);
|
|
129
171
|
if (!hasFile) {
|
|
130
|
-
warnings.push({ id: 'W4', severity: 'WARN', message: `ROADMAP.md references Phase ${
|
|
172
|
+
warnings.push({ id: 'W4', severity: 'WARN', message: `ROADMAP.md references active Phase ${label} but no files found in .planning/phases/`, fix: 'Create missing phase dirs or update ROADMAP' });
|
|
131
173
|
}
|
|
132
174
|
}
|
|
133
175
|
}
|
|
@@ -160,6 +202,8 @@ export function createCmdHealth(ctx) {
|
|
|
160
202
|
warnings.push({ id: 'W6', severity: 'WARN', message: 'No adapter surfaces detected', fix: 'Run `gsdd init --tools <platform>`' });
|
|
161
203
|
}
|
|
162
204
|
|
|
205
|
+
warnings.push(...runTruthChecks(planningDir, cwd, healthCheckIds));
|
|
206
|
+
|
|
163
207
|
// --- INFO checks ---
|
|
164
208
|
|
|
165
209
|
// I1: generation manifest was produced by a different framework version
|
|
@@ -220,6 +264,15 @@ export function createCmdHealth(ctx) {
|
|
|
220
264
|
};
|
|
221
265
|
}
|
|
222
266
|
|
|
267
|
+
function isFrameworkSourceRepo(cwd) {
|
|
268
|
+
return existsSync(join(cwd, 'distilled', 'templates')) && existsSync(join(cwd, 'distilled', 'workflows'));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function normalizePhaseLabel(value) {
|
|
272
|
+
if (!value) return null;
|
|
273
|
+
return value.toLowerCase().replace(/^0+(\d)/, '$1');
|
|
274
|
+
}
|
|
275
|
+
|
|
223
276
|
function listPhaseArtifacts(phasesDir) {
|
|
224
277
|
const artifacts = [];
|
|
225
278
|
for (const entry of readdirSync(phasesDir, { withFileTypes: true })) {
|
|
@@ -243,6 +296,6 @@ function createPhaseArtifact(dir, name) {
|
|
|
243
296
|
dir,
|
|
244
297
|
name,
|
|
245
298
|
displayPath: dir ? `${dir}/${name}` : name,
|
|
246
|
-
phasePrefix: name.match(/^(\d+(?:\.\d+)?)-/)?.[1] || dir.match(/^(\d+(?:\.\d+)?)-/)?.[1] || null,
|
|
299
|
+
phasePrefix: name.match(/^(\d+[a-z]?(?:\.\d+)?)-/i)?.[1] || dir.match(/^(\d+[a-z]?(?:\.\d+)?)-/i)?.[1] || null,
|
|
247
300
|
};
|
|
248
301
|
}
|