opencastle 0.9.2 → 0.10.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 (182) hide show
  1. package/README.md +12 -69
  2. package/dist/cli/doctor.d.ts.map +1 -1
  3. package/dist/cli/doctor.js +13 -7
  4. package/dist/cli/doctor.js.map +1 -1
  5. package/dist/cli/init.d.ts.map +1 -1
  6. package/dist/cli/init.js +2 -1
  7. package/dist/cli/init.js.map +1 -1
  8. package/dist/cli/init.test.d.ts +17 -0
  9. package/dist/cli/init.test.d.ts.map +1 -0
  10. package/dist/cli/init.test.js +881 -0
  11. package/dist/cli/init.test.js.map +1 -0
  12. package/dist/cli/mcp.d.ts +9 -0
  13. package/dist/cli/mcp.d.ts.map +1 -1
  14. package/dist/cli/mcp.js +56 -0
  15. package/dist/cli/mcp.js.map +1 -1
  16. package/dist/cli/run/adapters/copilot.d.ts +10 -2
  17. package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
  18. package/dist/cli/run/adapters/copilot.js +83 -56
  19. package/dist/cli/run/adapters/copilot.js.map +1 -1
  20. package/dist/cli/run.js +2 -2
  21. package/dist/cli/run.js.map +1 -1
  22. package/dist/cli/stack-config-update.test.d.ts +2 -0
  23. package/dist/cli/stack-config-update.test.d.ts.map +1 -0
  24. package/dist/cli/stack-config-update.test.js +185 -0
  25. package/dist/cli/stack-config-update.test.js.map +1 -0
  26. package/dist/cli/stack-config.d.ts +27 -0
  27. package/dist/cli/stack-config.d.ts.map +1 -1
  28. package/dist/cli/stack-config.js +80 -27
  29. package/dist/cli/stack-config.js.map +1 -1
  30. package/dist/cli/types.d.ts +1 -1
  31. package/dist/cli/types.d.ts.map +1 -1
  32. package/dist/cli/update.d.ts.map +1 -1
  33. package/dist/cli/update.js +184 -17
  34. package/dist/cli/update.js.map +1 -1
  35. package/dist/orchestrator/plugins/astro/config.d.ts +3 -0
  36. package/dist/orchestrator/plugins/astro/config.d.ts.map +1 -0
  37. package/dist/orchestrator/plugins/astro/config.js +27 -0
  38. package/dist/orchestrator/plugins/astro/config.js.map +1 -0
  39. package/dist/orchestrator/plugins/chrome-devtools/config.js +2 -2
  40. package/dist/orchestrator/plugins/chrome-devtools/config.js.map +1 -1
  41. package/dist/orchestrator/plugins/contentful/config.js +1 -1
  42. package/dist/orchestrator/plugins/contentful/config.js.map +1 -1
  43. package/dist/orchestrator/plugins/convex/config.js +1 -1
  44. package/dist/orchestrator/plugins/convex/config.js.map +1 -1
  45. package/dist/orchestrator/plugins/cypress/config.d.ts +3 -0
  46. package/dist/orchestrator/plugins/cypress/config.d.ts.map +1 -0
  47. package/dist/orchestrator/plugins/cypress/config.js +15 -0
  48. package/dist/orchestrator/plugins/cypress/config.js.map +1 -0
  49. package/dist/orchestrator/plugins/figma/config.d.ts +3 -0
  50. package/dist/orchestrator/plugins/figma/config.d.ts.map +1 -0
  51. package/dist/orchestrator/plugins/figma/config.js +33 -0
  52. package/dist/orchestrator/plugins/figma/config.js.map +1 -0
  53. package/dist/orchestrator/plugins/index.d.ts.map +1 -1
  54. package/dist/orchestrator/plugins/index.js +20 -0
  55. package/dist/orchestrator/plugins/index.js.map +1 -1
  56. package/dist/orchestrator/plugins/jira/config.d.ts.map +1 -1
  57. package/dist/orchestrator/plugins/jira/config.js +2 -3
  58. package/dist/orchestrator/plugins/jira/config.js.map +1 -1
  59. package/dist/orchestrator/plugins/linear/config.js +2 -2
  60. package/dist/orchestrator/plugins/linear/config.js.map +1 -1
  61. package/dist/orchestrator/plugins/netlify/config.d.ts +3 -0
  62. package/dist/orchestrator/plugins/netlify/config.d.ts.map +1 -0
  63. package/dist/orchestrator/plugins/netlify/config.js +30 -0
  64. package/dist/orchestrator/plugins/netlify/config.js.map +1 -0
  65. package/dist/orchestrator/plugins/nextjs/config.d.ts +3 -0
  66. package/dist/orchestrator/plugins/nextjs/config.d.ts.map +1 -0
  67. package/dist/orchestrator/plugins/nextjs/config.js +35 -0
  68. package/dist/orchestrator/plugins/nextjs/config.js.map +1 -0
  69. package/dist/orchestrator/plugins/nx/config.d.ts.map +1 -1
  70. package/dist/orchestrator/plugins/nx/config.js +2 -3
  71. package/dist/orchestrator/plugins/nx/config.js.map +1 -1
  72. package/dist/orchestrator/plugins/playwright/config.d.ts +3 -0
  73. package/dist/orchestrator/plugins/playwright/config.d.ts.map +1 -0
  74. package/dist/orchestrator/plugins/playwright/config.js +25 -0
  75. package/dist/orchestrator/plugins/playwright/config.js.map +1 -0
  76. package/dist/orchestrator/plugins/prisma/config.d.ts +3 -0
  77. package/dist/orchestrator/plugins/prisma/config.d.ts.map +1 -0
  78. package/dist/orchestrator/plugins/prisma/config.js +25 -0
  79. package/dist/orchestrator/plugins/prisma/config.js.map +1 -0
  80. package/dist/orchestrator/plugins/resend/config.d.ts +3 -0
  81. package/dist/orchestrator/plugins/resend/config.d.ts.map +1 -0
  82. package/dist/orchestrator/plugins/resend/config.js +46 -0
  83. package/dist/orchestrator/plugins/resend/config.js.map +1 -0
  84. package/dist/orchestrator/plugins/sanity/config.d.ts.map +1 -1
  85. package/dist/orchestrator/plugins/sanity/config.js +1 -2
  86. package/dist/orchestrator/plugins/sanity/config.js.map +1 -1
  87. package/dist/orchestrator/plugins/slack/config.js +1 -1
  88. package/dist/orchestrator/plugins/slack/config.js.map +1 -1
  89. package/dist/orchestrator/plugins/strapi/config.js +1 -1
  90. package/dist/orchestrator/plugins/strapi/config.js.map +1 -1
  91. package/dist/orchestrator/plugins/supabase/config.d.ts.map +1 -1
  92. package/dist/orchestrator/plugins/supabase/config.js +1 -2
  93. package/dist/orchestrator/plugins/supabase/config.js.map +1 -1
  94. package/dist/orchestrator/plugins/teams/config.d.ts.map +1 -1
  95. package/dist/orchestrator/plugins/teams/config.js +1 -2
  96. package/dist/orchestrator/plugins/teams/config.js.map +1 -1
  97. package/dist/orchestrator/plugins/turborepo/config.d.ts +3 -0
  98. package/dist/orchestrator/plugins/turborepo/config.d.ts.map +1 -0
  99. package/dist/orchestrator/plugins/turborepo/config.js +15 -0
  100. package/dist/orchestrator/plugins/turborepo/config.js.map +1 -0
  101. package/dist/orchestrator/plugins/types.d.ts +7 -7
  102. package/dist/orchestrator/plugins/types.d.ts.map +1 -1
  103. package/dist/orchestrator/plugins/vercel/config.d.ts.map +1 -1
  104. package/dist/orchestrator/plugins/vercel/config.js +2 -3
  105. package/dist/orchestrator/plugins/vercel/config.js.map +1 -1
  106. package/dist/orchestrator/plugins/vitest/config.d.ts +3 -0
  107. package/dist/orchestrator/plugins/vitest/config.d.ts.map +1 -0
  108. package/dist/orchestrator/plugins/vitest/config.js +15 -0
  109. package/dist/orchestrator/plugins/vitest/config.js.map +1 -0
  110. package/package.json +2 -1
  111. package/src/cli/doctor.ts +14 -7
  112. package/src/cli/init.test.ts +1141 -0
  113. package/src/cli/init.ts +2 -1
  114. package/src/cli/mcp.ts +77 -1
  115. package/src/cli/run/adapters/copilot.ts +86 -58
  116. package/src/cli/run.ts +2 -2
  117. package/src/cli/stack-config-update.test.ts +210 -0
  118. package/src/cli/stack-config.ts +110 -37
  119. package/src/cli/types.ts +1 -1
  120. package/src/cli/update.ts +230 -23
  121. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  122. package/src/orchestrator/agents/api-designer.agent.md +1 -11
  123. package/src/orchestrator/agents/architect.agent.md +1 -9
  124. package/src/orchestrator/agents/content-engineer.agent.md +1 -5
  125. package/src/orchestrator/agents/copywriter.agent.md +1 -9
  126. package/src/orchestrator/agents/data-expert.agent.md +2 -6
  127. package/src/orchestrator/agents/database-engineer.agent.md +1 -6
  128. package/src/orchestrator/agents/developer.agent.md +2 -12
  129. package/src/orchestrator/agents/devops-expert.agent.md +1 -5
  130. package/src/orchestrator/agents/documentation-writer.agent.md +1 -4
  131. package/src/orchestrator/agents/performance-expert.agent.md +1 -5
  132. package/src/orchestrator/agents/release-manager.agent.md +1 -11
  133. package/src/orchestrator/agents/researcher.agent.md +1 -4
  134. package/src/orchestrator/agents/security-expert.agent.md +2 -7
  135. package/src/orchestrator/agents/seo-specialist.agent.md +1 -10
  136. package/src/orchestrator/agents/testing-expert.agent.md +2 -11
  137. package/src/orchestrator/agents/ui-ux-expert.agent.md +3 -10
  138. package/src/orchestrator/customizations/README.md +2 -1
  139. package/src/orchestrator/customizations/agents/skill-matrix.json +106 -0
  140. package/src/orchestrator/customizations/agents/skill-matrix.md +58 -121
  141. package/src/orchestrator/instructions/general.instructions.md +1 -1
  142. package/src/orchestrator/plugins/astro/SKILL.md +288 -0
  143. package/src/orchestrator/plugins/astro/config.ts +28 -0
  144. package/src/orchestrator/plugins/chrome-devtools/config.ts +2 -2
  145. package/src/orchestrator/plugins/contentful/config.ts +1 -1
  146. package/src/orchestrator/plugins/convex/config.ts +1 -1
  147. package/src/orchestrator/plugins/cypress/SKILL.md +145 -0
  148. package/src/orchestrator/plugins/cypress/config.ts +16 -0
  149. package/src/orchestrator/plugins/figma/SKILL.md +85 -0
  150. package/src/orchestrator/plugins/figma/config.ts +34 -0
  151. package/src/orchestrator/plugins/index.ts +20 -0
  152. package/src/orchestrator/plugins/jira/config.ts +2 -3
  153. package/src/orchestrator/plugins/linear/config.ts +2 -2
  154. package/src/orchestrator/plugins/netlify/SKILL.md +134 -0
  155. package/src/orchestrator/plugins/netlify/config.ts +31 -0
  156. package/src/orchestrator/plugins/nextjs/SKILL.md +376 -0
  157. package/src/orchestrator/plugins/nextjs/config.ts +36 -0
  158. package/src/orchestrator/plugins/nx/config.ts +2 -3
  159. package/src/orchestrator/plugins/playwright/SKILL.md +191 -0
  160. package/src/orchestrator/plugins/playwright/config.ts +26 -0
  161. package/src/orchestrator/plugins/prisma/SKILL.md +137 -0
  162. package/src/orchestrator/plugins/prisma/config.ts +26 -0
  163. package/src/orchestrator/plugins/resend/SKILL.md +187 -0
  164. package/src/orchestrator/plugins/resend/config.ts +47 -0
  165. package/src/orchestrator/plugins/sanity/config.ts +1 -2
  166. package/src/orchestrator/plugins/slack/config.ts +1 -1
  167. package/src/orchestrator/plugins/strapi/config.ts +1 -1
  168. package/src/orchestrator/plugins/supabase/config.ts +1 -2
  169. package/src/orchestrator/plugins/teams/config.ts +1 -2
  170. package/src/orchestrator/plugins/turborepo/SKILL.md +121 -0
  171. package/src/orchestrator/plugins/turborepo/config.ts +16 -0
  172. package/src/orchestrator/plugins/types.ts +7 -7
  173. package/src/orchestrator/plugins/vercel/SKILL.md +99 -0
  174. package/src/orchestrator/plugins/vercel/config.ts +2 -3
  175. package/src/orchestrator/plugins/vitest/SKILL.md +166 -0
  176. package/src/orchestrator/plugins/vitest/config.ts +16 -0
  177. package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +6 -4
  178. package/src/orchestrator/prompts/create-skill.prompt.md +6 -7
  179. package/src/orchestrator/prompts/generate-task-spec.prompt.md +1 -1
  180. package/src/orchestrator/skills/agent-hooks/SKILL.md +2 -2
  181. package/src/orchestrator/skills/memory-merger/SKILL.md +1 -1
  182. package/src/orchestrator/skills/nextjs-patterns/SKILL.md +0 -200
package/src/cli/init.ts CHANGED
@@ -88,6 +88,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
88
88
  ...(repoInfo.databases ?? []),
89
89
  ...(repoInfo.deployment ?? []),
90
90
  ...(repoInfo.monorepo ? [repoInfo.monorepo] : []),
91
+ ...((repoInfo.frameworks ?? []).map(f => f === 'next' ? 'nextjs' : f)),
91
92
  ])
92
93
 
93
94
  console.log(` ${c.bold('── Tech Tools ────────────────────────────────')}`)
@@ -248,7 +249,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
248
249
  // ── OAuth setup guides ────────────────────────────────────────
249
250
  if (teamTools.includes('slack')) {
250
251
  console.log(` ${c.cyan('📖')} Slack MCP requires a Slack App with a bot token.`)
251
- console.log(` Setup guide: ${c.cyan('https://www.opencastle.dev/guides/plugins#slack')}\n`)
252
+ console.log(` Setup guide: ${c.cyan('https://www.opencastle.dev/docs/plugins#slack')}\n`)
252
253
  }
253
254
 
254
255
  console.log(`\n ${c.bold('Next steps:')}`)
package/src/cli/mcp.ts CHANGED
@@ -111,7 +111,7 @@ export async function scaffoldMcpConfig(
111
111
 
112
112
  for (const plugin of Object.values(PLUGINS)) {
113
113
  if (plugin.mcpServerKey && included.has(plugin.mcpServerKey)) {
114
- servers[plugin.mcpServerKey] = plugin.mcpConfig as VsCodeServer;
114
+ servers[plugin.mcpServerKey] = plugin.mcpConfig! as VsCodeServer;
115
115
  if (plugin.mcpInputs) {
116
116
  inputs.push(...plugin.mcpInputs);
117
117
  }
@@ -181,3 +181,79 @@ export async function scaffoldMcpConfig(
181
181
 
182
182
  return { path: destPath, action: 'created' };
183
183
  }
184
+
185
+ // ── MCP config rebuild for reconfigure ────────────────────────
186
+
187
+ /**
188
+ * Returns the relative path to the MCP config file for a given IDE.
189
+ */
190
+ function getMcpConfigRelPath(ide: IdeChoice): string {
191
+ switch (ide) {
192
+ case 'vscode':
193
+ return '.vscode/mcp.json';
194
+ case 'cursor':
195
+ return '.cursor/mcp.json';
196
+ case 'claude-code':
197
+ return '.claude/mcp.json';
198
+ case 'opencode':
199
+ return 'opencode.json';
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Rebuild the MCP config for a specific IDE after a stack reconfigure.
205
+ *
206
+ * 1. Reads the existing MCP config
207
+ * 2. Removes all plugin-managed server entries
208
+ * 3. Preserves manually-added server entries
209
+ * 4. Re-scaffolds with the new stack selection
210
+ */
211
+ export async function rebuildMcpConfig(
212
+ projectRoot: string,
213
+ ide: IdeChoice,
214
+ stack: StackConfig,
215
+ repoInfo?: RepoInfo
216
+ ): Promise<void> {
217
+ const destRelPath = getMcpConfigRelPath(ide);
218
+ const destPath = resolve(projectRoot, destRelPath);
219
+
220
+ if (!existsSync(destPath)) {
221
+ // No existing config — scaffold fresh
222
+ await scaffoldMcpConfig(projectRoot, destRelPath, stack, repoInfo, ide);
223
+ return;
224
+ }
225
+
226
+ // Read existing config and strip all plugin-managed servers
227
+ const existing = JSON.parse(await readFile(destPath, 'utf8')) as Record<string, unknown>;
228
+ const containerKey =
229
+ ide === 'opencode' ? 'mcp' : ide === 'vscode' ? 'servers' : 'mcpServers';
230
+
231
+ const existingServers = (existing[containerKey] ?? {}) as Record<string, unknown>;
232
+
233
+ // Get all known plugin server keys
234
+ const allPluginServerKeys = new Set(
235
+ Object.values(PLUGINS)
236
+ .filter((p) => p.mcpServerKey)
237
+ .map((p) => p.mcpServerKey!)
238
+ );
239
+
240
+ // Remove all plugin-managed servers (they'll be re-added by scaffoldMcpConfig)
241
+ for (const key of Object.keys(existingServers)) {
242
+ if (allPluginServerKeys.has(key)) {
243
+ delete existingServers[key];
244
+ }
245
+ }
246
+
247
+ // Remove plugin-managed inputs (VS Code only)
248
+ if (ide === 'vscode') {
249
+ delete existing.inputs;
250
+ }
251
+
252
+ existing[containerKey] = existingServers;
253
+
254
+ // Write the cleaned config (preserving manually-added servers)
255
+ await writeFile(destPath, JSON.stringify(existing, null, 2) + '\n');
256
+
257
+ // Re-scaffold: merges new plugin servers into the cleaned config
258
+ await scaffoldMcpConfig(projectRoot, destRelPath, stack, repoInfo, ide);
259
+ }
@@ -1,11 +1,26 @@
1
1
  import { spawn } from 'node:child_process'
2
+ import type { CopilotClient as CopilotClientType, CopilotSession, PermissionHandler } from '@github/copilot-sdk'
2
3
  import type { Task, ExecuteOptions, ExecuteResult } from '../../types.js'
3
4
 
4
5
  /** Adapter name */
5
6
  export const name = 'copilot'
6
7
 
8
+ /**
9
+ * Lazy-initialized shared client instance.
10
+ * The client manages a single Copilot CLI server process; all task sessions
11
+ * multiplex over it via JSON-RPC.
12
+ */
13
+ let clientPromise: Promise<CopilotClientType> | null = null
14
+
15
+ /** Cached permission handler from the SDK module. */
16
+ let cachedApproveAll: PermissionHandler | null = null
17
+
18
+ /** Active sessions keyed by task id — used by `kill()` for timeout enforcement. */
19
+ const activeSessions = new Map<string, CopilotSession>()
20
+
7
21
  /**
8
22
  * Check if the `copilot` CLI is available on the system PATH.
23
+ * The SDK communicates with the CLI in server mode, so it must be installed.
9
24
  */
10
25
  export async function isAvailable(): Promise<boolean> {
11
26
  return new Promise((resolve) => {
@@ -16,7 +31,33 @@ export async function isAvailable(): Promise<boolean> {
16
31
  }
17
32
 
18
33
  /**
19
- * Execute a task by invoking the Copilot CLI in autopilot mode.
34
+ * Get or create the shared CopilotClient.
35
+ * The client is started once and reused across all task executions.
36
+ */
37
+ async function getClient(): Promise<CopilotClientType> {
38
+ if (!clientPromise) {
39
+ clientPromise = (async () => {
40
+ const { CopilotClient, approveAll } = await import('@github/copilot-sdk')
41
+ cachedApproveAll = approveAll
42
+ const client = new CopilotClient({
43
+ autoStart: false,
44
+ logLevel: 'error',
45
+ })
46
+ await client.start()
47
+ return client
48
+ })()
49
+ }
50
+ return clientPromise
51
+ }
52
+
53
+ /**
54
+ * Execute a task using the Copilot SDK.
55
+ *
56
+ * Each task gets its own session with:
57
+ * - All tool permissions auto-approved (equivalent to `--allow-all-tools`)
58
+ * - No `ask_user` tool (autonomous — equivalent to `--no-ask-user`)
59
+ * - System message injected with the agent role
60
+ * - Streaming enabled in verbose mode for live output
20
61
  */
21
62
  export async function execute(task: Task, options: ExecuteOptions = {}): Promise<ExecuteResult> {
22
63
  let prompt = `You are a ${task.agent}. ${task.prompt}`
@@ -25,73 +66,60 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
25
66
  prompt += `\n\nOnly modify files under: ${task.files.join(', ')}`
26
67
  }
27
68
 
28
- const args = [
29
- '-p',
30
- prompt,
31
- '--autopilot',
32
- '--allow-all-tools',
33
- '--no-ask-user',
34
- '-s',
35
- '--max-autopilot-continues',
36
- '50',
37
- ]
38
-
39
- return new Promise((resolve) => {
40
- const proc = spawn('copilot', args, {
41
- stdio: ['ignore', 'pipe', 'pipe'],
42
- env: { ...process.env },
43
- cwd: process.cwd(),
44
- })
45
-
46
- let stdout = ''
47
- let stderr = ''
69
+ const client = await getClient()
48
70
 
49
- proc.stdout.on('data', (chunk: Buffer) => {
50
- stdout += chunk.toString()
51
- if (options.verbose) {
52
- process.stdout.write(chunk)
53
- }
54
- })
71
+ const session = await client.createSession({
72
+ onPermissionRequest: cachedApproveAll!,
73
+ systemMessage: {
74
+ content: [
75
+ `You are a ${task.agent}.`,
76
+ 'Work autonomously without asking questions.',
77
+ 'Follow all instructions precisely.',
78
+ ].join(' '),
79
+ },
80
+ infiniteSessions: { enabled: false },
81
+ ...(options.verbose ? { streaming: true } : {}),
82
+ })
55
83
 
56
- proc.stderr.on('data', (chunk: Buffer) => {
57
- stderr += chunk.toString()
58
- if (options.verbose) {
59
- process.stderr.write(chunk)
60
- }
61
- })
84
+ activeSessions.set(task.id, session)
62
85
 
63
- proc.on('close', (code) => {
64
- const output = [stdout, stderr].filter(Boolean).join('\n')
65
- resolve({
66
- success: code === 0,
67
- output: output.slice(0, 10000), // Cap output size
68
- exitCode: code ?? -1,
69
- })
86
+ // Stream deltas to stdout in verbose mode
87
+ if (options.verbose) {
88
+ session.on('assistant.message_delta', (event: { data: { deltaContent: string } }) => {
89
+ process.stdout.write(event.data.deltaContent)
70
90
  })
91
+ }
71
92
 
72
- proc.on('error', (err) => {
73
- resolve({
74
- success: false,
75
- output: `Failed to spawn copilot: ${err.message}`,
76
- exitCode: -1,
77
- })
78
- })
93
+ try {
94
+ const response = await session.sendAndWait({ prompt })
95
+ const output = response?.data?.content ?? ''
79
96
 
80
- // Store process ref for potential timeout kill
81
- task._process = proc
82
- })
97
+ return {
98
+ success: true,
99
+ output: output.slice(0, 10_000),
100
+ exitCode: 0,
101
+ }
102
+ } catch (err: unknown) {
103
+ return {
104
+ success: false,
105
+ output: `Copilot SDK error: ${(err as Error).message}`,
106
+ exitCode: 1,
107
+ }
108
+ } finally {
109
+ activeSessions.delete(task.id)
110
+ await session.destroy().catch(() => {})
111
+ }
83
112
  }
84
113
 
85
114
  /**
86
- * Kill the process associated with a task (used by timeout enforcement).
115
+ * Abort and destroy the session associated with a task.
116
+ * Called by the executor when a task exceeds its timeout.
87
117
  */
88
118
  export function kill(task: Task): void {
89
- if (task._process && !task._process.killed) {
90
- task._process.kill('SIGTERM')
91
- setTimeout(() => {
92
- if (task._process && !task._process.killed) {
93
- task._process.kill('SIGKILL')
94
- }
95
- }, 5000)
119
+ const session = activeSessions.get(task.id)
120
+ if (session) {
121
+ session.abort().catch(() => {})
122
+ session.destroy().catch(() => {})
123
+ activeSessions.delete(task.id)
96
124
  }
97
125
  }
package/src/cli/run.ts CHANGED
@@ -119,8 +119,8 @@ export default async function run({ args }: CliContext): Promise<void> {
119
119
  ' Install: npm install -g @anthropic-ai/claude-code\n' +
120
120
  ' Docs: https://docs.anthropic.com/en/docs/claude-code',
121
121
  copilot:
122
- ' Install: npm install -g @anthropic-ai/claude-code (or use VS Code)\n' +
123
- ' The Copilot CLI is bundled with GitHub Copilot in VS Code.\n' +
122
+ ' Requires the Copilot CLI installed and authenticated:\n' +
123
+ ' https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli\n' +
124
124
  ' Docs: https://docs.github.com/en/copilot',
125
125
  cursor:
126
126
  ' The Cursor agent CLI ships with the Cursor editor.\n' +
@@ -0,0 +1,210 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { updateSkillMatrixContent } from './stack-config.js';
3
+ import type { StackConfig } from './types.js';
4
+ import type { SkillMatrixData } from './stack-config.js';
5
+
6
+ function makeTemplate(): SkillMatrixData {
7
+ return {
8
+ bindings: {
9
+ framework: {
10
+ entries: [],
11
+ description: 'SSR/SSG, routing, layouts, Server/Client Components',
12
+ },
13
+ database: { entries: [], description: 'Schema, migrations, auth flow, roles' },
14
+ cms: { entries: [], description: 'Document types, queries, schema management' },
15
+ deployment: {
16
+ entries: [],
17
+ description: 'Hosting, cron jobs, env vars, caching, headers',
18
+ },
19
+ 'codebase-tool': {
20
+ entries: [],
21
+ description: 'Task running, building, linting, testing, code generation',
22
+ },
23
+ testing: {
24
+ entries: [],
25
+ description: 'Unit testing frameworks, coverage, test planning',
26
+ },
27
+ 'e2e-testing': {
28
+ entries: [],
29
+ description: 'Browser automation, E2E testing, viewport testing, visual validation',
30
+ },
31
+ 'task-management': {
32
+ entries: [],
33
+ description: 'Issue tracking, naming, priorities, workflow states',
34
+ },
35
+ },
36
+ agents: {},
37
+ };
38
+ }
39
+
40
+ function templateJson(): string {
41
+ return JSON.stringify(makeTemplate(), null, 2) + '\n';
42
+ }
43
+
44
+ function parse(result: string): SkillMatrixData {
45
+ return JSON.parse(result);
46
+ }
47
+
48
+ describe('updateSkillMatrixContent', () => {
49
+ it('fills database slot when a database tool is selected', () => {
50
+ const stack: StackConfig = { ides: ['vscode'], techTools: ['supabase'], teamTools: [] };
51
+ const data = parse(updateSkillMatrixContent(templateJson(), stack));
52
+ expect(data.bindings.database.entries).toEqual([
53
+ { name: 'Supabase', skill: 'supabase-database' },
54
+ ]);
55
+ });
56
+
57
+ it('fills cms slot when a CMS tool is selected', () => {
58
+ const stack: StackConfig = { ides: ['vscode'], techTools: ['sanity'], teamTools: [] };
59
+ const data = parse(updateSkillMatrixContent(templateJson(), stack));
60
+ expect(data.bindings.cms.entries).toEqual([{ name: 'Sanity', skill: 'sanity-cms' }]);
61
+ });
62
+
63
+ it('fills framework slot when a framework tool is selected', () => {
64
+ const stack: StackConfig = { ides: ['vscode'], techTools: ['nextjs'], teamTools: [] };
65
+ const data = parse(updateSkillMatrixContent(templateJson(), stack));
66
+ expect(data.bindings.framework.entries).toEqual([
67
+ { name: 'Next.js', skill: 'nextjs-framework' },
68
+ ]);
69
+ });
70
+
71
+ it('fills deployment slot when a deployment tool is selected', () => {
72
+ const stack: StackConfig = { ides: ['vscode'], techTools: ['vercel'], teamTools: [] };
73
+ const data = parse(updateSkillMatrixContent(templateJson(), stack));
74
+ expect(data.bindings.deployment.entries).toEqual([
75
+ { name: 'Vercel', skill: 'vercel-deployment' },
76
+ ]);
77
+ });
78
+
79
+ it('fills codebase-tool slot when a monorepo tool is selected', () => {
80
+ const stack: StackConfig = { ides: ['vscode'], techTools: ['nx'], teamTools: [] };
81
+ const data = parse(updateSkillMatrixContent(templateJson(), stack));
82
+ expect(data.bindings['codebase-tool'].entries).toEqual([
83
+ { name: 'NX', skill: 'nx-workspace' },
84
+ ]);
85
+ });
86
+
87
+ it('fills task-management slot when a tracker tool is selected', () => {
88
+ const stack: StackConfig = { ides: ['vscode'], techTools: [], teamTools: ['linear'] };
89
+ const data = parse(updateSkillMatrixContent(templateJson(), stack));
90
+ expect(data.bindings['task-management'].entries).toEqual([
91
+ { name: 'Linear', skill: 'linear-task-management' },
92
+ ]);
93
+ });
94
+
95
+ it('clears database slot when no database tool is selected', () => {
96
+ const template = makeTemplate();
97
+ template.bindings.database.entries = [
98
+ { name: 'Supabase', skill: 'supabase-database' },
99
+ ];
100
+ const stack: StackConfig = { ides: ['vscode'], techTools: [], teamTools: [] };
101
+ const data = parse(
102
+ updateSkillMatrixContent(JSON.stringify(template, null, 2) + '\n', stack)
103
+ );
104
+ expect(data.bindings.database.entries).toEqual([]);
105
+ });
106
+
107
+ it('switches from one database to another', () => {
108
+ const template = makeTemplate();
109
+ template.bindings.database.entries = [
110
+ { name: 'Supabase', skill: 'supabase-database' },
111
+ ];
112
+ const stack: StackConfig = { ides: ['vscode'], techTools: ['convex'], teamTools: [] };
113
+ const data = parse(
114
+ updateSkillMatrixContent(JSON.stringify(template, null, 2) + '\n', stack)
115
+ );
116
+ expect(data.bindings.database.entries).toEqual([
117
+ { name: 'Convex', skill: 'convex-database' },
118
+ ]);
119
+ });
120
+
121
+ it('fills multiple slots at once', () => {
122
+ const stack: StackConfig = {
123
+ ides: ['vscode'],
124
+ techTools: ['supabase', 'sanity', 'vercel', 'nextjs'],
125
+ teamTools: ['linear'],
126
+ };
127
+ const data = parse(updateSkillMatrixContent(templateJson(), stack));
128
+ expect(data.bindings.database.entries).toEqual([
129
+ { name: 'Supabase', skill: 'supabase-database' },
130
+ ]);
131
+ expect(data.bindings.cms.entries).toEqual([{ name: 'Sanity', skill: 'sanity-cms' }]);
132
+ expect(data.bindings.deployment.entries).toEqual([
133
+ { name: 'Vercel', skill: 'vercel-deployment' },
134
+ ]);
135
+ expect(data.bindings.framework.entries).toEqual([
136
+ { name: 'Next.js', skill: 'nextjs-framework' },
137
+ ]);
138
+ expect(data.bindings['task-management'].entries).toEqual([
139
+ { name: 'Linear', skill: 'linear-task-management' },
140
+ ]);
141
+ });
142
+
143
+ it('does not modify unrelated slots', () => {
144
+ const stack: StackConfig = { ides: ['vscode'], techTools: ['supabase'], teamTools: [] };
145
+ const data = parse(updateSkillMatrixContent(templateJson(), stack));
146
+ expect(data.bindings.framework.entries).toEqual([]);
147
+ expect(data.bindings.cms.entries).toEqual([]);
148
+ });
149
+
150
+ it('supports multiple plugins in the same slot', () => {
151
+ const stack: StackConfig = {
152
+ ides: ['vscode'],
153
+ techTools: ['supabase', 'convex'],
154
+ teamTools: [],
155
+ };
156
+ const data = parse(updateSkillMatrixContent(templateJson(), stack));
157
+ expect(data.bindings.database.entries).toEqual([
158
+ { name: 'Supabase', skill: 'supabase-database' },
159
+ { name: 'Convex', skill: 'convex-database' },
160
+ ]);
161
+ });
162
+
163
+ it('supports multiple frameworks in the same slot', () => {
164
+ const stack: StackConfig = {
165
+ ides: ['vscode'],
166
+ techTools: ['nextjs', 'astro'],
167
+ teamTools: [],
168
+ };
169
+ const data = parse(updateSkillMatrixContent(templateJson(), stack));
170
+ expect(data.bindings.framework.entries).toEqual([
171
+ { name: 'Next.js', skill: 'nextjs-framework' },
172
+ { name: 'Astro', skill: 'astro-framework' },
173
+ ]);
174
+ });
175
+
176
+ it('supports multiple CMS tools in the same slot', () => {
177
+ const stack: StackConfig = {
178
+ ides: ['vscode'],
179
+ techTools: ['sanity', 'contentful'],
180
+ teamTools: [],
181
+ };
182
+ const data = parse(updateSkillMatrixContent(templateJson(), stack));
183
+ expect(data.bindings.cms.entries).toEqual([
184
+ { name: 'Sanity', skill: 'sanity-cms' },
185
+ { name: 'Contentful', skill: 'contentful-cms' },
186
+ ]);
187
+ });
188
+
189
+ it('preserves agents section', () => {
190
+ const template = makeTemplate();
191
+ template.agents = {
192
+ Developer: { slots: ['framework'], directSkills: ['validation-gates'] },
193
+ };
194
+ const stack: StackConfig = { ides: ['vscode'], techTools: ['supabase'], teamTools: [] };
195
+ const data = parse(
196
+ updateSkillMatrixContent(JSON.stringify(template, null, 2) + '\n', stack)
197
+ );
198
+ expect(data.agents.Developer).toEqual({
199
+ slots: ['framework'],
200
+ directSkills: ['validation-gates'],
201
+ });
202
+ });
203
+
204
+ it('outputs valid JSON with trailing newline', () => {
205
+ const stack: StackConfig = { ides: ['vscode'], techTools: ['supabase'], teamTools: [] };
206
+ const result = updateSkillMatrixContent(templateJson(), stack);
207
+ expect(result.endsWith('\n')).toBe(true);
208
+ expect(() => JSON.parse(result)).not.toThrow();
209
+ });
210
+ });
@@ -1,3 +1,6 @@
1
+ import { resolve } from 'node:path';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
1
4
  import type { TechTool, TeamTool, StackConfig, CopyDirOptions, RepoInfo } from './types.js';
2
5
  import {
3
6
  PLUGINS,
@@ -15,7 +18,7 @@ import type { PluginConfig } from '../orchestrator/plugins/types.js';
15
18
  interface ToolInfo {
16
19
  tech: string;
17
20
  skill: string | null;
18
- mcpServer: string | null;
21
+ mcpServer?: string;
19
22
  }
20
23
 
21
24
  /** All tech-tool metadata — derived from plugin configs. */
@@ -135,6 +138,23 @@ export function getRequiredMcpEnvVars(stack: StackConfig, repoInfo?: RepoInfo):
135
138
 
136
139
  // ── Customization file transforms ─────────────────────────────
137
140
 
141
+ // ── Skill matrix JSON types ────────────────────────────────────
142
+
143
+ export interface SkillMatrixEntry {
144
+ name: string;
145
+ skill: string;
146
+ }
147
+
148
+ export interface SkillMatrixSlot {
149
+ entries: SkillMatrixEntry[];
150
+ description: string;
151
+ }
152
+
153
+ export interface SkillMatrixData {
154
+ bindings: Record<string, SkillMatrixSlot>;
155
+ agents: Record<string, { slots: string[]; directSkills: string[] }>;
156
+ }
157
+
138
158
  /**
139
159
  * Return a transform callback that pre-populates customization files
140
160
  * based on the user's stack selection.
@@ -145,47 +165,13 @@ export function getCustomizationsTransform(
145
165
  stack: StackConfig
146
166
  ): NonNullable<CopyDirOptions['transform']> {
147
167
  return (content: string, srcPath: string) => {
148
- if (srcPath.endsWith('skill-matrix.md')) {
149
- return transformSkillMatrix(content, stack);
168
+ if (srcPath.endsWith('skill-matrix.json')) {
169
+ return updateSkillMatrixContent(content, stack);
150
170
  }
151
171
  return content;
152
172
  };
153
173
  }
154
174
 
155
- /**
156
- * Fill in the `database` and `cms` rows in the skill matrix
157
- * based on the user's stack selection.
158
- */
159
- function transformSkillMatrix(content: string, stack: StackConfig): string {
160
- let result = content;
161
-
162
- // Find first selected DB tool
163
- const db = stack.techTools.find((t) => (DB_TOOLS as readonly string[]).includes(t));
164
- if (db) {
165
- const info = TECH_TOOL_INFO[db as TechTool];
166
- if (info?.skill) {
167
- result = result.replace(
168
- /(\| `database`\s*\|)\s*\|(\s*\|)/,
169
- `$1 ${info.tech} | \`${info.skill}\` $2`
170
- );
171
- }
172
- }
173
-
174
- // Find first selected CMS tool
175
- const cms = stack.techTools.find((t) => (CMS_TOOLS as readonly string[]).includes(t));
176
- if (cms) {
177
- const info = TECH_TOOL_INFO[cms as TechTool];
178
- if (info?.skill) {
179
- result = result.replace(
180
- /(\| `cms`\s*\|)\s*\|(\s*\|)/,
181
- `$1 ${info.tech} | \`${info.skill}\` $2`
182
- );
183
- }
184
- }
185
-
186
- return result;
187
- }
188
-
189
175
  // ── Agent tool injection ──────────────────────────────────────
190
176
 
191
177
  /**
@@ -253,3 +239,90 @@ export function getAgentTransform(
253
239
  return `---\n${newFrontmatter}\n---\n${body}`;
254
240
  };
255
241
  }
242
+
243
+ // ── Skill matrix update ─────────────────────────────────────────
244
+
245
+ /** Mapping from plugin subCategory to skill matrix slot name. */
246
+ const SUBCATEGORY_TO_SLOT: Record<string, string> = {
247
+ database: 'database',
248
+ cms: 'cms',
249
+ deployment: 'deployment',
250
+ framework: 'framework',
251
+ 'codebase-tool': 'codebase-tool',
252
+ 'task-management': 'task-management',
253
+ testing: 'testing',
254
+ 'e2e-testing': 'e2e-testing',
255
+ };
256
+
257
+ /**
258
+ * Get the filesystem path to the skill matrix file for a given IDE.
259
+ */
260
+ function getSkillMatrixPath(projectRoot: string, ide: string): string {
261
+ const relativePath = 'customizations/agents/skill-matrix.json';
262
+ switch (ide) {
263
+ case 'vscode':
264
+ return resolve(projectRoot, '.github', relativePath);
265
+ case 'cursor':
266
+ return resolve(projectRoot, '.cursor', 'rules', relativePath);
267
+ case 'claude-code':
268
+ return resolve(projectRoot, '.claude', relativePath);
269
+ case 'opencode':
270
+ return resolve(projectRoot, '.opencode', relativePath);
271
+ default:
272
+ return '';
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Update the skill matrix file in-place for a specific IDE.
278
+ * Updates slot entries based on the user's current stack selections.
279
+ * Returns true if the file was updated, false if unchanged or missing.
280
+ */
281
+ export async function updateSkillMatrixFile(
282
+ projectRoot: string,
283
+ ide: string,
284
+ stack: StackConfig
285
+ ): Promise<boolean> {
286
+ const matrixPath = getSkillMatrixPath(projectRoot, ide);
287
+ if (!matrixPath || !existsSync(matrixPath)) return false;
288
+
289
+ const content = await readFile(matrixPath, 'utf8');
290
+ const updated = updateSkillMatrixContent(content, stack);
291
+ if (updated !== content) {
292
+ await writeFile(matrixPath, updated);
293
+ return true;
294
+ }
295
+ return false;
296
+ }
297
+
298
+ /**
299
+ * Update skill matrix JSON content based on stack selections.
300
+ * Pure function — sets slot entries for all plugin-mapped subcategories.
301
+ * Supports multiple plugins per slot (e.g. multiple databases).
302
+ */
303
+ export function updateSkillMatrixContent(content: string, stack: StackConfig): string {
304
+ const data: SkillMatrixData = JSON.parse(content);
305
+ const allTools = [...stack.techTools, ...stack.teamTools] as string[];
306
+
307
+ for (const [subCategory, slotName] of Object.entries(SUBCATEGORY_TO_SLOT)) {
308
+ // Find ALL selected tools matching this subcategory (not just the first)
309
+ const matchingTools = allTools.filter((toolId) => {
310
+ const plugin = PLUGINS[toolId];
311
+ return plugin?.subCategory === subCategory;
312
+ });
313
+
314
+ const entries: SkillMatrixEntry[] = matchingTools
315
+ .map((toolId) => {
316
+ const plugin = PLUGINS[toolId];
317
+ if (!plugin?.skillName) return null;
318
+ return { name: plugin.name, skill: plugin.skillName };
319
+ })
320
+ .filter((e): e is SkillMatrixEntry => e !== null);
321
+
322
+ if (data.bindings[slotName]) {
323
+ data.bindings[slotName].entries = entries;
324
+ }
325
+ }
326
+
327
+ return JSON.stringify(data, null, 2) + '\n';
328
+ }