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/phase.mjs
CHANGED
|
@@ -7,6 +7,24 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from
|
|
|
7
7
|
import { join, basename } from 'path';
|
|
8
8
|
import { output } from './cli-utils.mjs';
|
|
9
9
|
|
|
10
|
+
const PHASE_STATUS_MARKERS = {
|
|
11
|
+
not_started: '[ ]',
|
|
12
|
+
todo: '[ ]',
|
|
13
|
+
in_progress: '[-]',
|
|
14
|
+
done: '[x]',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const PHASE_MARKER_RE = '(\\[[ x]\\]|\\[-\\]|⬜|ðŸ"„|✅|⬜|🔄|✅)';
|
|
18
|
+
const PHASE_TOKEN_RE = '(\\d+(?:\\.\\d+)*[a-z]?)';
|
|
19
|
+
const PHASE_LINE_RE = new RegExp(
|
|
20
|
+
`^[-*]\\s*${PHASE_MARKER_RE}\\s*\\*\\*Phase\\s+${PHASE_TOKEN_RE}:\\s*(.+?)\\*\\*`,
|
|
21
|
+
'i'
|
|
22
|
+
);
|
|
23
|
+
const ROADMAP_PHASE_STATUS_RE = new RegExp(
|
|
24
|
+
`^(\\s*[-*]\\s*)${PHASE_MARKER_RE}(\\s*\\*\\*Phase\\s+${PHASE_TOKEN_RE}:.*)$`,
|
|
25
|
+
'i'
|
|
26
|
+
);
|
|
27
|
+
|
|
10
28
|
function findFiles(dir, prefix) {
|
|
11
29
|
if (!existsSync(dir)) return [];
|
|
12
30
|
return readdirSync(dir).filter((f) => f.startsWith(prefix) || f.startsWith(prefix.replace(/^0+/, '')));
|
|
@@ -20,13 +38,7 @@ function parsePhaseStatuses(roadmap) {
|
|
|
20
38
|
const phases = [];
|
|
21
39
|
const lines = roadmap.split('\n');
|
|
22
40
|
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
|
-
);
|
|
41
|
+
const match = line.match(PHASE_LINE_RE);
|
|
30
42
|
if (match) {
|
|
31
43
|
const rawStatus = match[1].toLowerCase();
|
|
32
44
|
let status = 'not_started';
|
|
@@ -34,7 +46,7 @@ function parsePhaseStatuses(roadmap) {
|
|
|
34
46
|
else if (rawStatus === '[-]') status = 'in_progress';
|
|
35
47
|
else if (rawStatus === 'ðÿ"„' || rawStatus === '🔄') status = 'in_progress';
|
|
36
48
|
phases.push({
|
|
37
|
-
number:
|
|
49
|
+
number: match[2],
|
|
38
50
|
name: match[3].replace(/\*\*/g, '').split('-')[0].trim(),
|
|
39
51
|
status,
|
|
40
52
|
});
|
|
@@ -43,6 +55,80 @@ function parsePhaseStatuses(roadmap) {
|
|
|
43
55
|
return phases;
|
|
44
56
|
}
|
|
45
57
|
|
|
58
|
+
function normalizePhaseToken(value) {
|
|
59
|
+
const raw = String(value).trim().toLowerCase();
|
|
60
|
+
const match = raw.match(/^(\d+(?:\.\d+)*)([a-z]?)$/i);
|
|
61
|
+
if (!match) return raw;
|
|
62
|
+
|
|
63
|
+
const numericSegments = match[1]
|
|
64
|
+
.split('.')
|
|
65
|
+
.map((segment) => String(parseInt(segment, 10)));
|
|
66
|
+
return `${numericSegments.join('.')}${match[2] || ''}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function updateRoadmapPhaseStatus(roadmap, phaseNumber, status) {
|
|
70
|
+
const marker = PHASE_STATUS_MARKERS[status];
|
|
71
|
+
if (!marker) {
|
|
72
|
+
throw new Error(`Unsupported phase status: ${status}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const normalizedTarget = normalizePhaseToken(phaseNumber);
|
|
76
|
+
let matchCount = 0;
|
|
77
|
+
|
|
78
|
+
const updated = roadmap
|
|
79
|
+
.split('\n')
|
|
80
|
+
.map((line) => {
|
|
81
|
+
const match = line.match(ROADMAP_PHASE_STATUS_RE);
|
|
82
|
+
if (!match) return line;
|
|
83
|
+
if (normalizePhaseToken(match[4]) !== normalizedTarget) return line;
|
|
84
|
+
matchCount += 1;
|
|
85
|
+
return `${match[1]}${marker}${match[3]}`;
|
|
86
|
+
})
|
|
87
|
+
.join('\n');
|
|
88
|
+
|
|
89
|
+
if (matchCount === 0) {
|
|
90
|
+
throw new Error(`Phase ${phaseNumber} was not found in ROADMAP.md`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (matchCount > 1) {
|
|
94
|
+
throw new Error(`Phase ${phaseNumber} matched multiple ROADMAP.md entries`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return updated;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function cmdPhaseStatus(...args) {
|
|
101
|
+
const cwd = process.cwd();
|
|
102
|
+
const planningDir = join(cwd, '.planning');
|
|
103
|
+
const roadmapPath = join(planningDir, 'ROADMAP.md');
|
|
104
|
+
const [phaseNumber, status] = args;
|
|
105
|
+
|
|
106
|
+
if (!phaseNumber || !status) {
|
|
107
|
+
console.error('Usage: gsdd phase-status <phase-number> <not_started|todo|in_progress|done>');
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!existsSync(roadmapPath)) {
|
|
113
|
+
console.error('No ROADMAP.md found. Run the new-project workflow first.');
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const roadmap = readFileSync(roadmapPath, 'utf-8');
|
|
120
|
+
const updated = updateRoadmapPhaseStatus(roadmap, phaseNumber, status);
|
|
121
|
+
const changed = updated !== roadmap;
|
|
122
|
+
if (changed) {
|
|
123
|
+
writeFileSync(roadmapPath, updated);
|
|
124
|
+
}
|
|
125
|
+
output({ phase: phaseNumber, status, roadmap: '.planning/ROADMAP.md', changed });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(error.message);
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
46
132
|
export function cmdFindPhase(...args) {
|
|
47
133
|
const cwd = process.cwd();
|
|
48
134
|
const planningDir = join(cwd, '.planning');
|
|
@@ -67,7 +153,7 @@ export function cmdFindPhase(...args) {
|
|
|
67
153
|
const summaries = findFiles(phasesDir, `${padPhase(phaseNum)}-SUMMARY`);
|
|
68
154
|
|
|
69
155
|
output({
|
|
70
|
-
phase:
|
|
156
|
+
phase: normalizePhaseToken(phaseNum),
|
|
71
157
|
directory: phasesDir,
|
|
72
158
|
plans,
|
|
73
159
|
summaries,
|
|
@@ -99,18 +185,18 @@ export function cmdVerify(...args) {
|
|
|
99
185
|
const phaseNum = args[0];
|
|
100
186
|
if (!phaseNum) {
|
|
101
187
|
console.error('Usage: gsdd verify <phase-number>');
|
|
102
|
-
process.
|
|
188
|
+
process.exitCode = 1; return;
|
|
103
189
|
}
|
|
104
190
|
|
|
105
191
|
if (!existsSync(planningDir)) {
|
|
106
192
|
console.error('No .planning/ directory found.');
|
|
107
|
-
process.
|
|
193
|
+
process.exitCode = 1; return;
|
|
108
194
|
}
|
|
109
195
|
|
|
110
196
|
const planFile = findFiles(join(planningDir, 'phases'), `${padPhase(phaseNum)}-PLAN`)[0];
|
|
111
197
|
if (!planFile) {
|
|
112
198
|
console.error(`No plan found for phase ${phaseNum}`);
|
|
113
|
-
process.
|
|
199
|
+
process.exitCode = 1; return;
|
|
114
200
|
}
|
|
115
201
|
|
|
116
202
|
const planPath = join(planningDir, 'phases', planFile);
|
|
@@ -183,14 +269,14 @@ export function cmdScaffold(...args) {
|
|
|
183
269
|
|
|
184
270
|
if (type !== 'phase') {
|
|
185
271
|
console.error('Usage: gsdd scaffold phase <number> [name]');
|
|
186
|
-
process.
|
|
272
|
+
process.exitCode = 1; return;
|
|
187
273
|
}
|
|
188
274
|
|
|
189
275
|
const phaseNum = rest[0];
|
|
190
276
|
const phaseName = rest.slice(1).join(' ');
|
|
191
277
|
if (!phaseNum) {
|
|
192
278
|
console.error('Usage: gsdd scaffold phase <number> [name]');
|
|
193
|
-
process.
|
|
279
|
+
process.exitCode = 1; return;
|
|
194
280
|
}
|
|
195
281
|
|
|
196
282
|
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,146 @@
|
|
|
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
|
+
export function parseGitStatusShort(statusText = '') {
|
|
13
|
+
const lines = statusText
|
|
14
|
+
.replace(/\r\n/g, '\n')
|
|
15
|
+
.split('\n')
|
|
16
|
+
.map((line) => line.trimEnd())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
|
|
19
|
+
const files = [];
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const match = line.match(/^(.)(.)\s+(.+)$/);
|
|
22
|
+
if (!match) continue;
|
|
23
|
+
|
|
24
|
+
const indexStatus = match[1];
|
|
25
|
+
const worktreeStatus = match[2];
|
|
26
|
+
if (indexStatus === '!' && worktreeStatus === '!') continue;
|
|
27
|
+
|
|
28
|
+
const filePath = match[3].replace(/\\/g, '/');
|
|
29
|
+
files.push({
|
|
30
|
+
path: filePath,
|
|
31
|
+
staged: indexStatus !== ' ' && indexStatus !== '?' && indexStatus !== '!',
|
|
32
|
+
unstaged: worktreeStatus !== ' ' && worktreeStatus !== '?' && worktreeStatus !== '!',
|
|
33
|
+
untracked: indexStatus === '?' || worktreeStatus === '?',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
files,
|
|
39
|
+
stagedCount: files.filter((file) => file.staged).length,
|
|
40
|
+
unstagedCount: files.filter((file) => file.unstaged).length,
|
|
41
|
+
untrackedCount: files.filter((file) => file.untracked).length,
|
|
42
|
+
dirty: files.length > 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildProvenanceSnapshot({
|
|
47
|
+
checkpoint = {},
|
|
48
|
+
planning = {},
|
|
49
|
+
git = {},
|
|
50
|
+
} = {}) {
|
|
51
|
+
const status = parseGitStatusShort(git.statusShort || '');
|
|
52
|
+
const commitsAheadOfMain = normalizeCount(git.commitsAheadOfMain);
|
|
53
|
+
const commitsAheadOfRemote = normalizeCount(git.commitsAheadOfRemote);
|
|
54
|
+
const prState = normalizePrState(git.prState);
|
|
55
|
+
|
|
56
|
+
const warnings = [];
|
|
57
|
+
|
|
58
|
+
if (status.dirty) {
|
|
59
|
+
warnings.push({
|
|
60
|
+
id: 'dirty_worktree',
|
|
61
|
+
severity: 'warning',
|
|
62
|
+
summary: 'Local worktree contains staged, unstaged, or untracked changes.',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (commitsAheadOfMain !== 'unknown' && commitsAheadOfMain > 0) {
|
|
67
|
+
warnings.push({
|
|
68
|
+
id: 'ahead_of_main',
|
|
69
|
+
severity: 'warning',
|
|
70
|
+
summary: `${commitsAheadOfMain} commit(s) are ahead of main on the current branch.`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (commitsAheadOfRemote !== 'unknown' && commitsAheadOfRemote > 0) {
|
|
75
|
+
warnings.push({
|
|
76
|
+
id: 'unpushed_commits',
|
|
77
|
+
severity: 'warning',
|
|
78
|
+
summary: `${commitsAheadOfRemote} commit(s) are ahead of the tracked remote branch.`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (prState === 'none') {
|
|
83
|
+
warnings.push({
|
|
84
|
+
id: 'missing_pr',
|
|
85
|
+
severity: 'warning',
|
|
86
|
+
summary: 'No pull request is associated with the current branch.',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (git.staleBranch) {
|
|
91
|
+
warnings.push({
|
|
92
|
+
id: 'stale_branch',
|
|
93
|
+
severity: 'warning',
|
|
94
|
+
summary: 'The current branch is stale or spent relative to the intended integration surface.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (git.mixedScope) {
|
|
99
|
+
warnings.push({
|
|
100
|
+
id: 'mixed_scope',
|
|
101
|
+
severity: 'warning',
|
|
102
|
+
summary: 'The current worktree appears to mix multiple write scopes or phases.',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (git.materialCheckpointMismatch) {
|
|
107
|
+
warnings.push({
|
|
108
|
+
id: 'checkpoint_mismatch',
|
|
109
|
+
severity: 'acknowledgement_required',
|
|
110
|
+
summary: 'Checkpoint narrative truth materially understates or conflicts with the live branch/worktree truth.',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
checkpoint: {
|
|
116
|
+
workflow: checkpoint.workflow || 'generic',
|
|
117
|
+
phase: checkpoint.phase ?? null,
|
|
118
|
+
runtime: checkpoint.runtime || 'unknown',
|
|
119
|
+
hasNarrative: Boolean(checkpoint.hasNarrative),
|
|
120
|
+
},
|
|
121
|
+
planning: {
|
|
122
|
+
currentPhase: planning.currentPhase || null,
|
|
123
|
+
nextPhase: planning.nextPhase || null,
|
|
124
|
+
completedPhaseCount: Number.isFinite(Number(planning.completedPhaseCount))
|
|
125
|
+
? Number(planning.completedPhaseCount)
|
|
126
|
+
: 0,
|
|
127
|
+
},
|
|
128
|
+
git: {
|
|
129
|
+
branch: git.branch || 'unknown',
|
|
130
|
+
prState,
|
|
131
|
+
commitsAheadOfMain,
|
|
132
|
+
commitsAheadOfRemote,
|
|
133
|
+
stagedCount: status.stagedCount,
|
|
134
|
+
unstagedCount: status.unstagedCount,
|
|
135
|
+
untrackedCount: status.untrackedCount,
|
|
136
|
+
dirty: status.dirty,
|
|
137
|
+
},
|
|
138
|
+
integrationSurface: {
|
|
139
|
+
staleBranch: Boolean(git.staleBranch),
|
|
140
|
+
mixedScope: Boolean(git.mixedScope),
|
|
141
|
+
materialCheckpointMismatch: Boolean(git.materialCheckpointMismatch),
|
|
142
|
+
},
|
|
143
|
+
warnings,
|
|
144
|
+
requiresAcknowledgement: warnings.some((warning) => warning.severity === 'acknowledgement_required'),
|
|
145
|
+
};
|
|
146
|
+
}
|
package/bin/lib/templates.mjs
CHANGED
|
@@ -12,6 +12,23 @@ export function installProjectTemplates({ planningDir, distilledDir, agentsDir }
|
|
|
12
12
|
if (existsSync(globalTemplatesDir)) {
|
|
13
13
|
cpSync(globalTemplatesDir, localTemplatesDir, { recursive: true });
|
|
14
14
|
console.log(' - copied templates to .planning/templates/');
|
|
15
|
+
// Warn-only by design: init should not fail on missing templates because
|
|
16
|
+
// the user may still proceed and fix later. The hard gate lives in
|
|
17
|
+
// `gsdd health` (E6/E7/E8) which reports these as errors. This is the
|
|
18
|
+
// first layer of the 3-layer scaffold defense (warn at init, error at
|
|
19
|
+
// health, regression tests in manifest suite).
|
|
20
|
+
const expectedSubdirs = ['delegates', 'research', 'codebase'];
|
|
21
|
+
for (const subdir of expectedSubdirs) {
|
|
22
|
+
if (!existsSync(join(localTemplatesDir, subdir))) {
|
|
23
|
+
console.log(` - WARN: missing expected template subdir: ${subdir}/`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const expectedRootFiles = ['spec.md', 'roadmap.md', 'auth-matrix.md'];
|
|
27
|
+
for (const file of expectedRootFiles) {
|
|
28
|
+
if (!existsSync(join(localTemplatesDir, file))) {
|
|
29
|
+
console.log(` - WARN: missing expected root template file: ${file}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
15
32
|
} else {
|
|
16
33
|
console.log(' - WARN: missing distilled/templates/; cannot copy templates');
|
|
17
34
|
}
|