gsdd-cli 0.3.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.
- package/README.md +131 -67
- package/agents/DISTILLATION.md +15 -13
- package/agents/README.md +1 -1
- package/agents/planner.md +2 -0
- package/bin/adapters/agents.mjs +1 -0
- package/bin/adapters/claude.mjs +20 -4
- package/bin/adapters/codex.mjs +9 -1
- package/bin/adapters/opencode.mjs +20 -5
- package/bin/gsdd.mjs +24 -7
- package/bin/lib/cli-utils.mjs +1 -1
- package/bin/lib/evidence-contract.mjs +112 -0
- package/bin/lib/file-ops.mjs +161 -0
- package/bin/lib/health-truth.mjs +186 -0
- package/bin/lib/health.mjs +72 -67
- package/bin/lib/init-flow.mjs +50 -3
- package/bin/lib/init-prompts.mjs +22 -83
- package/bin/lib/init-runtime.mjs +47 -25
- package/bin/lib/init.mjs +3 -3
- package/bin/lib/lifecycle-preflight.mjs +333 -0
- package/bin/lib/lifecycle-state.mjs +293 -0
- package/bin/lib/models.mjs +19 -4
- package/bin/lib/phase.mjs +159 -18
- package/bin/lib/plan-constants.mjs +30 -0
- package/bin/lib/provenance.mjs +165 -0
- package/bin/lib/rendering.mjs +8 -0
- package/bin/lib/runtime-freshness.mjs +239 -0
- package/bin/lib/session-fingerprint.mjs +106 -0
- package/bin/lib/templates.mjs +17 -0
- package/distilled/DESIGN.md +733 -49
- package/distilled/EVIDENCE-INDEX.md +402 -0
- package/distilled/README.md +73 -33
- package/distilled/SKILL.md +89 -85
- package/distilled/templates/agents.block.md +13 -84
- package/distilled/templates/agents.md +0 -7
- package/distilled/templates/delegates/plan-checker.md +6 -3
- package/distilled/workflows/audit-milestone.md +56 -6
- package/distilled/workflows/complete-milestone.md +333 -0
- package/distilled/workflows/execute.md +201 -19
- package/distilled/workflows/map-codebase.md +17 -4
- package/distilled/workflows/new-milestone.md +262 -0
- package/distilled/workflows/new-project.md +7 -6
- package/distilled/workflows/pause.md +40 -6
- package/distilled/workflows/plan-milestone-gaps.md +183 -0
- package/distilled/workflows/plan.md +77 -11
- package/distilled/workflows/progress.md +107 -29
- package/distilled/workflows/quick.md +23 -12
- package/distilled/workflows/resume.md +135 -12
- package/distilled/workflows/verify-work.md +260 -0
- package/distilled/workflows/verify.md +159 -33
- package/docs/BROWNFIELD-PROOF.md +95 -0
- package/docs/RUNTIME-SUPPORT.md +77 -0
- package/docs/USER-GUIDE.md +439 -0
- package/docs/VERIFICATION-DISCIPLINE.md +59 -0
- package/docs/claude/context-monitor.md +98 -0
- package/docs/proof/consumer-node-cli/README.md +37 -0
- package/docs/proof/consumer-node-cli/ROADMAP.md +14 -0
- package/docs/proof/consumer-node-cli/SPEC.md +17 -0
- package/docs/proof/consumer-node-cli/brief.md +9 -0
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -0
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -0
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -0
- package/package.json +38 -29
package/bin/lib/phase.mjs
CHANGED
|
@@ -6,10 +6,82 @@
|
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
7
7
|
import { join, basename } from 'path';
|
|
8
8
|
import { output } from './cli-utils.mjs';
|
|
9
|
+
import { writeFingerprint } from './session-fingerprint.mjs';
|
|
10
|
+
|
|
11
|
+
const PHASE_STATUS_MARKERS = {
|
|
12
|
+
not_started: '[ ]',
|
|
13
|
+
todo: '[ ]',
|
|
14
|
+
in_progress: '[-]',
|
|
15
|
+
done: '[x]',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const PHASE_MARKER_RE = '(\\[[ x]\\]|\\[-\\]|⬜|ðŸ"„|✅|⬜|🔄|✅)';
|
|
19
|
+
const PHASE_TOKEN_RE = '(\\d+(?:\\.\\d+)*[a-z]?)';
|
|
20
|
+
const PHASE_LINE_RE = new RegExp(
|
|
21
|
+
`^[-*]\\s*${PHASE_MARKER_RE}\\s*\\*\\*Phase\\s+${PHASE_TOKEN_RE}:\\s*(.+?)\\*\\*`,
|
|
22
|
+
'i'
|
|
23
|
+
);
|
|
24
|
+
const ROADMAP_PHASE_STATUS_RE = new RegExp(
|
|
25
|
+
`^(\\s*[-*]\\s*)${PHASE_MARKER_RE}(\\s*\\*\\*Phase\\s+${PHASE_TOKEN_RE}:.*)$`,
|
|
26
|
+
'i'
|
|
27
|
+
);
|
|
9
28
|
|
|
10
29
|
function findFiles(dir, prefix) {
|
|
11
30
|
if (!existsSync(dir)) return [];
|
|
12
|
-
|
|
31
|
+
|
|
32
|
+
const phaseArtifactPrefix = String(prefix).match(/^(\d+(?:\.\d+)*[a-z]?)-(PLAN|SUMMARY)$/i);
|
|
33
|
+
if (!phaseArtifactPrefix) {
|
|
34
|
+
return readdirSync(dir).filter((f) => f.startsWith(prefix) || f.startsWith(prefix.replace(/^0+/, '')));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const targetPhase = normalizePhaseToken(phaseArtifactPrefix[1]);
|
|
38
|
+
const targetKind = phaseArtifactPrefix[2].toUpperCase();
|
|
39
|
+
|
|
40
|
+
return listPhaseArtifacts(dir)
|
|
41
|
+
.filter((artifact) => artifact.phaseToken === targetPhase && artifact.kind === targetKind)
|
|
42
|
+
.map((artifact) => artifact.displayPath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function listPhaseArtifacts(dir) {
|
|
46
|
+
if (!existsSync(dir)) return [];
|
|
47
|
+
|
|
48
|
+
const artifacts = [];
|
|
49
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
50
|
+
if (entry.isFile()) {
|
|
51
|
+
const artifact = classifyPhaseArtifact('', entry.name);
|
|
52
|
+
if (artifact) artifacts.push(artifact);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!entry.isDirectory()) continue;
|
|
57
|
+
|
|
58
|
+
const entryPath = join(dir, entry.name);
|
|
59
|
+
for (const child of readdirSync(entryPath, { withFileTypes: true })) {
|
|
60
|
+
if (!child.isFile()) continue;
|
|
61
|
+
const artifact = classifyPhaseArtifact(entry.name, child.name);
|
|
62
|
+
if (artifact) artifacts.push(artifact);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return artifacts;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function classifyPhaseArtifact(dir, name) {
|
|
70
|
+
const dirMatch = dir ? dir.match(/^(\d+(?:\.\d+)*[a-z]?)-/i) : null;
|
|
71
|
+
const nameMatch = name.match(/^(\d+(?:\.\d+)*[a-z]?)/i);
|
|
72
|
+
const phaseToken = normalizePhaseToken((dirMatch || nameMatch)?.[1] || '');
|
|
73
|
+
|
|
74
|
+
let kind = 'OTHER';
|
|
75
|
+
if (name.includes('PLAN')) kind = 'PLAN';
|
|
76
|
+
else if (name.includes('SUMMARY')) kind = 'SUMMARY';
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
dir,
|
|
80
|
+
name,
|
|
81
|
+
displayPath: dir ? `${dir}/${name}` : name,
|
|
82
|
+
phaseToken,
|
|
83
|
+
kind,
|
|
84
|
+
};
|
|
13
85
|
}
|
|
14
86
|
|
|
15
87
|
function padPhase(n) {
|
|
@@ -20,13 +92,7 @@ function parsePhaseStatuses(roadmap) {
|
|
|
20
92
|
const phases = [];
|
|
21
93
|
const lines = roadmap.split('\n');
|
|
22
94
|
for (const line of lines) {
|
|
23
|
-
|
|
24
|
-
// - checkbox statuses: [ ] / [x]
|
|
25
|
-
// - legacy emoji markers in ROADMAP templates: not started / in progress / done
|
|
26
|
-
// - mojibake-encoded variants that exist in some files
|
|
27
|
-
const match = line.match(
|
|
28
|
-
/^[-*]\s*(\[[ x]\]|\[-\]|⬜|ðŸ"„|✅|⬜|🔄|✅)\s*\*\*Phase\s+(\d+):\s*(.+?)\*\*/i
|
|
29
|
-
);
|
|
95
|
+
const match = line.match(PHASE_LINE_RE);
|
|
30
96
|
if (match) {
|
|
31
97
|
const rawStatus = match[1].toLowerCase();
|
|
32
98
|
let status = 'not_started';
|
|
@@ -34,7 +100,7 @@ function parsePhaseStatuses(roadmap) {
|
|
|
34
100
|
else if (rawStatus === '[-]') status = 'in_progress';
|
|
35
101
|
else if (rawStatus === 'ðÿ"„' || rawStatus === '🔄') status = 'in_progress';
|
|
36
102
|
phases.push({
|
|
37
|
-
number:
|
|
103
|
+
number: match[2],
|
|
38
104
|
name: match[3].replace(/\*\*/g, '').split('-')[0].trim(),
|
|
39
105
|
status,
|
|
40
106
|
});
|
|
@@ -43,6 +109,81 @@ function parsePhaseStatuses(roadmap) {
|
|
|
43
109
|
return phases;
|
|
44
110
|
}
|
|
45
111
|
|
|
112
|
+
function normalizePhaseToken(value) {
|
|
113
|
+
const raw = String(value).trim().toLowerCase();
|
|
114
|
+
const match = raw.match(/^(\d+(?:\.\d+)*)([a-z]?)$/i);
|
|
115
|
+
if (!match) return raw;
|
|
116
|
+
|
|
117
|
+
const numericSegments = match[1]
|
|
118
|
+
.split('.')
|
|
119
|
+
.map((segment) => String(parseInt(segment, 10)));
|
|
120
|
+
return `${numericSegments.join('.')}${match[2] || ''}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function updateRoadmapPhaseStatus(roadmap, phaseNumber, status) {
|
|
124
|
+
const marker = PHASE_STATUS_MARKERS[status];
|
|
125
|
+
if (!marker) {
|
|
126
|
+
throw new Error(`Unsupported phase status: ${status}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const normalizedTarget = normalizePhaseToken(phaseNumber);
|
|
130
|
+
let matchCount = 0;
|
|
131
|
+
|
|
132
|
+
const updated = roadmap
|
|
133
|
+
.split('\n')
|
|
134
|
+
.map((line) => {
|
|
135
|
+
const match = line.match(ROADMAP_PHASE_STATUS_RE);
|
|
136
|
+
if (!match) return line;
|
|
137
|
+
if (normalizePhaseToken(match[4]) !== normalizedTarget) return line;
|
|
138
|
+
matchCount += 1;
|
|
139
|
+
return `${match[1]}${marker}${match[3]}`;
|
|
140
|
+
})
|
|
141
|
+
.join('\n');
|
|
142
|
+
|
|
143
|
+
if (matchCount === 0) {
|
|
144
|
+
throw new Error(`Phase ${phaseNumber} was not found in ROADMAP.md`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (matchCount > 1) {
|
|
148
|
+
throw new Error(`Phase ${phaseNumber} matched multiple ROADMAP.md entries`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return updated;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function cmdPhaseStatus(...args) {
|
|
155
|
+
const cwd = process.cwd();
|
|
156
|
+
const planningDir = join(cwd, '.planning');
|
|
157
|
+
const roadmapPath = join(planningDir, 'ROADMAP.md');
|
|
158
|
+
const [phaseNumber, status] = args;
|
|
159
|
+
|
|
160
|
+
if (!phaseNumber || !status) {
|
|
161
|
+
console.error('Usage: gsdd phase-status <phase-number> <not_started|todo|in_progress|done>');
|
|
162
|
+
process.exitCode = 1;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!existsSync(roadmapPath)) {
|
|
167
|
+
console.error('No ROADMAP.md found. Run the new-project workflow first.');
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const roadmap = readFileSync(roadmapPath, 'utf-8');
|
|
174
|
+
const updated = updateRoadmapPhaseStatus(roadmap, phaseNumber, status);
|
|
175
|
+
const changed = updated !== roadmap;
|
|
176
|
+
if (changed) {
|
|
177
|
+
writeFileSync(roadmapPath, updated);
|
|
178
|
+
try { writeFingerprint(planningDir); } catch { /* best-effort */ }
|
|
179
|
+
}
|
|
180
|
+
output({ phase: phaseNumber, status, roadmap: '.planning/ROADMAP.md', changed });
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error(error.message);
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
46
187
|
export function cmdFindPhase(...args) {
|
|
47
188
|
const cwd = process.cwd();
|
|
48
189
|
const planningDir = join(cwd, '.planning');
|
|
@@ -67,7 +208,7 @@ export function cmdFindPhase(...args) {
|
|
|
67
208
|
const summaries = findFiles(phasesDir, `${padPhase(phaseNum)}-SUMMARY`);
|
|
68
209
|
|
|
69
210
|
output({
|
|
70
|
-
phase:
|
|
211
|
+
phase: normalizePhaseToken(phaseNum),
|
|
71
212
|
directory: phasesDir,
|
|
72
213
|
plans,
|
|
73
214
|
summaries,
|
|
@@ -77,9 +218,9 @@ export function cmdFindPhase(...args) {
|
|
|
77
218
|
return;
|
|
78
219
|
}
|
|
79
220
|
|
|
80
|
-
const
|
|
81
|
-
const plans =
|
|
82
|
-
const summaries =
|
|
221
|
+
const allArtifacts = listPhaseArtifacts(phasesDir);
|
|
222
|
+
const plans = allArtifacts.filter((artifact) => artifact.kind === 'PLAN');
|
|
223
|
+
const summaries = allArtifacts.filter((artifact) => artifact.kind === 'SUMMARY');
|
|
83
224
|
|
|
84
225
|
const roadmap = readFileSync(roadmapPath, 'utf-8');
|
|
85
226
|
const phases = parsePhaseStatuses(roadmap);
|
|
@@ -99,18 +240,18 @@ export function cmdVerify(...args) {
|
|
|
99
240
|
const phaseNum = args[0];
|
|
100
241
|
if (!phaseNum) {
|
|
101
242
|
console.error('Usage: gsdd verify <phase-number>');
|
|
102
|
-
process.
|
|
243
|
+
process.exitCode = 1; return;
|
|
103
244
|
}
|
|
104
245
|
|
|
105
246
|
if (!existsSync(planningDir)) {
|
|
106
247
|
console.error('No .planning/ directory found.');
|
|
107
|
-
process.
|
|
248
|
+
process.exitCode = 1; return;
|
|
108
249
|
}
|
|
109
250
|
|
|
110
251
|
const planFile = findFiles(join(planningDir, 'phases'), `${padPhase(phaseNum)}-PLAN`)[0];
|
|
111
252
|
if (!planFile) {
|
|
112
253
|
console.error(`No plan found for phase ${phaseNum}`);
|
|
113
|
-
process.
|
|
254
|
+
process.exitCode = 1; return;
|
|
114
255
|
}
|
|
115
256
|
|
|
116
257
|
const planPath = join(planningDir, 'phases', planFile);
|
|
@@ -183,14 +324,14 @@ export function cmdScaffold(...args) {
|
|
|
183
324
|
|
|
184
325
|
if (type !== 'phase') {
|
|
185
326
|
console.error('Usage: gsdd scaffold phase <number> [name]');
|
|
186
|
-
process.
|
|
327
|
+
process.exitCode = 1; return;
|
|
187
328
|
}
|
|
188
329
|
|
|
189
330
|
const phaseNum = rest[0];
|
|
190
331
|
const phaseName = rest.slice(1).join(' ');
|
|
191
332
|
if (!phaseNum) {
|
|
192
333
|
console.error('Usage: gsdd scaffold phase <number> [name]');
|
|
193
|
-
process.
|
|
334
|
+
process.exitCode = 1; return;
|
|
194
335
|
}
|
|
195
336
|
|
|
196
337
|
const phasesDir = join(planningDir, 'phases');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const PLAN_CHECK_DIMENSIONS = [
|
|
2
|
+
'requirement_coverage',
|
|
3
|
+
'task_completeness',
|
|
4
|
+
'dependency_correctness',
|
|
5
|
+
'key_link_completeness',
|
|
6
|
+
'scope_sanity',
|
|
7
|
+
'must_have_quality',
|
|
8
|
+
'context_compliance',
|
|
9
|
+
'goal_achievement',
|
|
10
|
+
'approach_alignment',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export const MAX_CHECKER_CYCLES = 3;
|
|
14
|
+
|
|
15
|
+
export const CHECKER_STATUSES = ['passed', 'issues_found'];
|
|
16
|
+
|
|
17
|
+
export const CHECKER_JSON_SCHEMA = {
|
|
18
|
+
status: 'passed | issues_found',
|
|
19
|
+
summary: 'string',
|
|
20
|
+
issues: [
|
|
21
|
+
{
|
|
22
|
+
dimension: 'string',
|
|
23
|
+
severity: 'blocker | warning',
|
|
24
|
+
description: 'string',
|
|
25
|
+
plan: 'string',
|
|
26
|
+
task: 'string',
|
|
27
|
+
fix_hint: 'string',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
function normalizePrState(prState) {
|
|
2
|
+
if (!prState) return 'none';
|
|
3
|
+
return String(prState).trim().toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function normalizeCount(value) {
|
|
7
|
+
if (value === 'unknown' || value === null || value === undefined) return 'unknown';
|
|
8
|
+
const numeric = Number(value);
|
|
9
|
+
return Number.isFinite(numeric) ? numeric : 'unknown';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeCheckpointWorkflow(workflow) {
|
|
13
|
+
const normalized = String(workflow || 'generic').trim().toLowerCase();
|
|
14
|
+
return ['phase', 'quick', 'generic'].includes(normalized) ? normalized : 'generic';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function classifyCheckpointRouting(workflow) {
|
|
18
|
+
const normalizedWorkflow = normalizeCheckpointWorkflow(workflow);
|
|
19
|
+
const progressBlocks = normalizedWorkflow === 'phase' || normalizedWorkflow === 'quick';
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
workflow: normalizedWorkflow,
|
|
23
|
+
routingClass: progressBlocks ? 'blocking' : 'informational',
|
|
24
|
+
progressBlocks,
|
|
25
|
+
resumeOwnsCleanup: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseGitStatusShort(statusText = '') {
|
|
30
|
+
const lines = statusText
|
|
31
|
+
.replace(/\r\n/g, '\n')
|
|
32
|
+
.split('\n')
|
|
33
|
+
.map((line) => line.trimEnd())
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
|
|
36
|
+
const files = [];
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
const match = line.match(/^(.)(.)\s+(.+)$/);
|
|
39
|
+
if (!match) continue;
|
|
40
|
+
|
|
41
|
+
const indexStatus = match[1];
|
|
42
|
+
const worktreeStatus = match[2];
|
|
43
|
+
if (indexStatus === '!' && worktreeStatus === '!') continue;
|
|
44
|
+
|
|
45
|
+
const filePath = match[3].replace(/\\/g, '/');
|
|
46
|
+
files.push({
|
|
47
|
+
path: filePath,
|
|
48
|
+
staged: indexStatus !== ' ' && indexStatus !== '?' && indexStatus !== '!',
|
|
49
|
+
unstaged: worktreeStatus !== ' ' && worktreeStatus !== '?' && worktreeStatus !== '!',
|
|
50
|
+
untracked: indexStatus === '?' || worktreeStatus === '?',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
files,
|
|
56
|
+
stagedCount: files.filter((file) => file.staged).length,
|
|
57
|
+
unstagedCount: files.filter((file) => file.unstaged).length,
|
|
58
|
+
untrackedCount: files.filter((file) => file.untracked).length,
|
|
59
|
+
dirty: files.length > 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildProvenanceSnapshot({
|
|
64
|
+
checkpoint = {},
|
|
65
|
+
planning = {},
|
|
66
|
+
git = {},
|
|
67
|
+
} = {}) {
|
|
68
|
+
const checkpointRouting = classifyCheckpointRouting(checkpoint.workflow);
|
|
69
|
+
const status = parseGitStatusShort(git.statusShort || '');
|
|
70
|
+
const commitsAheadOfMain = normalizeCount(git.commitsAheadOfMain);
|
|
71
|
+
const commitsAheadOfRemote = normalizeCount(git.commitsAheadOfRemote);
|
|
72
|
+
const prState = normalizePrState(git.prState);
|
|
73
|
+
|
|
74
|
+
const warnings = [];
|
|
75
|
+
|
|
76
|
+
if (status.dirty) {
|
|
77
|
+
warnings.push({
|
|
78
|
+
id: 'dirty_worktree',
|
|
79
|
+
severity: 'warning',
|
|
80
|
+
summary: 'Local worktree contains staged, unstaged, or untracked changes.',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (commitsAheadOfMain !== 'unknown' && commitsAheadOfMain > 0) {
|
|
85
|
+
warnings.push({
|
|
86
|
+
id: 'ahead_of_main',
|
|
87
|
+
severity: 'warning',
|
|
88
|
+
summary: `${commitsAheadOfMain} commit(s) are ahead of main on the current branch.`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (commitsAheadOfRemote !== 'unknown' && commitsAheadOfRemote > 0) {
|
|
93
|
+
warnings.push({
|
|
94
|
+
id: 'unpushed_commits',
|
|
95
|
+
severity: 'warning',
|
|
96
|
+
summary: `${commitsAheadOfRemote} commit(s) are ahead of the tracked remote branch.`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (prState === 'none') {
|
|
101
|
+
warnings.push({
|
|
102
|
+
id: 'missing_pr',
|
|
103
|
+
severity: 'warning',
|
|
104
|
+
summary: 'No pull request is associated with the current branch.',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (git.staleBranch) {
|
|
109
|
+
warnings.push({
|
|
110
|
+
id: 'stale_branch',
|
|
111
|
+
severity: 'warning',
|
|
112
|
+
summary: 'The current branch is stale or spent relative to the intended integration surface.',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (git.mixedScope) {
|
|
117
|
+
warnings.push({
|
|
118
|
+
id: 'mixed_scope',
|
|
119
|
+
severity: 'warning',
|
|
120
|
+
summary: 'The current worktree appears to mix multiple write scopes or phases.',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (git.materialCheckpointMismatch) {
|
|
125
|
+
warnings.push({
|
|
126
|
+
id: 'checkpoint_mismatch',
|
|
127
|
+
severity: 'acknowledgement_required',
|
|
128
|
+
summary: 'Checkpoint narrative truth materially understates or conflicts with the live branch/worktree truth.',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
checkpoint: {
|
|
134
|
+
workflow: checkpointRouting.workflow,
|
|
135
|
+
phase: checkpoint.phase ?? null,
|
|
136
|
+
runtime: checkpoint.runtime || 'unknown',
|
|
137
|
+
hasNarrative: Boolean(checkpoint.hasNarrative),
|
|
138
|
+
routing: checkpointRouting,
|
|
139
|
+
},
|
|
140
|
+
planning: {
|
|
141
|
+
currentPhase: planning.currentPhase || null,
|
|
142
|
+
nextPhase: planning.nextPhase || null,
|
|
143
|
+
completedPhaseCount: Number.isFinite(Number(planning.completedPhaseCount))
|
|
144
|
+
? Number(planning.completedPhaseCount)
|
|
145
|
+
: 0,
|
|
146
|
+
},
|
|
147
|
+
git: {
|
|
148
|
+
branch: git.branch || 'unknown',
|
|
149
|
+
prState,
|
|
150
|
+
commitsAheadOfMain,
|
|
151
|
+
commitsAheadOfRemote,
|
|
152
|
+
stagedCount: status.stagedCount,
|
|
153
|
+
unstagedCount: status.unstagedCount,
|
|
154
|
+
untrackedCount: status.untrackedCount,
|
|
155
|
+
dirty: status.dirty,
|
|
156
|
+
},
|
|
157
|
+
integrationSurface: {
|
|
158
|
+
staleBranch: Boolean(git.staleBranch),
|
|
159
|
+
mixedScope: Boolean(git.mixedScope),
|
|
160
|
+
materialCheckpointMismatch: Boolean(git.materialCheckpointMismatch),
|
|
161
|
+
},
|
|
162
|
+
warnings,
|
|
163
|
+
requiresAcknowledgement: warnings.some((warning) => warning.severity === 'acknowledgement_required'),
|
|
164
|
+
};
|
|
165
|
+
}
|
package/bin/lib/rendering.mjs
CHANGED
|
@@ -30,6 +30,13 @@ agent: ${workflow.agent}
|
|
|
30
30
|
${workflowContent}`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function buildPortableSkillEntries(workflows) {
|
|
34
|
+
return workflows.map((workflow) => ({
|
|
35
|
+
relativePath: `.agents/skills/${workflow.name}/SKILL.md`,
|
|
36
|
+
content: renderSkillContent(workflow),
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
function renderOpenCodeCommandContent(workflow) {
|
|
34
41
|
const workflowContent = getWorkflowContent(workflow.workflow);
|
|
35
42
|
return `---
|
|
@@ -85,6 +92,7 @@ function upsertBoundedBlock(existing, blockContent) {
|
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
export {
|
|
95
|
+
buildPortableSkillEntries,
|
|
88
96
|
getDelegateContent,
|
|
89
97
|
getWorkflowContent,
|
|
90
98
|
renderAgentsBoundedBlock,
|
|
@@ -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
|
+
}
|