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.
Files changed (62) hide show
  1. package/README.md +131 -67
  2. package/agents/DISTILLATION.md +15 -13
  3. package/agents/README.md +1 -1
  4. package/agents/planner.md +2 -0
  5. package/bin/adapters/agents.mjs +1 -0
  6. package/bin/adapters/claude.mjs +20 -4
  7. package/bin/adapters/codex.mjs +9 -1
  8. package/bin/adapters/opencode.mjs +20 -5
  9. package/bin/gsdd.mjs +24 -7
  10. package/bin/lib/cli-utils.mjs +1 -1
  11. package/bin/lib/evidence-contract.mjs +112 -0
  12. package/bin/lib/file-ops.mjs +161 -0
  13. package/bin/lib/health-truth.mjs +186 -0
  14. package/bin/lib/health.mjs +72 -67
  15. package/bin/lib/init-flow.mjs +50 -3
  16. package/bin/lib/init-prompts.mjs +22 -83
  17. package/bin/lib/init-runtime.mjs +47 -25
  18. package/bin/lib/init.mjs +3 -3
  19. package/bin/lib/lifecycle-preflight.mjs +333 -0
  20. package/bin/lib/lifecycle-state.mjs +293 -0
  21. package/bin/lib/models.mjs +19 -4
  22. package/bin/lib/phase.mjs +159 -18
  23. package/bin/lib/plan-constants.mjs +30 -0
  24. package/bin/lib/provenance.mjs +165 -0
  25. package/bin/lib/rendering.mjs +8 -0
  26. package/bin/lib/runtime-freshness.mjs +239 -0
  27. package/bin/lib/session-fingerprint.mjs +106 -0
  28. package/bin/lib/templates.mjs +17 -0
  29. package/distilled/DESIGN.md +733 -49
  30. package/distilled/EVIDENCE-INDEX.md +402 -0
  31. package/distilled/README.md +73 -33
  32. package/distilled/SKILL.md +89 -85
  33. package/distilled/templates/agents.block.md +13 -84
  34. package/distilled/templates/agents.md +0 -7
  35. package/distilled/templates/delegates/plan-checker.md +6 -3
  36. package/distilled/workflows/audit-milestone.md +56 -6
  37. package/distilled/workflows/complete-milestone.md +333 -0
  38. package/distilled/workflows/execute.md +201 -19
  39. package/distilled/workflows/map-codebase.md +17 -4
  40. package/distilled/workflows/new-milestone.md +262 -0
  41. package/distilled/workflows/new-project.md +7 -6
  42. package/distilled/workflows/pause.md +40 -6
  43. package/distilled/workflows/plan-milestone-gaps.md +183 -0
  44. package/distilled/workflows/plan.md +77 -11
  45. package/distilled/workflows/progress.md +107 -29
  46. package/distilled/workflows/quick.md +23 -12
  47. package/distilled/workflows/resume.md +135 -12
  48. package/distilled/workflows/verify-work.md +260 -0
  49. package/distilled/workflows/verify.md +159 -33
  50. package/docs/BROWNFIELD-PROOF.md +95 -0
  51. package/docs/RUNTIME-SUPPORT.md +77 -0
  52. package/docs/USER-GUIDE.md +439 -0
  53. package/docs/VERIFICATION-DISCIPLINE.md +59 -0
  54. package/docs/claude/context-monitor.md +98 -0
  55. package/docs/proof/consumer-node-cli/README.md +37 -0
  56. package/docs/proof/consumer-node-cli/ROADMAP.md +14 -0
  57. package/docs/proof/consumer-node-cli/SPEC.md +17 -0
  58. package/docs/proof/consumer-node-cli/brief.md +9 -0
  59. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -0
  60. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -0
  61. package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -0
  62. package/package.json +38 -29
@@ -0,0 +1,333 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { output } from './cli-utils.mjs';
4
+ import { describeEvidenceSurface } from './evidence-contract.mjs';
5
+ import { evaluateLifecycleState, normalizePhaseToken } from './lifecycle-state.mjs';
6
+ import { checkDrift } from './session-fingerprint.mjs';
7
+
8
+ const SURFACE_POLICIES = {
9
+ progress: {
10
+ classification: 'read_only',
11
+ ownedWrites: [],
12
+ explicitLifecycleMutation: 'none',
13
+ },
14
+ execute: {
15
+ classification: 'owned_write',
16
+ ownedWrites: ['summary'],
17
+ explicitLifecycleMutation: 'phase-status',
18
+ phaseRequired: true,
19
+ },
20
+ verify: {
21
+ classification: 'owned_write',
22
+ ownedWrites: ['verification'],
23
+ explicitLifecycleMutation: 'phase-status',
24
+ phaseRequired: true,
25
+ },
26
+ 'audit-milestone': {
27
+ classification: 'owned_write',
28
+ ownedWrites: ['milestone-audit'],
29
+ explicitLifecycleMutation: 'none',
30
+ },
31
+ 'complete-milestone': {
32
+ classification: 'owned_write',
33
+ ownedWrites: ['milestone-archives', 'milestones-ledger', 'spec', 'roadmap'],
34
+ explicitLifecycleMutation: 'none',
35
+ },
36
+ 'new-milestone': {
37
+ classification: 'owned_write',
38
+ ownedWrites: ['spec', 'roadmap', 'phase-directories'],
39
+ explicitLifecycleMutation: 'none',
40
+ },
41
+ resume: {
42
+ classification: 'owned_write',
43
+ ownedWrites: ['checkpoint-cleanup'],
44
+ explicitLifecycleMutation: 'none',
45
+ },
46
+ };
47
+
48
+ export function evaluateLifecyclePreflight({
49
+ planningDir,
50
+ surface,
51
+ phaseNumber = null,
52
+ expectsMutation = 'none',
53
+ } = {}) {
54
+ if (!planningDir) {
55
+ throw new Error('planningDir is required');
56
+ }
57
+
58
+ const policy = SURFACE_POLICIES[surface];
59
+ if (!policy) {
60
+ throw new Error(`Unsupported lifecycle surface: ${surface}`);
61
+ }
62
+
63
+ const lifecycle = evaluateLifecycleState({ planningDir });
64
+ const normalizedPhase = phaseNumber ? normalizePhaseToken(phaseNumber) : null;
65
+ const checkpointPath = join(planningDir, '.continue-here.md');
66
+ const specPath = join(planningDir, 'SPEC.md');
67
+ const milestonesPath = join(planningDir, 'MILESTONES.md');
68
+ const blockers = [];
69
+
70
+ if (!existsSync(planningDir)) {
71
+ blockers.push(blocker('missing_planning_dir', '.planning/ does not exist yet.', ['.planning/']));
72
+ }
73
+
74
+ if (expectsMutation !== 'none' && expectsMutation !== policy.explicitLifecycleMutation) {
75
+ blockers.push(
76
+ blocker(
77
+ 'illegal_lifecycle_mutation',
78
+ `${surface} is classified as ${policy.classification} and cannot mutate lifecycle state via ${expectsMutation}.`,
79
+ []
80
+ )
81
+ );
82
+ }
83
+
84
+ if (policy.phaseRequired && !normalizedPhase) {
85
+ blockers.push(blocker('missing_phase_argument', `${surface} requires an explicit phase number.`, []));
86
+ }
87
+
88
+ if (normalizedPhase) {
89
+ blockers.push(...buildPhaseBlockers({ lifecycle, phaseToken: normalizedPhase, surface }));
90
+ }
91
+
92
+ if (surface === 'audit-milestone') {
93
+ blockers.push(...buildAuditBlockers(lifecycle));
94
+ }
95
+
96
+ if (surface === 'complete-milestone') {
97
+ blockers.push(...buildAuditBlockers(lifecycle, { allowArchivedBlocker: true }));
98
+ blockers.push(...buildCompletionBlockers(planningDir, lifecycle));
99
+ }
100
+
101
+ if (surface === 'new-milestone') {
102
+ if (!existsSync(specPath)) {
103
+ blockers.push(blocker('missing_spec', 'SPEC.md is required before starting a new milestone.', ['.planning/SPEC.md']));
104
+ }
105
+ if (!existsSync(milestonesPath)) {
106
+ blockers.push(blocker('missing_milestones', 'MILESTONES.md is required before starting a new milestone.', ['.planning/MILESTONES.md']));
107
+ }
108
+ if (lifecycle.currentMilestone.version && lifecycle.currentMilestone.archiveState !== 'archived') {
109
+ blockers.push(
110
+ blocker(
111
+ 'active_milestone_in_progress',
112
+ `Milestone ${lifecycle.currentMilestone.version} is still active. Archive or remove the active roadmap before starting the next milestone.`,
113
+ ['.planning/ROADMAP.md']
114
+ )
115
+ );
116
+ }
117
+ }
118
+
119
+ if (surface === 'resume' && !existsSync(checkpointPath)) {
120
+ blockers.push(blocker('missing_checkpoint', 'resume requires .planning/.continue-here.md to exist.', ['.planning/.continue-here.md']));
121
+ }
122
+
123
+ const warnings = [];
124
+
125
+ if (existsSync(planningDir)) {
126
+ const drift = checkDrift(planningDir);
127
+ if (drift.drifted) {
128
+ warnings.push({
129
+ code: 'planning_state_drift',
130
+ message: `Planning state has drifted since the last recorded session: ${drift.details.join('; ')}`,
131
+ artifacts: ['.planning/ROADMAP.md', '.planning/SPEC.md', '.planning/config.json'],
132
+ });
133
+ }
134
+ }
135
+
136
+ return {
137
+ surface,
138
+ phase: normalizedPhase,
139
+ classification: policy.classification,
140
+ ownedWrites: policy.ownedWrites,
141
+ explicitLifecycleMutation: policy.explicitLifecycleMutation,
142
+ closureEvidence: describeEvidenceSurface(surface),
143
+ mutationRequest: expectsMutation,
144
+ allowed: blockers.length === 0,
145
+ status: blockers.length === 0 ? 'allowed' : 'blocked',
146
+ reason: blockers[0]?.code ?? null,
147
+ blockers,
148
+ warnings,
149
+ lifecycle: {
150
+ currentMilestone: lifecycle.currentMilestone,
151
+ currentPhase: lifecycle.currentPhase ? lifecycle.currentPhase.number : null,
152
+ nextPhase: lifecycle.nextPhase ? lifecycle.nextPhase.number : null,
153
+ counts: lifecycle.counts,
154
+ },
155
+ };
156
+ }
157
+
158
+ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
159
+ const blockers = [];
160
+ const phaseEntry = lifecycle.phases.find((phase) => phase.number === phaseToken);
161
+ if (!phaseEntry) {
162
+ blockers.push(
163
+ blocker(
164
+ 'missing_phase',
165
+ `Phase ${phaseToken} was not found in the active roadmap.`,
166
+ ['.planning/ROADMAP.md']
167
+ )
168
+ );
169
+ return blockers;
170
+ }
171
+
172
+ const planArtifacts = lifecycle.phaseArtifacts.filter((artifact) => artifact.phaseToken === phaseToken && artifact.kind === 'plan');
173
+ const summaryArtifacts = lifecycle.phaseArtifacts.filter((artifact) => artifact.phaseToken === phaseToken && artifact.kind === 'summary');
174
+ const pendingPlans = planArtifacts.filter(
175
+ (artifact) => !summaryArtifacts.some((candidate) => candidate.dir === artifact.dir && candidate.baseId === artifact.baseId)
176
+ );
177
+
178
+ if (surface === 'execute') {
179
+ if (planArtifacts.length === 0) {
180
+ blockers.push(
181
+ blocker(
182
+ 'missing_plan',
183
+ `Phase ${phaseToken} cannot execute because no PLAN artifact exists.`,
184
+ ['.planning/phases/']
185
+ )
186
+ );
187
+ } else if (pendingPlans.length === 0) {
188
+ blockers.push(
189
+ blocker(
190
+ 'no_pending_plan',
191
+ `Phase ${phaseToken} has no pending PLAN artifacts left to execute.`,
192
+ planArtifacts.map((artifact) => artifact.displayPath)
193
+ )
194
+ );
195
+ }
196
+ }
197
+
198
+ if (surface === 'verify') {
199
+ if (planArtifacts.length === 0) {
200
+ blockers.push(
201
+ blocker(
202
+ 'missing_plan',
203
+ `Phase ${phaseToken} cannot be verified because no PLAN artifact exists.`,
204
+ ['.planning/phases/']
205
+ )
206
+ );
207
+ }
208
+ if (summaryArtifacts.length === 0) {
209
+ blockers.push(
210
+ blocker(
211
+ 'missing_summary',
212
+ `Phase ${phaseToken} cannot be verified because no SUMMARY artifact exists yet.`,
213
+ ['.planning/phases/']
214
+ )
215
+ );
216
+ }
217
+ }
218
+
219
+ return blockers;
220
+ }
221
+
222
+ function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
223
+ const blockers = [];
224
+ if (!lifecycle.currentMilestone.version) {
225
+ blockers.push(blocker('missing_milestone', 'No active or retained milestone could be derived from ROADMAP.md.', ['.planning/ROADMAP.md']));
226
+ return blockers;
227
+ }
228
+
229
+ if (lifecycle.currentMilestone.archiveState === 'archived') {
230
+ blockers.push(
231
+ blocker(
232
+ allowArchivedBlocker ? 'milestone_already_archived' : 'milestone_already_archived',
233
+ `Milestone ${lifecycle.currentMilestone.version} is already archived-with-ROADMAP.md evidence.`,
234
+ ['.planning/ROADMAP.md', '.planning/MILESTONES.md']
235
+ )
236
+ );
237
+ }
238
+
239
+ if (lifecycle.counts.total === 0) {
240
+ blockers.push(blocker('missing_phases', 'No active milestone phases were found in ROADMAP.md.', ['.planning/ROADMAP.md']));
241
+ } else if (lifecycle.counts.completed !== lifecycle.counts.total) {
242
+ blockers.push(
243
+ blocker(
244
+ 'incomplete_phases',
245
+ `Milestone ${lifecycle.currentMilestone.version} still has incomplete phases (${lifecycle.counts.completed}/${lifecycle.counts.total} complete).`,
246
+ ['.planning/ROADMAP.md']
247
+ )
248
+ );
249
+ }
250
+
251
+ const phasesMissingVerification = lifecycle.phases
252
+ .filter((phase) => phase.status === 'done')
253
+ .filter((phase) => !phase.artifacts.some((artifact) => artifact.kind === 'verification'))
254
+ .map((phase) => phase.number);
255
+
256
+ if (phasesMissingVerification.length > 0) {
257
+ blockers.push(
258
+ blocker(
259
+ 'missing_verification',
260
+ `Completed phases are missing VERIFICATION artifacts (${phasesMissingVerification.join(', ')}).`,
261
+ ['.planning/phases/']
262
+ )
263
+ );
264
+ }
265
+
266
+ return blockers;
267
+ }
268
+
269
+ function buildCompletionBlockers(planningDir, lifecycle) {
270
+ const auditPath = join(planningDir, `${lifecycle.currentMilestone.version}-MILESTONE-AUDIT.md`);
271
+ if (!existsSync(auditPath)) {
272
+ return [
273
+ blocker(
274
+ 'missing_milestone_audit',
275
+ `Milestone ${lifecycle.currentMilestone.version} cannot be completed without a milestone audit artifact.`,
276
+ [auditPath]
277
+ ),
278
+ ];
279
+ }
280
+
281
+ const auditContent = readFileSync(auditPath, 'utf-8');
282
+ const statusMatch = auditContent.match(/^status:\s*(.+)$/m);
283
+ if (!statusMatch || statusMatch[1].trim() !== 'passed') {
284
+ return [
285
+ blocker(
286
+ 'audit_not_passed',
287
+ `Milestone ${lifecycle.currentMilestone.version} requires a passed audit before completion.`,
288
+ [auditPath]
289
+ ),
290
+ ];
291
+ }
292
+
293
+ return [];
294
+ }
295
+
296
+ function blocker(code, message, artifacts) {
297
+ return { code, message, artifacts };
298
+ }
299
+
300
+ export function cmdLifecyclePreflight(...args) {
301
+ const cwd = process.cwd();
302
+ const planningDir = join(cwd, '.planning');
303
+ const [surface, maybePhase, ...rest] = args;
304
+
305
+ if (!surface) {
306
+ console.error('Usage: gsdd lifecycle-preflight <surface> [phase] [--expects-mutation <none|phase-status>]');
307
+ process.exitCode = 1;
308
+ return;
309
+ }
310
+
311
+ let phaseNumber = maybePhase && !maybePhase.startsWith('--') ? maybePhase : null;
312
+ let expectsMutation = 'none';
313
+
314
+ const flagArgs = phaseNumber ? rest : [maybePhase, ...rest].filter(Boolean);
315
+ for (let index = 0; index < flagArgs.length; index += 1) {
316
+ const arg = flagArgs[index];
317
+ if (arg === '--expects-mutation') {
318
+ expectsMutation = flagArgs[index + 1] ?? 'none';
319
+ index += 1;
320
+ }
321
+ }
322
+
323
+ try {
324
+ const result = evaluateLifecyclePreflight({ planningDir, surface, phaseNumber, expectsMutation });
325
+ output(result);
326
+ if (!result.allowed) {
327
+ process.exitCode = 1;
328
+ }
329
+ } catch (error) {
330
+ console.error(error.message);
331
+ process.exitCode = 1;
332
+ }
333
+ }
@@ -0,0 +1,293 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ const PHASE_LINE_RE = /^\s*[-*]\s*\[([ x-])\]\s*\*\*Phase\s+(\d+(?:\.\d+)*[a-z]?):\s*(.+?)\*\*(?:\s+—\s+\[([^\]]+)])?/i;
5
+ const ACTIVE_MILESTONE_HEADING_RE = /^###\s+(v[^\s]+)\s+(.+)$/im;
6
+ const MILESTONE_LEDGER_HEADING_RE = /^##\s+(?:✅\s+)?(v[^\s]+)\s*(?:—|-)?\s*(.*)$/i;
7
+
8
+ export function evaluateLifecycleState({ planningDir, provenance = null } = {}) {
9
+ if (!planningDir) {
10
+ throw new Error('planningDir is required');
11
+ }
12
+
13
+ const specPath = join(planningDir, 'SPEC.md');
14
+ const roadmapPath = join(planningDir, 'ROADMAP.md');
15
+ const milestonesPath = join(planningDir, 'MILESTONES.md');
16
+ const phasesDir = join(planningDir, 'phases');
17
+
18
+ const spec = readTextIfExists(specPath);
19
+ const roadmap = readTextIfExists(roadmapPath);
20
+ const milestones = readTextIfExists(milestonesPath);
21
+
22
+ const phases = parseActiveRoadmapPhases(roadmap);
23
+ const phaseArtifacts = collectPhaseArtifacts(phasesDir);
24
+ const enrichedPhases = phases.map((phase) => {
25
+ const matchingArtifacts = phaseArtifacts.filter((artifact) => artifact.phaseToken === phase.number);
26
+ const hasPlan = matchingArtifacts.some((artifact) => artifact.kind === 'plan');
27
+ const hasSummary = matchingArtifacts.some((artifact) => artifact.kind === 'summary');
28
+ return {
29
+ ...phase,
30
+ hasArtifacts: matchingArtifacts.length > 0,
31
+ hasPlan,
32
+ hasSummary,
33
+ artifacts: matchingArtifacts,
34
+ };
35
+ });
36
+
37
+ const incompletePlans = phaseArtifacts.filter((artifact) => {
38
+ if (artifact.kind !== 'plan') return false;
39
+ return !phaseArtifacts.some((candidate) =>
40
+ candidate.dir === artifact.dir &&
41
+ candidate.baseId === artifact.baseId &&
42
+ candidate.kind === 'summary'
43
+ );
44
+ });
45
+
46
+ const counts = {
47
+ total: enrichedPhases.length,
48
+ completed: enrichedPhases.filter((phase) => phase.status === 'done').length,
49
+ inProgress: enrichedPhases.filter((phase) => phase.status === 'in_progress').length,
50
+ notStarted: enrichedPhases.filter((phase) => phase.status === 'not_started').length,
51
+ };
52
+
53
+ const milestoneLedger = parseMilestoneLedger(milestones);
54
+ const currentMilestone = deriveCurrentMilestone({
55
+ roadmap,
56
+ planningDir,
57
+ milestoneLedger,
58
+ counts,
59
+ });
60
+
61
+ return {
62
+ paths: {
63
+ specPath,
64
+ roadmapPath,
65
+ milestonesPath,
66
+ phasesDir,
67
+ },
68
+ currentMilestone,
69
+ phases: enrichedPhases,
70
+ currentPhase: enrichedPhases.find((phase) => phase.status === 'in_progress') || null,
71
+ nextPhase: enrichedPhases.find((phase) => phase.status === 'not_started') || null,
72
+ counts,
73
+ phaseArtifacts,
74
+ incompletePlans,
75
+ requirementAlignment: evaluateRequirementAlignment(spec, enrichedPhases),
76
+ provenance: provenance
77
+ ? {
78
+ provided: true,
79
+ ...provenance,
80
+ }
81
+ : { provided: false },
82
+ };
83
+ }
84
+
85
+ function readTextIfExists(filePath) {
86
+ return existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
87
+ }
88
+
89
+ function normalizeContent(content) {
90
+ return String(content || '').replace(/\r\n/g, '\n');
91
+ }
92
+
93
+ export function normalizePhaseToken(value) {
94
+ const raw = String(value || '').trim().toLowerCase();
95
+ const match = raw.match(/^(\d+(?:\.\d+)*)([a-z]?)$/i);
96
+ if (!match) return raw;
97
+
98
+ const numericSegments = match[1]
99
+ .split('.')
100
+ .map((segment) => String(parseInt(segment, 10)));
101
+ return `${numericSegments.join('.')}${match[2] || ''}`;
102
+ }
103
+
104
+ function normalizePhaseStatus(marker) {
105
+ const normalized = String(marker || '').trim().toLowerCase();
106
+ if (normalized === 'x') return 'done';
107
+ if (normalized === '-') return 'in_progress';
108
+ return 'not_started';
109
+ }
110
+
111
+ function parseActiveRoadmapPhases(roadmap) {
112
+ if (!roadmap) return [];
113
+
114
+ const phases = [];
115
+ let inDetails = false;
116
+
117
+ for (const line of normalizeContent(roadmap).split('\n')) {
118
+ if (line.includes('<details>') && !line.includes('</details>')) {
119
+ inDetails = true;
120
+ continue;
121
+ }
122
+ if (line.includes('</details>')) {
123
+ inDetails = false;
124
+ continue;
125
+ }
126
+ if (inDetails) continue;
127
+
128
+ const match = line.match(PHASE_LINE_RE);
129
+ if (!match) continue;
130
+
131
+ phases.push({
132
+ number: normalizePhaseToken(match[2]),
133
+ title: match[3].trim(),
134
+ status: normalizePhaseStatus(match[1]),
135
+ requirements: splitRequirementList(match[4]),
136
+ });
137
+ }
138
+
139
+ return phases;
140
+ }
141
+
142
+ function splitRequirementList(rawRequirements = '') {
143
+ return String(rawRequirements)
144
+ .split(',')
145
+ .map((entry) => entry.trim())
146
+ .filter(Boolean);
147
+ }
148
+
149
+ function collectPhaseArtifacts(phasesDir) {
150
+ if (!existsSync(phasesDir)) return [];
151
+
152
+ const artifacts = [];
153
+ for (const entry of readdirSync(phasesDir, { withFileTypes: true })) {
154
+ const entryPath = join(phasesDir, entry.name);
155
+ if (entry.isFile()) {
156
+ const artifact = classifyPhaseArtifact('', entry.name);
157
+ if (artifact) artifacts.push(artifact);
158
+ continue;
159
+ }
160
+
161
+ if (!entry.isDirectory()) continue;
162
+
163
+ for (const child of readdirSync(entryPath, { withFileTypes: true })) {
164
+ if (!child.isFile()) continue;
165
+ const artifact = classifyPhaseArtifact(entry.name, child.name);
166
+ if (artifact) artifacts.push(artifact);
167
+ }
168
+ }
169
+
170
+ return artifacts;
171
+ }
172
+
173
+ function classifyPhaseArtifact(dir, name) {
174
+ const baseIdMatch = name.match(/^(\d+(?:\.\d+)*[a-z]?(?:-\d+)?)/i);
175
+ const phaseTokenMatch = (dir ? dir.match(/^(\d+(?:\.\d+)*[a-z]?)-/i) : null)
176
+ || name.match(/^(\d+(?:\.\d+)*[a-z]?)/i);
177
+
178
+ if (!baseIdMatch || !phaseTokenMatch) return null;
179
+
180
+ let kind = 'other';
181
+ if (name.includes('PLAN')) kind = 'plan';
182
+ else if (name.includes('SUMMARY')) kind = 'summary';
183
+ else if (name.includes('VERIFICATION')) kind = 'verification';
184
+ else if (name.includes('APPROACH')) kind = 'approach';
185
+
186
+ return {
187
+ dir,
188
+ name,
189
+ displayPath: dir ? `${dir}/${name}` : name,
190
+ baseId: baseIdMatch[1],
191
+ phaseToken: normalizePhaseToken(phaseTokenMatch[1]),
192
+ kind,
193
+ };
194
+ }
195
+
196
+ function parseMilestoneLedger(content) {
197
+ if (!content) return [];
198
+
199
+ const entries = [];
200
+ let current = null;
201
+
202
+ for (const line of normalizeContent(content).split('\n')) {
203
+ const headingMatch = line.match(MILESTONE_LEDGER_HEADING_RE);
204
+ if (headingMatch) {
205
+ if (current) entries.push(current);
206
+ current = {
207
+ version: headingMatch[1],
208
+ title: headingMatch[2].trim(),
209
+ lines: [],
210
+ };
211
+ continue;
212
+ }
213
+
214
+ if (current) {
215
+ current.lines.push(line);
216
+ }
217
+ }
218
+
219
+ if (current) entries.push(current);
220
+
221
+ return entries.map((entry) => {
222
+ const section = entry.lines.join('\n');
223
+ const statusMatch = section.match(/^- Status:\s*(.+)$/im);
224
+ const shipped = /(^- Status:\s*shipped$)|(^- Shipped:\s+)/im.test(section) || /^✅/.test(entry.title);
225
+ return {
226
+ version: entry.version,
227
+ title: entry.title.replace(/^✅\s*/, ''),
228
+ status: statusMatch ? statusMatch[1].trim().toLowerCase() : shipped ? 'shipped' : 'unknown',
229
+ shipped,
230
+ };
231
+ });
232
+ }
233
+
234
+ function deriveCurrentMilestone({ roadmap, planningDir, milestoneLedger, counts }) {
235
+ const normalizedRoadmap = normalizeContent(roadmap);
236
+ const headingMatch = normalizedRoadmap.match(ACTIVE_MILESTONE_HEADING_RE);
237
+ if (!headingMatch) {
238
+ return {
239
+ version: null,
240
+ title: null,
241
+ shippedInLedger: false,
242
+ hasMatchingAudit: false,
243
+ archiveState: 'unknown',
244
+ counts,
245
+ };
246
+ }
247
+
248
+ const version = headingMatch[1];
249
+ const title = headingMatch[2].trim();
250
+ const ledgerEntry = milestoneLedger.find((entry) => entry.version === version) || null;
251
+ const auditPath = join(planningDir, `${version}-MILESTONE-AUDIT.md`);
252
+ const hasMatchingAudit = existsSync(auditPath);
253
+ const shippedInLedger = Boolean(ledgerEntry?.shipped);
254
+
255
+ return {
256
+ version,
257
+ title,
258
+ shippedInLedger,
259
+ hasMatchingAudit,
260
+ archiveState: shippedInLedger && hasMatchingAudit ? 'archived' : 'active',
261
+ counts,
262
+ };
263
+ }
264
+
265
+ function evaluateRequirementAlignment(spec, phases) {
266
+ const checkedRequirements = new Set(
267
+ [...normalizeContent(spec).matchAll(/- \[x\] \*\*\[([A-Z0-9-]+)]\*\*/g)].map((result) => result[1])
268
+ );
269
+
270
+ const roadmapRequirements = new Map();
271
+ for (const phase of phases) {
272
+ for (const requirementId of phase.requirements) {
273
+ roadmapRequirements.set(requirementId, phase.status === 'done');
274
+ }
275
+ }
276
+
277
+ const mismatches = [];
278
+ for (const [requirementId, roadmapStatus] of roadmapRequirements.entries()) {
279
+ const specChecked = checkedRequirements.has(requirementId);
280
+ if (roadmapStatus && !specChecked) {
281
+ mismatches.push(`${requirementId} phase complete but SPEC unchecked`);
282
+ }
283
+ if (!roadmapStatus && specChecked) {
284
+ mismatches.push(`${requirementId} SPEC checked but phase incomplete`);
285
+ }
286
+ }
287
+
288
+ return {
289
+ checkedRequirements,
290
+ roadmapRequirements,
291
+ mismatches,
292
+ };
293
+ }
@@ -15,6 +15,21 @@ export const DEFAULT_GIT_PROTOCOL = {
15
15
  pr: 'Follow the existing repo or team review workflow. Do not assume PR creation, timing, or naming unless explicitly requested.',
16
16
  };
17
17
 
18
+ export const RIGOR_PROFILES = {
19
+ quick: { researchDepth: 'fast', workflow: { research: false, discuss: false, planCheck: false, verifier: true } },
20
+ balanced: { researchDepth: 'balanced', workflow: { research: true, discuss: false, planCheck: true, verifier: true } },
21
+ thorough: { researchDepth: 'deep', workflow: { research: true, discuss: true, planCheck: true, verifier: true } },
22
+ };
23
+
24
+ export const COST_PROFILES = {
25
+ budget: { modelProfile: 'budget', parallelization: false },
26
+ balanced: { modelProfile: 'balanced', parallelization: true },
27
+ quality: { modelProfile: 'quality', parallelization: true },
28
+ };
29
+
30
+ export function resolveRigor(id) { return RIGOR_PROFILES[id] ?? RIGOR_PROFILES.balanced; }
31
+ export function resolveCost(id) { return COST_PROFILES[id] ?? COST_PROFILES.balanced; }
32
+
18
33
  export const VALID_MODEL_PROFILES = ['quality', 'balanced', 'budget'];
19
34
  export const PORTABLE_AGENT_IDS = ['plan-checker', 'approach-explorer'];
20
35
  export const MODEL_RUNTIME_IDS = ['claude', 'opencode', 'codex'];
@@ -25,12 +40,12 @@ export function normalizeModelProfile(value) {
25
40
  }
26
41
 
27
42
  export function buildDefaultConfig({ autoAdvance = false } = {}) {
43
+ const rigor = resolveRigor('balanced');
44
+ const cost = resolveCost('balanced');
28
45
  const config = {
29
- researchDepth: 'balanced',
30
- parallelization: true,
46
+ ...rigor,
47
+ ...cost,
31
48
  commitDocs: true,
32
- modelProfile: 'balanced',
33
- workflow: { research: true, discuss: false, planCheck: true, verifier: true },
34
49
  gitProtocol: { ...DEFAULT_GIT_PROTOCOL },
35
50
  initVersion: 'v1.1',
36
51
  };