gsdd-cli 0.3.1 → 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.
Files changed (43) hide show
  1. package/README.md +44 -29
  2. package/agents/DISTILLATION.md +15 -13
  3. package/agents/planner.md +2 -0
  4. package/bin/adapters/agents.mjs +1 -0
  5. package/bin/adapters/claude.mjs +12 -3
  6. package/bin/adapters/codex.mjs +4 -0
  7. package/bin/adapters/opencode.mjs +13 -4
  8. package/bin/gsdd.mjs +20 -5
  9. package/bin/lib/cli-utils.mjs +1 -1
  10. package/bin/lib/file-ops.mjs +161 -0
  11. package/bin/lib/health-truth.mjs +188 -0
  12. package/bin/lib/health.mjs +65 -12
  13. package/bin/lib/init-flow.mjs +50 -3
  14. package/bin/lib/init-prompts.mjs +22 -83
  15. package/bin/lib/init-runtime.mjs +9 -7
  16. package/bin/lib/init.mjs +5 -1
  17. package/bin/lib/models.mjs +19 -4
  18. package/bin/lib/phase.mjs +100 -14
  19. package/bin/lib/plan-constants.mjs +30 -0
  20. package/bin/lib/provenance.mjs +146 -0
  21. package/bin/lib/templates.mjs +17 -0
  22. package/distilled/DESIGN.md +345 -47
  23. package/distilled/EVIDENCE-INDEX.md +297 -0
  24. package/distilled/README.md +44 -20
  25. package/distilled/SKILL.md +89 -85
  26. package/distilled/templates/agents.block.md +13 -84
  27. package/distilled/templates/agents.md +0 -7
  28. package/distilled/templates/delegates/plan-checker.md +6 -3
  29. package/distilled/workflows/audit-milestone.md +6 -6
  30. package/distilled/workflows/complete-milestone.md +297 -0
  31. package/distilled/workflows/execute.md +188 -19
  32. package/distilled/workflows/map-codebase.md +5 -3
  33. package/distilled/workflows/new-milestone.md +249 -0
  34. package/distilled/workflows/new-project.md +5 -6
  35. package/distilled/workflows/pause.md +40 -6
  36. package/distilled/workflows/plan-milestone-gaps.md +183 -0
  37. package/distilled/workflows/plan.md +75 -11
  38. package/distilled/workflows/progress.md +42 -19
  39. package/distilled/workflows/quick.md +14 -10
  40. package/distilled/workflows/resume.md +121 -11
  41. package/distilled/workflows/verify-work.md +260 -0
  42. package/distilled/workflows/verify.md +124 -33
  43. package/package.json +7 -5
@@ -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
  };
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
- // Supports:
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: parseInt(match[2], 10),
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: parseInt(phaseNum, 10),
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.exit(1);
188
+ process.exitCode = 1; return;
103
189
  }
104
190
 
105
191
  if (!existsSync(planningDir)) {
106
192
  console.error('No .planning/ directory found.');
107
- process.exit(1);
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.exit(1);
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.exit(1);
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.exit(1);
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
+ }
@@ -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
  }