gsdd-cli 0.1.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 (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +528 -0
  3. package/agents/DISTILLATION.md +306 -0
  4. package/agents/README.md +53 -0
  5. package/agents/debugger.md +82 -0
  6. package/agents/executor.md +394 -0
  7. package/agents/integration-checker.md +318 -0
  8. package/agents/mapper.md +103 -0
  9. package/agents/planner.md +296 -0
  10. package/agents/researcher.md +84 -0
  11. package/agents/roadmapper.md +296 -0
  12. package/agents/synthesizer.md +236 -0
  13. package/agents/verifier.md +337 -0
  14. package/bin/adapters/agents.mjs +33 -0
  15. package/bin/adapters/claude.mjs +145 -0
  16. package/bin/adapters/codex.mjs +58 -0
  17. package/bin/adapters/index.mjs +20 -0
  18. package/bin/adapters/opencode.mjs +237 -0
  19. package/bin/gsdd.mjs +102 -0
  20. package/bin/lib/cli-utils.mjs +28 -0
  21. package/bin/lib/health.mjs +248 -0
  22. package/bin/lib/init.mjs +379 -0
  23. package/bin/lib/manifest.mjs +134 -0
  24. package/bin/lib/models.mjs +379 -0
  25. package/bin/lib/phase.mjs +237 -0
  26. package/bin/lib/rendering.mjs +95 -0
  27. package/bin/lib/templates.mjs +207 -0
  28. package/distilled/DESIGN.md +1286 -0
  29. package/distilled/README.md +169 -0
  30. package/distilled/SKILL.md +85 -0
  31. package/distilled/templates/agents.block.md +90 -0
  32. package/distilled/templates/agents.md +13 -0
  33. package/distilled/templates/auth-matrix.md +78 -0
  34. package/distilled/templates/codebase/architecture.md +110 -0
  35. package/distilled/templates/codebase/concerns.md +95 -0
  36. package/distilled/templates/codebase/conventions.md +193 -0
  37. package/distilled/templates/codebase/stack.md +96 -0
  38. package/distilled/templates/delegates/mapper-arch.md +26 -0
  39. package/distilled/templates/delegates/mapper-concerns.md +27 -0
  40. package/distilled/templates/delegates/mapper-quality.md +28 -0
  41. package/distilled/templates/delegates/mapper-tech.md +25 -0
  42. package/distilled/templates/delegates/plan-checker.md +55 -0
  43. package/distilled/templates/delegates/researcher-architecture.md +30 -0
  44. package/distilled/templates/delegates/researcher-features.md +30 -0
  45. package/distilled/templates/delegates/researcher-pitfalls.md +30 -0
  46. package/distilled/templates/delegates/researcher-stack.md +30 -0
  47. package/distilled/templates/delegates/researcher-synthesizer.md +31 -0
  48. package/distilled/templates/research/architecture.md +57 -0
  49. package/distilled/templates/research/features.md +23 -0
  50. package/distilled/templates/research/pitfalls.md +46 -0
  51. package/distilled/templates/research/stack.md +45 -0
  52. package/distilled/templates/research/summary.md +67 -0
  53. package/distilled/templates/roadmap.md +62 -0
  54. package/distilled/templates/spec.md +110 -0
  55. package/distilled/workflows/audit-milestone.md +220 -0
  56. package/distilled/workflows/execute.md +270 -0
  57. package/distilled/workflows/map-codebase.md +246 -0
  58. package/distilled/workflows/new-project.md +418 -0
  59. package/distilled/workflows/pause.md +121 -0
  60. package/distilled/workflows/plan.md +383 -0
  61. package/distilled/workflows/progress.md +199 -0
  62. package/distilled/workflows/quick.md +187 -0
  63. package/distilled/workflows/resume.md +152 -0
  64. package/distilled/workflows/verify.md +307 -0
  65. package/package.json +45 -0
@@ -0,0 +1,237 @@
1
+ // phase.mjs — Phase discovery, verification, and scaffolding
2
+ //
3
+ // IMPORTANT: No module-scope process.cwd() — ESM caching means sub-modules
4
+ // evaluate once, so CWD must be computed inside function bodies.
5
+
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
7
+ import { join, basename } from 'path';
8
+ import { output } from './cli-utils.mjs';
9
+
10
+ function findFiles(dir, prefix) {
11
+ if (!existsSync(dir)) return [];
12
+ return readdirSync(dir).filter((f) => f.startsWith(prefix) || f.startsWith(prefix.replace(/^0+/, '')));
13
+ }
14
+
15
+ function padPhase(n) {
16
+ return String(n).padStart(2, '0');
17
+ }
18
+
19
+ function parsePhaseStatuses(roadmap) {
20
+ const phases = [];
21
+ const lines = roadmap.split('\n');
22
+ 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
+ );
30
+ if (match) {
31
+ const rawStatus = match[1].toLowerCase();
32
+ let status = 'not_started';
33
+ if (rawStatus === '[x]' || rawStatus === '✅' || rawStatus === '✅') status = 'done';
34
+ else if (rawStatus === '[-]') status = 'in_progress';
35
+ else if (rawStatus === 'ðÿ"„' || rawStatus === '🔄') status = 'in_progress';
36
+ phases.push({
37
+ number: parseInt(match[2], 10),
38
+ name: match[3].replace(/\*\*/g, '').split('-')[0].trim(),
39
+ status,
40
+ });
41
+ }
42
+ }
43
+ return phases;
44
+ }
45
+
46
+ export function cmdFindPhase(...args) {
47
+ const cwd = process.cwd();
48
+ const planningDir = join(cwd, '.planning');
49
+ const phaseNum = args[0];
50
+
51
+ if (!existsSync(planningDir)) {
52
+ output({ error: 'No .planning/ directory found. Run `gsdd init` then the new-project workflow first.' });
53
+ return;
54
+ }
55
+
56
+ const roadmapPath = join(planningDir, 'ROADMAP.md');
57
+ if (!existsSync(roadmapPath)) {
58
+ output({ error: 'No ROADMAP.md found. Run the new-project workflow first.' });
59
+ return;
60
+ }
61
+
62
+ const phasesDir = join(planningDir, 'phases');
63
+ const researchDir = join(planningDir, 'research');
64
+
65
+ if (phaseNum) {
66
+ const plans = findFiles(phasesDir, `${padPhase(phaseNum)}-PLAN`);
67
+ const summaries = findFiles(phasesDir, `${padPhase(phaseNum)}-SUMMARY`);
68
+
69
+ output({
70
+ phase: parseInt(phaseNum, 10),
71
+ directory: phasesDir,
72
+ plans,
73
+ summaries,
74
+ hasResearch: existsSync(researchDir) && readdirSync(researchDir).length > 0,
75
+ incomplete: plans.filter((p) => !summaries.some((s) => s.replace('SUMMARY', '') === p.replace('PLAN', ''))),
76
+ });
77
+ return;
78
+ }
79
+
80
+ const allFiles = existsSync(phasesDir) ? readdirSync(phasesDir) : [];
81
+ const plans = allFiles.filter((f) => f.includes('PLAN'));
82
+ const summaries = allFiles.filter((f) => f.includes('SUMMARY'));
83
+
84
+ const roadmap = readFileSync(roadmapPath, 'utf-8');
85
+ const phases = parsePhaseStatuses(roadmap);
86
+
87
+ output({
88
+ phases,
89
+ planCount: plans.length,
90
+ summaryCount: summaries.length,
91
+ currentPhase: phases.find((p) => p.status === 'in_progress') || phases.find((p) => p.status === 'not_started') || null,
92
+ hasResearch: existsSync(researchDir) && readdirSync(researchDir).length > 0,
93
+ });
94
+ }
95
+
96
+ export function cmdVerify(...args) {
97
+ const cwd = process.cwd();
98
+ const planningDir = join(cwd, '.planning');
99
+ const phaseNum = args[0];
100
+ if (!phaseNum) {
101
+ console.error('Usage: gsdd verify <phase-number>');
102
+ process.exit(1);
103
+ }
104
+
105
+ if (!existsSync(planningDir)) {
106
+ console.error('No .planning/ directory found.');
107
+ process.exit(1);
108
+ }
109
+
110
+ const planFile = findFiles(join(planningDir, 'phases'), `${padPhase(phaseNum)}-PLAN`)[0];
111
+ if (!planFile) {
112
+ console.error(`No plan found for phase ${phaseNum}`);
113
+ process.exit(1);
114
+ }
115
+
116
+ const planPath = join(planningDir, 'phases', planFile);
117
+ const plan = readFileSync(planPath, 'utf-8');
118
+
119
+ const fileMatches = plan.matchAll(/<files>([\s\S]*?)<\/files>/g);
120
+ const expectedFiles = [];
121
+ for (const match of fileMatches) {
122
+ const lines = match[1]
123
+ .split('\n')
124
+ .map((l) => l.trim())
125
+ .filter((l) => l.startsWith('-'));
126
+ for (const line of lines) {
127
+ const fileMatch = line.match(/(?:CREATE|MODIFY):\s*(.+)/);
128
+ if (fileMatch) expectedFiles.push(fileMatch[1].trim());
129
+ }
130
+ }
131
+
132
+ const results = expectedFiles.map((f) => {
133
+ const fullPath = join(cwd, f);
134
+ const exists = existsSync(fullPath);
135
+ let substantive = false;
136
+ if (exists) {
137
+ try {
138
+ const content = readFileSync(fullPath, 'utf-8');
139
+ const meaningfulLines = content.split('\n').filter(
140
+ (l) => l.trim() && !/^\s*(\/\/|\/\*|\*|#)/.test(l)
141
+ );
142
+ substantive = meaningfulLines.length >= 3 && !content.includes('// TODO: implement');
143
+ } catch {
144
+ substantive = false;
145
+ }
146
+ }
147
+ return { file: f, exists, substantive };
148
+ });
149
+
150
+ const antiPatterns = [];
151
+ for (const r of results) {
152
+ if (!r.exists) continue;
153
+ try {
154
+ const content = readFileSync(join(cwd, r.file), 'utf-8');
155
+ const lines = content.split('\n');
156
+ lines.forEach((line, i) => {
157
+ if (/TODO|FIXME|HACK|XXX/.test(line)) {
158
+ antiPatterns.push({ file: r.file, line: i + 1, pattern: 'TODO/FIXME', content: line.trim() });
159
+ }
160
+ if (/catch\s*\([^)]*\)\s*\{[\s]*\}/.test(line) || /catch\s*\([^)]*\)\s*\{\s*$/.test(line)) {
161
+ antiPatterns.push({ file: r.file, line: i + 1, pattern: 'Empty catch', content: line.trim() });
162
+ }
163
+ });
164
+ } catch {
165
+ // skip unreadable files
166
+ }
167
+ }
168
+
169
+ output({
170
+ phase: parseInt(phaseNum, 10),
171
+ artifacts: results,
172
+ allExist: results.every((r) => r.exists),
173
+ allSubstantive: results.filter((r) => r.exists).every((r) => r.substantive),
174
+ antiPatterns,
175
+ antiPatternCount: antiPatterns.length,
176
+ });
177
+ }
178
+
179
+ export function cmdScaffold(...args) {
180
+ const cwd = process.cwd();
181
+ const planningDir = join(cwd, '.planning');
182
+ const [type, ...rest] = args;
183
+
184
+ if (type !== 'phase') {
185
+ console.error('Usage: gsdd scaffold phase <number> [name]');
186
+ process.exit(1);
187
+ }
188
+
189
+ const phaseNum = rest[0];
190
+ const phaseName = rest.slice(1).join(' ');
191
+ if (!phaseNum) {
192
+ console.error('Usage: gsdd scaffold phase <number> [name]');
193
+ process.exit(1);
194
+ }
195
+
196
+ const phasesDir = join(planningDir, 'phases');
197
+ mkdirSync(phasesDir, { recursive: true });
198
+
199
+ const planFile = join(phasesDir, `${padPhase(phaseNum)}-PLAN.md`);
200
+ if (existsSync(planFile)) {
201
+ console.log(` - ${basename(planFile)} already exists`);
202
+ return;
203
+ }
204
+
205
+ const content = `# Phase ${phaseNum}: ${phaseName || '[Name]'} - Plan
206
+
207
+ ## Phase Goal
208
+ [From ROADMAP.md]
209
+
210
+ ## Requirements Covered
211
+ [REQ-IDs from SPEC.md]
212
+
213
+ ## Approach
214
+ [2-3 sentences]
215
+
216
+ ## Must-Haves (from success criteria)
217
+ 1. [Success criterion]
218
+
219
+ ## Tasks
220
+
221
+ <!-- Add tasks using XML format:
222
+ <task id="${phaseNum}-01">
223
+ <files>
224
+ - CREATE: path/to/file
225
+ </files>
226
+ <action>Description of what to implement</action>
227
+ <verify>How to verify it works</verify>
228
+ <done>When is this task done</done>
229
+ </task>
230
+ -->
231
+
232
+ ## Notes
233
+ `;
234
+
235
+ writeFileSync(planFile, content);
236
+ console.log(` - created ${basename(planFile)}`);
237
+ }
@@ -0,0 +1,95 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ const DISTILLED_DIR = join(__dirname, '..', '..', 'distilled');
8
+
9
+ function getWorkflowContent(workflowFile) {
10
+ const filePath = join(DISTILLED_DIR, 'workflows', workflowFile);
11
+ if (existsSync(filePath)) return readFileSync(filePath, 'utf-8');
12
+ return `<!-- Workflow file not found: ${workflowFile} -->\n`;
13
+ }
14
+
15
+ function getDelegateContent(delegateFile) {
16
+ const filePath = join(DISTILLED_DIR, 'templates', 'delegates', delegateFile);
17
+ if (existsSync(filePath)) return readFileSync(filePath, 'utf-8');
18
+ return `<!-- Delegate file not found: ${delegateFile} -->\n`;
19
+ }
20
+
21
+ function renderSkillContent(workflow) {
22
+ const workflowContent = getWorkflowContent(workflow.workflow);
23
+ return `---
24
+ name: ${workflow.name}
25
+ description: ${workflow.description}
26
+ context: fork
27
+ agent: ${workflow.agent}
28
+ ---
29
+
30
+ ${workflowContent}`;
31
+ }
32
+
33
+ function renderOpenCodeCommandContent(workflow) {
34
+ const workflowContent = getWorkflowContent(workflow.workflow);
35
+ return `---
36
+ description: ${workflow.description}
37
+ ---
38
+
39
+ ${workflowContent}`;
40
+ }
41
+
42
+ function renderAgentsBoundedBlock() {
43
+ const blockPath = join(DISTILLED_DIR, 'templates', 'agents.block.md');
44
+ if (existsSync(blockPath)) return readFileSync(blockPath, 'utf-8').trim();
45
+ return '## GSDD Governance (Generated)\n\n- Framework: GSDD\n- Planning: .planning/\n- Workflows: .agents/skills/gsdd-*/SKILL.md';
46
+ }
47
+
48
+ function renderAgentsFileContent() {
49
+ const templatePath = join(DISTILLED_DIR, 'templates', 'agents.md');
50
+ if (existsSync(templatePath)) {
51
+ const template = readFileSync(templatePath, 'utf-8');
52
+ return template.replace('{{GSDD_BLOCK}}', renderAgentsBoundedBlock()).trimEnd() + '\n';
53
+ }
54
+ const block = renderAgentsBoundedBlock();
55
+ return `# AGENTS.md - GSDD Governance\n\n<!-- BEGIN GSDD -->\n${block}\n<!-- END GSDD -->\n`;
56
+ }
57
+
58
+ function escapeRegExp(value) {
59
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
60
+ }
61
+
62
+ function upsertBoundedBlock(existing, blockContent) {
63
+ const begin = '<!-- BEGIN GSDD -->';
64
+ const end = '<!-- END GSDD -->';
65
+ const bounded = `${begin}\n${blockContent.trimEnd()}\n${end}`;
66
+
67
+ const re = new RegExp(`${escapeRegExp(begin)}[\\s\\S]*?${escapeRegExp(end)}`, 'm');
68
+ if (re.test(existing)) return existing.replace(re, bounded);
69
+
70
+ const lines = existing.split(/\r?\n/);
71
+ const h1Idx = lines.findIndex((line) => /^#\s+/.test(line));
72
+ if (h1Idx !== -1) {
73
+ const insertAt = h1Idx + 1;
74
+ const out = [
75
+ ...lines.slice(0, insertAt),
76
+ '',
77
+ bounded,
78
+ '',
79
+ ...lines.slice(insertAt),
80
+ ];
81
+ return out.join('\n').replace(/\n{3,}/g, '\n\n');
82
+ }
83
+
84
+ return `${bounded}\n\n${existing}`.replace(/\n{3,}/g, '\n\n');
85
+ }
86
+
87
+ export {
88
+ getDelegateContent,
89
+ getWorkflowContent,
90
+ renderAgentsBoundedBlock,
91
+ renderAgentsFileContent,
92
+ renderOpenCodeCommandContent,
93
+ renderSkillContent,
94
+ upsertBoundedBlock,
95
+ };
@@ -0,0 +1,207 @@
1
+ // templates.mjs - Project template and role installation/refresh helpers
2
+
3
+ import { existsSync, mkdirSync, readdirSync, cpSync, unlinkSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { fileHash, readManifest } from './manifest.mjs';
6
+
7
+ export function installProjectTemplates({ planningDir, distilledDir, agentsDir }) {
8
+ const localTemplatesDir = join(planningDir, 'templates');
9
+ const globalTemplatesDir = join(distilledDir, 'templates');
10
+
11
+ if (!existsSync(localTemplatesDir)) {
12
+ if (existsSync(globalTemplatesDir)) {
13
+ cpSync(globalTemplatesDir, localTemplatesDir, { recursive: true });
14
+ console.log(' - copied templates to .planning/templates/');
15
+ } else {
16
+ console.log(' - WARN: missing distilled/templates/; cannot copy templates');
17
+ }
18
+ } else {
19
+ console.log(' - .planning/templates/ already exists');
20
+ }
21
+
22
+ const localRolesDir = join(localTemplatesDir, 'roles');
23
+ if (!existsSync(localRolesDir)) {
24
+ if (existsSync(agentsDir)) {
25
+ mkdirSync(localRolesDir, { recursive: true });
26
+ for (const file of listRoleFiles(agentsDir)) {
27
+ cpSync(join(agentsDir, file), join(localRolesDir, file));
28
+ }
29
+ console.log(' - copied role contracts to .planning/templates/roles/');
30
+ } else {
31
+ console.log(' - WARN: missing agents/; cannot copy role contracts');
32
+ }
33
+ } else {
34
+ console.log(' - .planning/templates/roles/ already exists');
35
+ }
36
+ }
37
+
38
+ export function refreshTemplates({ planningDir, distilledDir, agentsDir, isDry = false }) {
39
+ const existingManifest = readManifest(planningDir);
40
+ const globalTemplatesDir = join(distilledDir, 'templates');
41
+ const localTemplatesDir = join(planningDir, 'templates');
42
+
43
+ const categories = [
44
+ { name: 'delegates', src: join(globalTemplatesDir, 'delegates'), dest: join(localTemplatesDir, 'delegates'), manifestKey: 'delegates' },
45
+ { name: 'research', src: join(globalTemplatesDir, 'research'), dest: join(localTemplatesDir, 'research'), manifestKey: 'research' },
46
+ { name: 'codebase', src: join(globalTemplatesDir, 'codebase'), dest: join(localTemplatesDir, 'codebase'), manifestKey: 'codebase' },
47
+ ];
48
+
49
+ for (const category of categories) {
50
+ refreshCategory(category, existingManifest, isDry);
51
+ }
52
+
53
+ refreshRootTemplates(globalTemplatesDir, localTemplatesDir, existingManifest, isDry);
54
+ refreshRoles(agentsDir, join(localTemplatesDir, 'roles'), existingManifest, isDry);
55
+ }
56
+
57
+ function listRoleFiles(agentsDir) {
58
+ return readdirSync(agentsDir).filter(
59
+ (file) => file.endsWith('.md') && file !== 'README.md' && !file.startsWith('_')
60
+ );
61
+ }
62
+
63
+ function refreshCategory({ name, src, dest, manifestKey }, existingManifest, isDry) {
64
+ if (!existsSync(src)) return;
65
+ if (!existsSync(dest) && !isDry) {
66
+ mkdirSync(dest, { recursive: true });
67
+ }
68
+
69
+ const manifestHashes = existingManifest?.templates?.[manifestKey] || null;
70
+ const sourceFiles = readdirSync(src).filter((file) => file.endsWith('.md'));
71
+ const installedFiles = existsSync(dest) ? readdirSync(dest).filter((file) => file.endsWith('.md')) : [];
72
+
73
+ for (const file of sourceFiles) {
74
+ const srcPath = join(src, file);
75
+ const destPath = join(dest, file);
76
+ const srcHash = fileHash(srcPath);
77
+
78
+ if (existsSync(destPath)) {
79
+ const destHash = fileHash(destPath);
80
+ if (destHash === srcHash) continue;
81
+
82
+ const manifestHash = manifestHashes?.[file];
83
+ if (manifestHash && destHash !== manifestHash) {
84
+ console.log(` - WARN: ${name}/${file} was modified locally; overwriting with framework source`);
85
+ }
86
+ }
87
+
88
+ if (isDry) {
89
+ console.log(` - would refresh ${name}/${file}`);
90
+ } else {
91
+ cpSync(srcPath, destPath);
92
+ console.log(` - refreshed ${name}/${file}`);
93
+ }
94
+ }
95
+
96
+ for (const file of installedFiles) {
97
+ if (!sourceFiles.includes(file)) {
98
+ if (isDry) {
99
+ console.log(` - would remove orphan ${name}/${file}`);
100
+ } else {
101
+ const orphanPath = join(dest, file);
102
+ if (existsSync(orphanPath)) {
103
+ unlinkSync(orphanPath);
104
+ }
105
+ console.log(` - removed orphan ${name}/${file}`);
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ function refreshRootTemplates(globalTemplatesDir, localTemplatesDir, existingManifest, isDry) {
112
+ if (!existsSync(globalTemplatesDir)) return;
113
+
114
+ const manifestHashes = existingManifest?.templates?.root || null;
115
+ const sourceFiles = readdirSync(globalTemplatesDir).filter((file) => file.endsWith('.md'));
116
+
117
+ for (const file of sourceFiles) {
118
+ const srcPath = join(globalTemplatesDir, file);
119
+ const destPath = join(localTemplatesDir, file);
120
+ const srcHash = fileHash(srcPath);
121
+
122
+ if (existsSync(destPath)) {
123
+ const destHash = fileHash(destPath);
124
+ if (destHash === srcHash) continue;
125
+
126
+ const manifestHash = manifestHashes?.[file];
127
+ if (manifestHash && destHash !== manifestHash) {
128
+ console.log(` - WARN: templates/${file} was modified locally; overwriting with framework source`);
129
+ }
130
+ }
131
+
132
+ if (isDry) {
133
+ console.log(` - would refresh templates/${file}`);
134
+ } else {
135
+ cpSync(srcPath, destPath);
136
+ console.log(` - refreshed templates/${file}`);
137
+ }
138
+ }
139
+
140
+ const installedRootFiles = existsSync(localTemplatesDir)
141
+ ? readdirSync(localTemplatesDir).filter((file) => file.endsWith('.md'))
142
+ : [];
143
+
144
+ for (const file of installedRootFiles) {
145
+ if (!sourceFiles.includes(file)) {
146
+ if (isDry) {
147
+ console.log(` - would remove orphan templates/${file}`);
148
+ } else {
149
+ const orphanPath = join(localTemplatesDir, file);
150
+ if (existsSync(orphanPath)) {
151
+ unlinkSync(orphanPath);
152
+ }
153
+ console.log(` - removed orphan templates/${file}`);
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ function refreshRoles(agentsDir, localRolesDir, existingManifest, isDry) {
160
+ if (!existsSync(agentsDir)) return;
161
+ if (!existsSync(localRolesDir) && !isDry) {
162
+ mkdirSync(localRolesDir, { recursive: true });
163
+ }
164
+
165
+ const manifestHashes = existingManifest?.roles || null;
166
+ const sourceFiles = listRoleFiles(agentsDir);
167
+ const installedFiles = existsSync(localRolesDir)
168
+ ? readdirSync(localRolesDir).filter((file) => file.endsWith('.md'))
169
+ : [];
170
+
171
+ for (const file of sourceFiles) {
172
+ const srcPath = join(agentsDir, file);
173
+ const destPath = join(localRolesDir, file);
174
+ const srcHash = fileHash(srcPath);
175
+
176
+ if (existsSync(destPath)) {
177
+ const destHash = fileHash(destPath);
178
+ if (destHash === srcHash) continue;
179
+
180
+ const manifestHash = manifestHashes?.[file];
181
+ if (manifestHash && destHash !== manifestHash) {
182
+ console.log(` - WARN: roles/${file} was modified locally; overwriting with framework source`);
183
+ }
184
+ }
185
+
186
+ if (isDry) {
187
+ console.log(` - would refresh roles/${file}`);
188
+ } else {
189
+ cpSync(srcPath, destPath);
190
+ console.log(` - refreshed roles/${file}`);
191
+ }
192
+ }
193
+
194
+ for (const file of installedFiles) {
195
+ if (!sourceFiles.includes(file)) {
196
+ if (isDry) {
197
+ console.log(` - would remove orphan roles/${file}`);
198
+ } else {
199
+ const orphanPath = join(localRolesDir, file);
200
+ if (existsSync(orphanPath)) {
201
+ unlinkSync(orphanPath);
202
+ }
203
+ console.log(` - removed orphan roles/${file}`);
204
+ }
205
+ }
206
+ }
207
+ }