opencastle 0.8.0 → 0.8.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 (87) hide show
  1. package/README.md +27 -0
  2. package/bin/cli.mjs +2 -0
  3. package/dist/cli/adapters/claude-code.d.ts +2 -5
  4. package/dist/cli/adapters/claude-code.d.ts.map +1 -1
  5. package/dist/cli/adapters/claude-code.js +12 -251
  6. package/dist/cli/adapters/claude-code.js.map +1 -1
  7. package/dist/cli/adapters/cursor.d.ts.map +1 -1
  8. package/dist/cli/adapters/cursor.js +3 -17
  9. package/dist/cli/adapters/cursor.js.map +1 -1
  10. package/dist/cli/adapters/frontmatter.d.ts +26 -0
  11. package/dist/cli/adapters/frontmatter.d.ts.map +1 -0
  12. package/dist/cli/adapters/frontmatter.js +40 -0
  13. package/dist/cli/adapters/frontmatter.js.map +1 -0
  14. package/dist/cli/adapters/index.d.ts +5 -0
  15. package/dist/cli/adapters/index.d.ts.map +1 -0
  16. package/dist/cli/adapters/index.js +9 -0
  17. package/dist/cli/adapters/index.js.map +1 -0
  18. package/dist/cli/adapters/opencode.d.ts +2 -5
  19. package/dist/cli/adapters/opencode.d.ts.map +1 -1
  20. package/dist/cli/adapters/opencode.js +12 -250
  21. package/dist/cli/adapters/opencode.js.map +1 -1
  22. package/dist/cli/adapters/single-file-base.d.ts +40 -0
  23. package/dist/cli/adapters/single-file-base.d.ts.map +1 -0
  24. package/dist/cli/adapters/single-file-base.js +246 -0
  25. package/dist/cli/adapters/single-file-base.js.map +1 -0
  26. package/dist/cli/dashboard.d.ts.map +1 -1
  27. package/dist/cli/dashboard.js +3 -2
  28. package/dist/cli/dashboard.js.map +1 -1
  29. package/dist/cli/detect.d.ts.map +1 -1
  30. package/dist/cli/detect.js +13 -11
  31. package/dist/cli/detect.js.map +1 -1
  32. package/dist/cli/doctor.d.ts +3 -0
  33. package/dist/cli/doctor.d.ts.map +1 -0
  34. package/dist/cli/doctor.js +205 -0
  35. package/dist/cli/doctor.js.map +1 -0
  36. package/dist/cli/init.d.ts.map +1 -1
  37. package/dist/cli/init.js +31 -19
  38. package/dist/cli/init.js.map +1 -1
  39. package/dist/cli/run/schema.d.ts +1 -5
  40. package/dist/cli/run/schema.d.ts.map +1 -1
  41. package/dist/cli/run/schema.js +6 -330
  42. package/dist/cli/run/schema.js.map +1 -1
  43. package/dist/cli/run.d.ts.map +1 -1
  44. package/dist/cli/run.js +14 -1
  45. package/dist/cli/run.js.map +1 -1
  46. package/dist/cli/types.d.ts +0 -5
  47. package/dist/cli/types.d.ts.map +1 -1
  48. package/dist/cli/update.d.ts.map +1 -1
  49. package/dist/cli/update.js +4 -17
  50. package/dist/cli/update.js.map +1 -1
  51. package/package.json +7 -2
  52. package/src/cli/adapters/claude-code.ts +13 -304
  53. package/src/cli/adapters/cursor.ts +3 -23
  54. package/src/cli/adapters/frontmatter.ts +47 -0
  55. package/src/cli/adapters/index.ts +13 -0
  56. package/src/cli/adapters/opencode.ts +12 -301
  57. package/src/cli/adapters/single-file-base.ts +320 -0
  58. package/src/cli/dashboard.ts +3 -2
  59. package/src/cli/detect.ts +19 -15
  60. package/src/cli/doctor.ts +235 -0
  61. package/src/cli/init.ts +31 -24
  62. package/src/cli/run/schema.ts +7 -365
  63. package/src/cli/run.ts +17 -1
  64. package/src/cli/types.ts +0 -6
  65. package/src/cli/update.ts +5 -23
  66. package/src/dashboard/dist/_astro/{index.CWVzbF4T.css → index.Bnq19_1M.css} +1 -1
  67. package/src/dashboard/dist/index.html +170 -11
  68. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  69. package/src/dashboard/seed-data/reviews.ndjson +6 -0
  70. package/src/dashboard/src/pages/index.astro +213 -10
  71. package/src/dashboard/src/styles/dashboard.css +196 -0
  72. package/src/orchestrator/agent-workflows/bug-fix.md +2 -2
  73. package/src/orchestrator/agent-workflows/data-pipeline.md +8 -8
  74. package/src/orchestrator/agent-workflows/database-migration.md +2 -2
  75. package/src/orchestrator/agent-workflows/feature-implementation.md +2 -2
  76. package/src/orchestrator/agent-workflows/performance-optimization.md +2 -2
  77. package/src/orchestrator/agent-workflows/refactoring.md +2 -2
  78. package/src/orchestrator/agent-workflows/schema-changes.md +2 -2
  79. package/src/orchestrator/agent-workflows/security-audit.md +2 -2
  80. package/src/orchestrator/agents/data-expert.agent.md +2 -2
  81. package/src/orchestrator/agents/researcher.agent.md +0 -16
  82. package/src/orchestrator/agents/team-lead.agent.md +17 -6
  83. package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +1 -3
  84. package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +1 -1
  85. package/src/orchestrator/skills/agent-hooks/SKILL.md +4 -2
  86. package/src/orchestrator/skills/self-improvement/SKILL.md +1 -1
  87. package/src/orchestrator/prompts/metrics-report.prompt.md +0 -144
@@ -1,10 +1,4 @@
1
- import { resolve, basename } from 'node:path'
2
- import { mkdir, writeFile, readdir, readFile, unlink, rm } from 'node:fs/promises'
3
- import { existsSync } from 'node:fs'
4
- import { copyDir, getOrchestratorRoot, getPluginsRoot, getPluginSkillEntries } from '../copy.js'
5
- import { scaffoldMcpConfig } from '../mcp.js'
6
- import { getExcludedSkills, getExcludedAgents, getCustomizationsTransform, getIncludedPluginIds } from '../stack-config.js'
7
- import type { CopyResults, ManagedPaths, RepoInfo, StackConfig } from '../types.js'
1
+ import { createSingleFileAdapter } from './single-file-base.js'
8
2
 
9
3
  /**
10
4
  * OpenCode adapter.
@@ -21,300 +15,17 @@ import type { CopyResults, ManagedPaths, RepoInfo, StackConfig } from '../types.
21
15
  */
22
16
 
23
17
  export const IDE_ID = 'opencode'
24
- export const IDE_LABEL = 'OpenCode'
25
18
 
26
- // ─── Helpers ──────────────────────────────────────────────────────
19
+ const { install, update, getManagedPaths } = createSingleFileAdapter({
20
+ rootFile: 'AGENTS.md',
21
+ dotDir: '.opencode',
22
+ mcpConfigPath: 'opencode.json',
23
+ mcpFormat: 'opencode',
24
+ promptsDir: 'prompts',
25
+ workflowsDir: 'workflows',
26
+ workflowPrefix: '',
27
+ frameworkDirs: ['agents', 'skills', 'prompts', 'workflows'],
28
+ })
27
29
 
28
- function stripFrontmatter(content: string): string {
29
- const m = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
30
- return m ? m[2].trim() : content.trim()
31
- }
30
+ export { install, update, getManagedPaths }
32
31
 
33
- function parseFrontmatterMeta(content: string): Record<string, string> {
34
- const m = content.match(/^---\n([\s\S]*?)\n---/)
35
- if (!m) return {}
36
- const meta: Record<string, string> = {}
37
- for (const line of m[1].split('\n')) {
38
- const kv = line.match(/^(\w[\w-]*):\s*['"]?(.+?)['"]?\s*$/)
39
- if (kv) meta[kv[1]] = kv[2]
40
- }
41
- return meta
42
- }
43
-
44
- // ─── Install ──────────────────────────────────────────────────────
45
-
46
- export async function install(
47
- pkgRoot: string,
48
- projectRoot: string,
49
- stack?: StackConfig,
50
- repoInfo?: RepoInfo
51
- ): Promise<CopyResults> {
52
- const srcRoot = getOrchestratorRoot(pkgRoot)
53
- const results: CopyResults = { copied: [], skipped: [], created: [] }
54
-
55
- const excludedSkills = stack ? getExcludedSkills(stack) : new Set<string>()
56
- const excludedAgents = stack ? getExcludedAgents(stack) : new Set<string>()
57
-
58
- // 1. Build AGENTS.md ← instructions/* + agent index + skill index
59
- const agentsMd = resolve(projectRoot, 'AGENTS.md')
60
- if (!existsSync(agentsMd)) {
61
- const sections: string[] = []
62
-
63
- sections.push(
64
- '# Project Instructions\n\n' +
65
- 'All conventions, architecture, and project context are embedded below. ' +
66
- 'Skills are in `.opencode/skills/` — read them when a task matches. ' +
67
- 'Agent definitions are in `.opencode/agents/` — read the relevant file when adopting a persona.'
68
- )
69
-
70
- // Always-loaded instruction files
71
- const instDir = resolve(srcRoot, 'instructions')
72
- if (existsSync(instDir)) {
73
- for (const file of (await readdir(instDir)).sort()) {
74
- if (!file.endsWith('.md')) continue
75
- const content = await readFile(resolve(instDir, file), 'utf8')
76
- sections.push(
77
- `\n---\n\n<!-- Source: instructions/${file} -->\n\n${stripFrontmatter(content)}`
78
- )
79
- }
80
- }
81
-
82
- // Agent reference
83
- const agentsDir = resolve(srcRoot, 'agents')
84
- if (existsSync(agentsDir)) {
85
- const agentLines: string[] = ['\n---\n\n## Agent Definitions\n']
86
- agentLines.push(
87
- 'The following agent personas are available. Adopt the appropriate persona when asked.\n'
88
- )
89
- for (const file of (await readdir(agentsDir)).sort()) {
90
- if (!file.endsWith('.md')) continue
91
- if (excludedAgents.has(file)) continue
92
- const meta = parseFrontmatterMeta(
93
- await readFile(resolve(agentsDir, file), 'utf8')
94
- )
95
- const name = meta['name'] ?? basename(file, '.agent.md')
96
- const desc = meta['description'] ?? ''
97
- agentLines.push(`- **${name}**: ${desc}`)
98
- }
99
- agentLines.push(
100
- '\nFull agent definitions are in `.opencode/agents/`. Read the relevant file when adopting a persona.'
101
- )
102
- sections.push(agentLines.join('\n'))
103
- }
104
-
105
- // Skill index
106
- const skillsDir = resolve(srcRoot, 'skills')
107
- if (existsSync(skillsDir)) {
108
- const skillLines: string[] = ['\n---\n\n## Available Skills\n']
109
- skillLines.push(
110
- 'Skills are on-demand knowledge files. Read the file when the task matches.\n'
111
- )
112
- const subdirs = (
113
- await readdir(skillsDir, { withFileTypes: true })
114
- ).filter((e) => e.isDirectory())
115
- for (const entry of subdirs.sort((a, b) =>
116
- a.name.localeCompare(b.name)
117
- )) {
118
- if (excludedSkills.has(entry.name)) continue
119
- const skillFile = resolve(skillsDir, entry.name, 'SKILL.md')
120
- if (!existsSync(skillFile)) continue
121
- const meta = parseFrontmatterMeta(await readFile(skillFile, 'utf8'))
122
- const desc = meta['description'] ?? ''
123
- skillLines.push(
124
- `- **${entry.name}** (\`.opencode/skills/${entry.name}.md\`): ${desc}`
125
- )
126
- }
127
-
128
- // Plugin skills
129
- const pluginsRoot = getPluginsRoot(pkgRoot)
130
- const includedPlugins = stack ? getIncludedPluginIds(stack) : undefined
131
- const pluginEntries = await getPluginSkillEntries(pluginsRoot, includedPlugins)
132
- for (const { id, skillPath } of pluginEntries.sort((a, b) => a.id.localeCompare(b.id))) {
133
- const pluginMeta = parseFrontmatterMeta(await readFile(skillPath, 'utf8'))
134
- const pluginDesc = pluginMeta['description'] ?? ''
135
- skillLines.push(
136
- `- **${id}** (\`.opencode/skills/${id}.md\`): ${pluginDesc}`
137
- )
138
- }
139
-
140
- sections.push(skillLines.join('\n'))
141
- }
142
-
143
- await writeFile(agentsMd, sections.join('\n') + '\n')
144
- results.created.push(agentsMd)
145
- } else {
146
- results.skipped.push(agentsMd)
147
- }
148
-
149
- const openDir = resolve(projectRoot, '.opencode')
150
-
151
- // 2. Agent definitions → .opencode/agents/
152
- const agentsDir = resolve(srcRoot, 'agents')
153
- if (existsSync(agentsDir)) {
154
- const destAgents = resolve(openDir, 'agents')
155
- await mkdir(destAgents, { recursive: true })
156
- for (const file of await readdir(agentsDir)) {
157
- if (!file.endsWith('.md')) continue
158
- if (excludedAgents.has(file)) continue
159
- const destPath = resolve(destAgents, file)
160
- if (existsSync(destPath)) {
161
- results.skipped.push(destPath)
162
- continue
163
- }
164
- const content = await readFile(resolve(agentsDir, file), 'utf8')
165
- await writeFile(destPath, stripFrontmatter(content) + '\n')
166
- results.created.push(destPath)
167
- }
168
- }
169
-
170
- // 3. Skills → .opencode/skills/<name>.md
171
- const skillsDir = resolve(srcRoot, 'skills')
172
- if (existsSync(skillsDir)) {
173
- const destSkills = resolve(openDir, 'skills')
174
- await mkdir(destSkills, { recursive: true })
175
- const subdirs = (
176
- await readdir(skillsDir, { withFileTypes: true })
177
- ).filter((e) => e.isDirectory())
178
- for (const entry of subdirs) {
179
- if (excludedSkills.has(entry.name)) continue
180
- const skillFile = resolve(skillsDir, entry.name, 'SKILL.md')
181
- if (!existsSync(skillFile)) continue
182
- const destPath = resolve(destSkills, `${entry.name}.md`)
183
- if (existsSync(destPath)) {
184
- results.skipped.push(destPath)
185
- continue
186
- }
187
- const content = await readFile(skillFile, 'utf8')
188
- await writeFile(destPath, stripFrontmatter(content) + '\n')
189
- results.created.push(destPath)
190
- }
191
- }
192
-
193
- // 3b. Plugin skills → .opencode/skills/<plugin-id>.md
194
- {
195
- const pluginsRoot = getPluginsRoot(pkgRoot)
196
- const includedPlugins = stack ? getIncludedPluginIds(stack) : undefined
197
- const pluginEntries = await getPluginSkillEntries(pluginsRoot, includedPlugins)
198
- const destSkills = resolve(openDir, 'skills')
199
- await mkdir(destSkills, { recursive: true })
200
- for (const { id, skillPath } of pluginEntries) {
201
- const destPath = resolve(destSkills, `${id}.md`)
202
- if (existsSync(destPath)) {
203
- results.skipped.push(destPath)
204
- continue
205
- }
206
- const content = await readFile(skillPath, 'utf8')
207
- await writeFile(destPath, stripFrontmatter(content) + '\n')
208
- results.created.push(destPath)
209
- }
210
- }
211
-
212
- // 4. Prompts → .opencode/prompts/<name>.md
213
- const promptDir = resolve(srcRoot, 'prompts')
214
- if (existsSync(promptDir)) {
215
- const destPrompts = resolve(openDir, 'prompts')
216
- await mkdir(destPrompts, { recursive: true })
217
- for (const file of await readdir(promptDir)) {
218
- if (!file.endsWith('.md')) continue
219
- const name = basename(file, '.prompt.md') || basename(file, '.md')
220
- const destPath = resolve(destPrompts, `${name}.md`)
221
- if (existsSync(destPath)) {
222
- results.skipped.push(destPath)
223
- continue
224
- }
225
- const content = await readFile(resolve(promptDir, file), 'utf8')
226
- await writeFile(destPath, stripFrontmatter(content) + '\n')
227
- results.created.push(destPath)
228
- }
229
- }
230
-
231
- // 5. Agent Workflows → .opencode/workflows/<name>.md
232
- const wfDir = resolve(srcRoot, 'agent-workflows')
233
- if (existsSync(wfDir)) {
234
- const destWf = resolve(openDir, 'workflows')
235
- await mkdir(destWf, { recursive: true })
236
- for (const file of await readdir(wfDir)) {
237
- if (!file.endsWith('.md')) continue
238
- if (file === 'README.md') continue
239
- const name = basename(file, '.md')
240
- const destPath = resolve(destWf, `${name}.md`)
241
- if (existsSync(destPath)) {
242
- results.skipped.push(destPath)
243
- continue
244
- }
245
- const content = await readFile(resolve(wfDir, file), 'utf8')
246
- await writeFile(destPath, stripFrontmatter(content) + '\n')
247
- results.created.push(destPath)
248
- }
249
- }
250
-
251
- // 6. Customizations (scaffold once, pre-populated with stack choices)
252
- const custDir = resolve(srcRoot, 'customizations')
253
- if (existsSync(custDir)) {
254
- const destCust = resolve(openDir, 'customizations')
255
- const custTransform = stack ? getCustomizationsTransform(stack) : undefined
256
- const sub = await copyDir(custDir, destCust, { transform: custTransform })
257
- results.created.push(...sub.created)
258
- results.skipped.push(...sub.skipped)
259
- }
260
-
261
- // 7. MCP server config → opencode.json (OpenCode format)
262
- const mcpResult = await scaffoldMcpConfig(
263
- projectRoot,
264
- 'opencode.json',
265
- stack,
266
- repoInfo,
267
- 'opencode'
268
- )
269
- results[mcpResult.action].push(mcpResult.path)
270
-
271
- return results
272
- }
273
-
274
- // ─── Update ───────────────────────────────────────────────────────
275
-
276
- export async function update(
277
- pkgRoot: string,
278
- projectRoot: string,
279
- stack?: StackConfig
280
- ): Promise<CopyResults> {
281
- const results: CopyResults = { copied: [], skipped: [], created: [] }
282
- const openDir = resolve(projectRoot, '.opencode')
283
-
284
- // 1. Regenerate AGENTS.md (overwrite)
285
- const agentsMd = resolve(projectRoot, 'AGENTS.md')
286
- if (existsSync(agentsMd)) {
287
- await unlink(agentsMd)
288
- }
289
-
290
- // 2. Remove existing framework files so install() recreates them
291
- const frameworkDirs = ['agents', 'skills', 'prompts', 'workflows']
292
- for (const dir of frameworkDirs) {
293
- const dirPath = resolve(openDir, dir)
294
- if (existsSync(dirPath)) {
295
- await rm(dirPath, { recursive: true })
296
- }
297
- }
298
-
299
- // 3. Re-run full install
300
- const installResult = await install(pkgRoot, projectRoot, stack)
301
- results.copied.push(...installResult.created)
302
- results.skipped.push(...installResult.skipped)
303
-
304
- return results
305
- }
306
-
307
- // ─── Managed paths ────────────────────────────────────────────────
308
-
309
- export function getManagedPaths(): ManagedPaths {
310
- return {
311
- framework: [
312
- 'AGENTS.md',
313
- '.opencode/agents/',
314
- '.opencode/skills/',
315
- '.opencode/prompts/',
316
- '.opencode/workflows/',
317
- ],
318
- customizable: ['.opencode/customizations/', 'opencode.json'],
319
- }
320
- }
@@ -0,0 +1,320 @@
1
+ import { resolve, basename } from 'node:path'
2
+ import { mkdir, writeFile, readdir, readFile, unlink, rm } from 'node:fs/promises'
3
+ import { existsSync } from 'node:fs'
4
+ import { copyDir, getOrchestratorRoot, getPluginsRoot, getPluginSkillEntries } from '../copy.js'
5
+ import { scaffoldMcpConfig } from '../mcp.js'
6
+ import { getExcludedSkills, getExcludedAgents, getCustomizationsTransform, getIncludedPluginIds } from '../stack-config.js'
7
+ import type { CopyResults, IdeAdapter, IdeChoice, ManagedPaths, RepoInfo, StackConfig } from '../types.js'
8
+ import { stripFrontmatter, parseFrontmatterMeta } from './frontmatter.js'
9
+
10
+ /**
11
+ * Configuration for adapters that produce a single root instructions file
12
+ * and a dot-directory structure (e.g. Claude Code → CLAUDE.md + .claude/,
13
+ * OpenCode → AGENTS.md + .opencode/).
14
+ */
15
+ export interface SingleFileAdapterConfig {
16
+ /** Root instructions file name, e.g. 'CLAUDE.md' */
17
+ rootFile: string
18
+ /** Dot directory for framework files, e.g. '.claude' */
19
+ dotDir: string
20
+ /** Path for MCP config relative to project root, e.g. '.claude/mcp.json' */
21
+ mcpConfigPath: string
22
+ /** MCP format identifier passed to scaffoldMcpConfig */
23
+ mcpFormat: IdeChoice
24
+ /** Subdirectory name under dotDir for prompt output, e.g. 'commands' or 'prompts' */
25
+ promptsDir: string
26
+ /** Subdirectory name under dotDir for workflow output, e.g. 'commands' or 'workflows' */
27
+ workflowsDir: string
28
+ /** Prefix prepended to workflow filenames, e.g. 'workflow-' or '' */
29
+ workflowPrefix: string
30
+ /** Framework subdirectories (under dotDir) to remove during update */
31
+ frameworkDirs: string[]
32
+ }
33
+
34
+ /**
35
+ * Creates install/update/getManagedPaths functions from a config object.
36
+ *
37
+ * Both Claude Code and OpenCode share the same structure:
38
+ * 1. A single root .md file with embedded instructions, agent index, and skill index
39
+ * 2. Agent definitions stripped of frontmatter
40
+ * 3. Skills stripped of frontmatter
41
+ * 4. Prompts stripped of frontmatter
42
+ * 5. Workflows stripped of frontmatter
43
+ * 6. Customizations scaffolded once
44
+ * 7. MCP config scaffolded once
45
+ *
46
+ * The only differences are directory names and file naming conventions.
47
+ */
48
+ export function createSingleFileAdapter(config: SingleFileAdapterConfig): IdeAdapter {
49
+ async function install(
50
+ pkgRoot: string,
51
+ projectRoot: string,
52
+ stack?: StackConfig,
53
+ repoInfo?: RepoInfo
54
+ ): Promise<CopyResults> {
55
+ const srcRoot = getOrchestratorRoot(pkgRoot)
56
+ const results: CopyResults = { copied: [], skipped: [], created: [] }
57
+
58
+ const excludedSkills = stack ? getExcludedSkills(stack) : new Set<string>()
59
+ const excludedAgents = stack ? getExcludedAgents(stack) : new Set<string>()
60
+
61
+ // 1. Build root instructions file
62
+ const rootPath = resolve(projectRoot, config.rootFile)
63
+ if (!existsSync(rootPath)) {
64
+ const sections: string[] = []
65
+
66
+ sections.push(
67
+ '# Project Instructions\n\n' +
68
+ 'All conventions, architecture, and project context are embedded below. ' +
69
+ `Skills are in \`${config.dotDir}/skills/\` — read them when a task matches. ` +
70
+ `Agent definitions are in \`${config.dotDir}/agents/\` — read the relevant file when adopting a persona.`
71
+ )
72
+
73
+ // Always-loaded instruction files
74
+ const instDir = resolve(srcRoot, 'instructions')
75
+ if (existsSync(instDir)) {
76
+ for (const file of (await readdir(instDir)).sort()) {
77
+ if (!file.endsWith('.md')) continue
78
+ const content = await readFile(resolve(instDir, file), 'utf8')
79
+ sections.push(
80
+ `\n---\n\n<!-- Source: instructions/${file} -->\n\n${stripFrontmatter(content)}`
81
+ )
82
+ }
83
+ }
84
+
85
+ // Agent reference
86
+ const agentsDir = resolve(srcRoot, 'agents')
87
+ if (existsSync(agentsDir)) {
88
+ const agentLines: string[] = ['\n---\n\n## Agent Definitions\n']
89
+ agentLines.push(
90
+ 'The following agent personas are available. Adopt the appropriate persona when asked.\n'
91
+ )
92
+ for (const file of (await readdir(agentsDir)).sort()) {
93
+ if (!file.endsWith('.md')) continue
94
+ if (excludedAgents.has(file)) continue
95
+ const meta = parseFrontmatterMeta(
96
+ await readFile(resolve(agentsDir, file), 'utf8')
97
+ )
98
+ const name = meta['name'] ?? basename(file, '.agent.md')
99
+ const desc = meta['description'] ?? ''
100
+ agentLines.push(`- **${name}**: ${desc}`)
101
+ }
102
+ agentLines.push(
103
+ `\nFull agent definitions are in \`${config.dotDir}/agents/\`. Read the relevant file when adopting a persona.`
104
+ )
105
+ sections.push(agentLines.join('\n'))
106
+ }
107
+
108
+ // Skill index
109
+ const skillsDir = resolve(srcRoot, 'skills')
110
+ if (existsSync(skillsDir)) {
111
+ const skillLines: string[] = ['\n---\n\n## Available Skills\n']
112
+ skillLines.push(
113
+ 'Skills are on-demand knowledge files. Read the file when the task matches.\n'
114
+ )
115
+ const subdirs = (
116
+ await readdir(skillsDir, { withFileTypes: true })
117
+ ).filter((e) => e.isDirectory())
118
+ for (const entry of subdirs.sort((a, b) =>
119
+ a.name.localeCompare(b.name)
120
+ )) {
121
+ if (excludedSkills.has(entry.name)) continue
122
+ const skillFile = resolve(skillsDir, entry.name, 'SKILL.md')
123
+ if (!existsSync(skillFile)) continue
124
+ const meta = parseFrontmatterMeta(await readFile(skillFile, 'utf8'))
125
+ const desc = meta['description'] ?? ''
126
+ skillLines.push(
127
+ `- **${entry.name}** (\`${config.dotDir}/skills/${entry.name}.md\`): ${desc}`
128
+ )
129
+ }
130
+
131
+ // Plugin skills
132
+ const pluginsRoot = getPluginsRoot(pkgRoot)
133
+ const includedPlugins = stack ? getIncludedPluginIds(stack) : undefined
134
+ const pluginEntries = await getPluginSkillEntries(pluginsRoot, includedPlugins)
135
+ for (const { id, skillPath } of pluginEntries.sort((a, b) => a.id.localeCompare(b.id))) {
136
+ const pluginMeta = parseFrontmatterMeta(await readFile(skillPath, 'utf8'))
137
+ const pluginDesc = pluginMeta['description'] ?? ''
138
+ skillLines.push(
139
+ `- **${id}** (\`${config.dotDir}/skills/${id}.md\`): ${pluginDesc}`
140
+ )
141
+ }
142
+
143
+ sections.push(skillLines.join('\n'))
144
+ }
145
+
146
+ await writeFile(rootPath, sections.join('\n') + '\n')
147
+ results.created.push(rootPath)
148
+ } else {
149
+ results.skipped.push(rootPath)
150
+ }
151
+
152
+ const dotDirPath = resolve(projectRoot, config.dotDir)
153
+
154
+ // 2. Agent definitions → dotDir/agents/
155
+ const agentsDir = resolve(srcRoot, 'agents')
156
+ if (existsSync(agentsDir)) {
157
+ const destAgents = resolve(dotDirPath, 'agents')
158
+ await mkdir(destAgents, { recursive: true })
159
+ for (const file of await readdir(agentsDir)) {
160
+ if (!file.endsWith('.md')) continue
161
+ if (excludedAgents.has(file)) continue
162
+ const destPath = resolve(destAgents, file)
163
+ if (existsSync(destPath)) {
164
+ results.skipped.push(destPath)
165
+ continue
166
+ }
167
+ const content = await readFile(resolve(agentsDir, file), 'utf8')
168
+ await writeFile(destPath, stripFrontmatter(content) + '\n')
169
+ results.created.push(destPath)
170
+ }
171
+ }
172
+
173
+ // 3. Skills → dotDir/skills/<name>.md
174
+ const skillsDir = resolve(srcRoot, 'skills')
175
+ if (existsSync(skillsDir)) {
176
+ const destSkills = resolve(dotDirPath, 'skills')
177
+ await mkdir(destSkills, { recursive: true })
178
+ const subdirs = (
179
+ await readdir(skillsDir, { withFileTypes: true })
180
+ ).filter((e) => e.isDirectory())
181
+ for (const entry of subdirs) {
182
+ if (excludedSkills.has(entry.name)) continue
183
+ const skillFile = resolve(skillsDir, entry.name, 'SKILL.md')
184
+ if (!existsSync(skillFile)) continue
185
+ const destPath = resolve(destSkills, `${entry.name}.md`)
186
+ if (existsSync(destPath)) {
187
+ results.skipped.push(destPath)
188
+ continue
189
+ }
190
+ const content = await readFile(skillFile, 'utf8')
191
+ await writeFile(destPath, stripFrontmatter(content) + '\n')
192
+ results.created.push(destPath)
193
+ }
194
+ }
195
+
196
+ // 3b. Plugin skills → dotDir/skills/<plugin-id>.md
197
+ {
198
+ const pluginsRoot = getPluginsRoot(pkgRoot)
199
+ const includedPlugins = stack ? getIncludedPluginIds(stack) : undefined
200
+ const pluginEntries = await getPluginSkillEntries(pluginsRoot, includedPlugins)
201
+ const destSkills = resolve(dotDirPath, 'skills')
202
+ await mkdir(destSkills, { recursive: true })
203
+ for (const { id, skillPath } of pluginEntries) {
204
+ const destPath = resolve(destSkills, `${id}.md`)
205
+ if (existsSync(destPath)) {
206
+ results.skipped.push(destPath)
207
+ continue
208
+ }
209
+ const content = await readFile(skillPath, 'utf8')
210
+ await writeFile(destPath, stripFrontmatter(content) + '\n')
211
+ results.created.push(destPath)
212
+ }
213
+ }
214
+
215
+ // 4. Prompts → dotDir/<promptsDir>/<name>.md
216
+ const promptDir = resolve(srcRoot, 'prompts')
217
+ if (existsSync(promptDir)) {
218
+ const destPrompts = resolve(dotDirPath, config.promptsDir)
219
+ await mkdir(destPrompts, { recursive: true })
220
+ for (const file of await readdir(promptDir)) {
221
+ if (!file.endsWith('.md')) continue
222
+ const name = basename(file, '.prompt.md') || basename(file, '.md')
223
+ const destPath = resolve(destPrompts, `${name}.md`)
224
+ if (existsSync(destPath)) {
225
+ results.skipped.push(destPath)
226
+ continue
227
+ }
228
+ const content = await readFile(resolve(promptDir, file), 'utf8')
229
+ await writeFile(destPath, stripFrontmatter(content) + '\n')
230
+ results.created.push(destPath)
231
+ }
232
+ }
233
+
234
+ // 5. Agent Workflows → dotDir/<workflowsDir>/<prefix><name>.md
235
+ const wfDir = resolve(srcRoot, 'agent-workflows')
236
+ if (existsSync(wfDir)) {
237
+ const destWf = resolve(dotDirPath, config.workflowsDir)
238
+ await mkdir(destWf, { recursive: true })
239
+ for (const file of await readdir(wfDir)) {
240
+ if (!file.endsWith('.md')) continue
241
+ if (file === 'README.md') continue
242
+ const name = basename(file, '.md')
243
+ const destPath = resolve(destWf, `${config.workflowPrefix}${name}.md`)
244
+ if (existsSync(destPath)) {
245
+ results.skipped.push(destPath)
246
+ continue
247
+ }
248
+ const content = await readFile(resolve(wfDir, file), 'utf8')
249
+ await writeFile(destPath, stripFrontmatter(content) + '\n')
250
+ results.created.push(destPath)
251
+ }
252
+ }
253
+
254
+ // 6. Customizations (scaffold once)
255
+ const custDir = resolve(srcRoot, 'customizations')
256
+ if (existsSync(custDir)) {
257
+ const destCust = resolve(dotDirPath, 'customizations')
258
+ const custTransform = stack ? getCustomizationsTransform(stack) : undefined
259
+ const sub = await copyDir(custDir, destCust, { transform: custTransform })
260
+ results.created.push(...sub.created)
261
+ results.skipped.push(...sub.skipped)
262
+ }
263
+
264
+ // 7. MCP server config (scaffold once)
265
+ const mcpResult = await scaffoldMcpConfig(
266
+ projectRoot,
267
+ config.mcpConfigPath,
268
+ stack,
269
+ repoInfo,
270
+ config.mcpFormat
271
+ )
272
+ results[mcpResult.action].push(mcpResult.path)
273
+
274
+ return results
275
+ }
276
+
277
+ async function update(
278
+ pkgRoot: string,
279
+ projectRoot: string,
280
+ stack?: StackConfig
281
+ ): Promise<CopyResults> {
282
+ const results: CopyResults = { copied: [], skipped: [], created: [] }
283
+ const dotDirPath = resolve(projectRoot, config.dotDir)
284
+
285
+ // 1. Remove root instructions file so install() recreates it
286
+ const rootPath = resolve(projectRoot, config.rootFile)
287
+ if (existsSync(rootPath)) {
288
+ await unlink(rootPath)
289
+ }
290
+
291
+ // 2. Remove existing framework directories
292
+ for (const dir of config.frameworkDirs) {
293
+ const dirPath = resolve(dotDirPath, dir)
294
+ if (existsSync(dirPath)) {
295
+ await rm(dirPath, { recursive: true })
296
+ }
297
+ }
298
+
299
+ // 3. Re-run full install
300
+ const installResult = await install(pkgRoot, projectRoot, stack)
301
+ results.copied.push(...installResult.created)
302
+ results.skipped.push(...installResult.skipped)
303
+
304
+ return results
305
+ }
306
+
307
+ function getManagedPaths(): ManagedPaths {
308
+ // Deduplicate dirs (e.g. promptsDir === workflowsDir for claude-code's 'commands')
309
+ const dirs = new Set(['agents', 'skills', ...config.frameworkDirs])
310
+ return {
311
+ framework: [
312
+ config.rootFile,
313
+ ...Array.from(dirs).map((d) => `${config.dotDir}/${d}/`),
314
+ ],
315
+ customizable: [`${config.dotDir}/customizations/`, config.mcpConfigPath],
316
+ }
317
+ }
318
+
319
+ return { install, update, getManagedPaths }
320
+ }
@@ -2,7 +2,7 @@ import { createServer } from 'node:http'
2
2
  import type { IncomingMessage, ServerResponse, Server } from 'node:http'
3
3
  import { readFile, access } from 'node:fs/promises'
4
4
  import { resolve, join, extname } from 'node:path'
5
- import { exec } from 'node:child_process'
5
+ import { execFile } from 'node:child_process'
6
6
  import type { CliContext } from './types.js'
7
7
 
8
8
  const MIME_TYPES: Record<string, string> = {
@@ -22,6 +22,7 @@ const DATA_FILES = [
22
22
  'sessions.ndjson',
23
23
  'delegations.ndjson',
24
24
  'panels.ndjson',
25
+ 'reviews.ndjson',
25
26
  ]
26
27
 
27
28
  interface DashboardArgs {
@@ -57,7 +58,7 @@ function openUrl(url: string): void {
57
58
  : plat === 'win32'
58
59
  ? 'start'
59
60
  : 'xdg-open'
60
- exec(`${cmd} ${url}`)
61
+ execFile(cmd, [url])
61
62
  }
62
63
 
63
64
  async function fileExists(filePath: string): Promise<boolean> {