@arvoretech/hub 0.6.0 → 0.6.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.
@@ -0,0 +1,1175 @@
1
+ // src/commands/generate.ts
2
+ import { Command } from "commander";
3
+ import { existsSync as existsSync2 } from "fs";
4
+ import { mkdir as mkdir2, writeFile as writeFile2, readdir as readdir2, copyFile, readFile as readFile3, cp } from "fs/promises";
5
+ import { join as join3, resolve } from "path";
6
+ import chalk2 from "chalk";
7
+ import inquirer from "inquirer";
8
+
9
+ // src/core/hub-config.ts
10
+ import { readFile } from "fs/promises";
11
+ import { join } from "path";
12
+ import { parse } from "yaml";
13
+ async function loadHubConfig(dir) {
14
+ const configPath = join(dir, "hub.yaml");
15
+ const content = await readFile(configPath, "utf-8");
16
+ return parse(content);
17
+ }
18
+
19
+ // src/core/hub-cache.ts
20
+ import { createHash } from "crypto";
21
+ import { existsSync } from "fs";
22
+ import { mkdir, readdir, readFile as readFile2, writeFile } from "fs/promises";
23
+ import { join as join2 } from "path";
24
+ import chalk from "chalk";
25
+ var HUB_DIR = ".hub";
26
+ var CONFIG_FILE = "config.json";
27
+ async function readCache(hubDir) {
28
+ const filePath = join2(hubDir, HUB_DIR, CONFIG_FILE);
29
+ if (!existsSync(filePath)) return {};
30
+ try {
31
+ const content = await readFile2(filePath, "utf-8");
32
+ return JSON.parse(content);
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+ async function writeCache(hubDir, cache) {
38
+ const dir = join2(hubDir, HUB_DIR);
39
+ await mkdir(dir, { recursive: true });
40
+ await writeFile(join2(dir, CONFIG_FILE), JSON.stringify(cache, null, 2) + "\n", "utf-8");
41
+ }
42
+ async function getSavedEditor(hubDir) {
43
+ const cache = await readCache(hubDir);
44
+ return cache.editor;
45
+ }
46
+ async function collectFileHashes(dir, extensions) {
47
+ if (!existsSync(dir)) return [];
48
+ const entries = await readdir(dir, { withFileTypes: true });
49
+ const hashes = [];
50
+ for (const entry of entries) {
51
+ const fullPath = join2(dir, entry.name);
52
+ if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
53
+ const content = await readFile2(fullPath, "utf-8");
54
+ hashes.push(`${entry.name}:${createHash("sha256").update(content).digest("hex")}`);
55
+ } else if (entry.isDirectory()) {
56
+ const subHashes = await collectFileHashes(fullPath, extensions);
57
+ hashes.push(...subHashes.map((h) => `${entry.name}/${h}`));
58
+ }
59
+ }
60
+ return hashes.sort();
61
+ }
62
+ async function computeInputsHash(hubDir) {
63
+ const parts = [];
64
+ const hubYamlPath = join2(hubDir, "hub.yaml");
65
+ if (existsSync(hubYamlPath)) {
66
+ const content = await readFile2(hubYamlPath, "utf-8");
67
+ parts.push(`hub.yaml:${createHash("sha256").update(content).digest("hex")}`);
68
+ }
69
+ const dirs = ["agents", "skills", "hooks", "commands"];
70
+ for (const dir of dirs) {
71
+ const dirHashes = await collectFileHashes(join2(hubDir, dir), [".md", ".sh"]);
72
+ parts.push(...dirHashes.map((h) => `${dir}/${h}`));
73
+ }
74
+ return createHash("sha256").update(parts.join("\n")).digest("hex");
75
+ }
76
+ async function saveGenerateState(hubDir, editor) {
77
+ const hash = await computeInputsHash(hubDir);
78
+ const cache = await readCache(hubDir);
79
+ cache.editor = editor;
80
+ cache.lastGenerate = {
81
+ hash,
82
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
83
+ editor
84
+ };
85
+ await writeCache(hubDir, cache);
86
+ }
87
+ async function checkOutdated(hubDir) {
88
+ const cache = await readCache(hubDir);
89
+ if (!cache.lastGenerate) {
90
+ return { outdated: false, reason: "no-previous-generate" };
91
+ }
92
+ const currentHash = await computeInputsHash(hubDir);
93
+ if (currentHash !== cache.lastGenerate.hash) {
94
+ return {
95
+ outdated: true,
96
+ editor: cache.editor,
97
+ reason: "inputs-changed"
98
+ };
99
+ }
100
+ return { outdated: false, editor: cache.editor };
101
+ }
102
+ async function checkAndAutoRegenerate(hubDir) {
103
+ try {
104
+ const result = await checkOutdated(hubDir);
105
+ if (!result.outdated) return;
106
+ if (!result.editor) {
107
+ console.log(
108
+ chalk.yellow(
109
+ "\n Configs are outdated. Run 'hub generate' to regenerate.\n"
110
+ )
111
+ );
112
+ return;
113
+ }
114
+ console.log(chalk.yellow("\n Detected outdated configs, auto-regenerating..."));
115
+ const { generators: generators2 } = await import("./generate-G3ZOX55F.js");
116
+ const generator = generators2[result.editor];
117
+ if (!generator) {
118
+ console.log(chalk.red(` Unknown editor '${result.editor}' in cache. Run 'hub generate' manually.`));
119
+ return;
120
+ }
121
+ const config = await loadHubConfig(hubDir);
122
+ await generator.generate(config, hubDir);
123
+ await saveGenerateState(hubDir, result.editor);
124
+ console.log(chalk.green(" Auto-regeneration complete!\n"));
125
+ } catch (err) {
126
+ console.log(chalk.yellow(` Auto-regeneration failed: ${err.message}`));
127
+ console.log(chalk.dim(" Run 'hub generate' manually to fix.\n"));
128
+ }
129
+ }
130
+
131
+ // src/commands/generate.ts
132
+ var HUB_MARKER_START = "# >>> hub-managed (do not edit this section)";
133
+ var HUB_MARKER_END = "# <<< hub-managed";
134
+ var HOOK_EVENT_MAP = {
135
+ session_start: { cursor: "sessionStart", claude: "SessionStart", kiro: void 0 },
136
+ session_end: { cursor: "sessionEnd", claude: "SessionEnd", kiro: void 0 },
137
+ pre_tool_use: { cursor: "preToolUse", claude: "PreToolUse", kiro: "pre_tool_use" },
138
+ post_tool_use: { cursor: "postToolUse", claude: "PostToolUse", kiro: "post_tool_use" },
139
+ post_tool_use_failure: { cursor: void 0, claude: "PostToolUseFailure", kiro: void 0 },
140
+ stop: { cursor: "stop", claude: "Stop", kiro: "agent_stop" },
141
+ subagent_start: { cursor: "subagentStart", claude: "SubagentStart", kiro: void 0 },
142
+ subagent_stop: { cursor: "subagentStop", claude: "SubagentStop", kiro: void 0 },
143
+ pre_compact: { cursor: "preCompact", claude: "PreCompact", kiro: void 0 },
144
+ before_submit_prompt: { cursor: "beforeSubmitPrompt", claude: "UserPromptSubmit", kiro: "prompt_submit" },
145
+ before_shell_execution: { cursor: "beforeShellExecution", claude: void 0, kiro: void 0 },
146
+ after_shell_execution: { cursor: "afterShellExecution", claude: void 0, kiro: void 0 },
147
+ before_mcp_execution: { cursor: "beforeMCPExecution", claude: void 0, kiro: void 0 },
148
+ after_mcp_execution: { cursor: "afterMCPExecution", claude: void 0, kiro: void 0 },
149
+ after_file_edit: { cursor: "afterFileEdit", claude: void 0, kiro: "file_save" },
150
+ before_read_file: { cursor: "beforeReadFile", claude: void 0, kiro: void 0 },
151
+ before_tab_file_read: { cursor: "beforeTabFileRead", claude: void 0, kiro: void 0 },
152
+ after_tab_file_edit: { cursor: "afterTabFileEdit", claude: void 0, kiro: void 0 },
153
+ after_agent_response: { cursor: "afterAgentResponse", claude: void 0, kiro: void 0 },
154
+ after_agent_thought: { cursor: "afterAgentThought", claude: void 0, kiro: void 0 },
155
+ notification: { cursor: void 0, claude: "Notification", kiro: void 0 },
156
+ permission_request: { cursor: void 0, claude: "PermissionRequest", kiro: void 0 },
157
+ task_completed: { cursor: void 0, claude: "TaskCompleted", kiro: void 0 },
158
+ teammate_idle: { cursor: void 0, claude: "TeammateIdle", kiro: void 0 }
159
+ };
160
+ function buildCursorHooks(hooks) {
161
+ const cursorHooks = {};
162
+ for (const [event, entries] of Object.entries(hooks)) {
163
+ const mapped = HOOK_EVENT_MAP[event]?.cursor;
164
+ if (!mapped) continue;
165
+ const cursorEntries = entries.map((entry) => {
166
+ const obj = { type: entry.type };
167
+ if (entry.type === "command" && entry.command) obj.command = entry.command;
168
+ if (entry.type === "prompt" && entry.prompt) obj.prompt = entry.prompt;
169
+ if (entry.matcher) obj.matcher = entry.matcher;
170
+ if (entry.timeout_ms) obj.timeout = entry.timeout_ms;
171
+ return obj;
172
+ });
173
+ if (cursorEntries.length > 0) {
174
+ cursorHooks[mapped] = cursorEntries;
175
+ }
176
+ }
177
+ if (Object.keys(cursorHooks).length === 0) return null;
178
+ return { version: 1, hooks: cursorHooks };
179
+ }
180
+ function buildClaudeHooks(hooks) {
181
+ const claudeHooks = {};
182
+ for (const [event, entries] of Object.entries(hooks)) {
183
+ const mapped = HOOK_EVENT_MAP[event]?.claude;
184
+ if (!mapped) continue;
185
+ const claudeEntries = entries.map((entry) => {
186
+ const obj = { type: entry.type };
187
+ if (entry.type === "command" && entry.command) obj.command = entry.command;
188
+ if (entry.type === "prompt" && entry.prompt) obj.prompt = entry.prompt;
189
+ if (entry.matcher) obj.matcher = entry.matcher;
190
+ if (entry.timeout_ms) obj.timeout = entry.timeout_ms;
191
+ return obj;
192
+ });
193
+ if (claudeEntries.length > 0) {
194
+ claudeHooks[mapped] = claudeEntries;
195
+ }
196
+ }
197
+ if (Object.keys(claudeHooks).length === 0) return null;
198
+ return claudeHooks;
199
+ }
200
+ async function generateCursorCommands(config, hubDir, cursorDir) {
201
+ const commandsDir = join3(cursorDir, "commands");
202
+ let count = 0;
203
+ if (config.commands_dir) {
204
+ const srcDir = resolve(hubDir, config.commands_dir);
205
+ try {
206
+ const files = await readdir2(srcDir);
207
+ const mdFiles = files.filter((f) => f.endsWith(".md"));
208
+ if (mdFiles.length > 0) {
209
+ await mkdir2(commandsDir, { recursive: true });
210
+ for (const file of mdFiles) {
211
+ await copyFile(join3(srcDir, file), join3(commandsDir, file));
212
+ count++;
213
+ }
214
+ }
215
+ } catch {
216
+ console.log(chalk2.yellow(` Commands directory ${config.commands_dir} not found, skipping`));
217
+ }
218
+ }
219
+ if (config.commands) {
220
+ await mkdir2(commandsDir, { recursive: true });
221
+ for (const [name, filePath] of Object.entries(config.commands)) {
222
+ const src = resolve(hubDir, filePath);
223
+ const dest = join3(commandsDir, name.endsWith(".md") ? name : `${name}.md`);
224
+ try {
225
+ await copyFile(src, dest);
226
+ count++;
227
+ } catch {
228
+ console.log(chalk2.yellow(` Command file ${filePath} not found, skipping`));
229
+ }
230
+ }
231
+ }
232
+ if (count > 0) {
233
+ console.log(chalk2.green(` Copied ${count} commands to .cursor/commands/`));
234
+ }
235
+ }
236
+ async function writeManagedFile(filePath, managedLines) {
237
+ const managedBlock = [HUB_MARKER_START, ...managedLines, HUB_MARKER_END].join("\n");
238
+ if (existsSync2(filePath)) {
239
+ const existing = await readFile3(filePath, "utf-8");
240
+ const startIdx = existing.indexOf(HUB_MARKER_START);
241
+ const endIdx = existing.indexOf(HUB_MARKER_END);
242
+ if (startIdx !== -1 && endIdx !== -1) {
243
+ const before = existing.substring(0, startIdx);
244
+ const after = existing.substring(endIdx + HUB_MARKER_END.length);
245
+ await writeFile2(filePath, before + managedBlock + after, "utf-8");
246
+ return;
247
+ }
248
+ await writeFile2(filePath, managedBlock + "\n\n" + existing, "utf-8");
249
+ return;
250
+ }
251
+ await writeFile2(filePath, managedBlock + "\n", "utf-8");
252
+ }
253
+ async function generateCursor(config, hubDir) {
254
+ const cursorDir = join3(hubDir, ".cursor");
255
+ await mkdir2(join3(cursorDir, "rules"), { recursive: true });
256
+ await mkdir2(join3(cursorDir, "agents"), { recursive: true });
257
+ const gitignoreLines = buildGitignoreLines(config);
258
+ await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
259
+ console.log(chalk2.green(" Generated .gitignore"));
260
+ const cursorignoreLines = [
261
+ "# Re-include repositories for AI context"
262
+ ];
263
+ for (const repo of config.repos) {
264
+ const repoDir = repo.path.replace("./", "");
265
+ cursorignoreLines.push(`!${repoDir}/`);
266
+ }
267
+ cursorignoreLines.push("", "# Re-include tasks for agent collaboration", "!tasks/");
268
+ await writeManagedFile(join3(hubDir, ".cursorignore"), cursorignoreLines);
269
+ console.log(chalk2.green(" Generated .cursorignore"));
270
+ if (config.mcps?.length) {
271
+ const mcpConfig = {};
272
+ for (const mcp of config.mcps) {
273
+ mcpConfig[mcp.name] = buildCursorMcpEntry(mcp);
274
+ }
275
+ await writeFile2(
276
+ join3(cursorDir, "mcp.json"),
277
+ JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
278
+ "utf-8"
279
+ );
280
+ console.log(chalk2.green(" Generated .cursor/mcp.json"));
281
+ }
282
+ const orchestratorRule = buildOrchestratorRule(config);
283
+ await writeFile2(join3(cursorDir, "rules", "orchestrator.mdc"), orchestratorRule, "utf-8");
284
+ console.log(chalk2.green(" Generated .cursor/rules/orchestrator.mdc"));
285
+ const agentsDir = resolve(hubDir, "agents");
286
+ try {
287
+ const agentFiles = await readdir2(agentsDir);
288
+ const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
289
+ for (const file of mdFiles) {
290
+ await copyFile(join3(agentsDir, file), join3(cursorDir, "agents", file));
291
+ }
292
+ console.log(chalk2.green(` Copied ${mdFiles.length} agent definitions`));
293
+ } catch {
294
+ console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
295
+ }
296
+ const skillsDir = resolve(hubDir, "skills");
297
+ try {
298
+ const skillFolders = await readdir2(skillsDir);
299
+ const cursorSkillsDir = join3(cursorDir, "skills");
300
+ await mkdir2(cursorSkillsDir, { recursive: true });
301
+ let count = 0;
302
+ for (const folder of skillFolders) {
303
+ const skillFile = join3(skillsDir, folder, "SKILL.md");
304
+ try {
305
+ await readFile3(skillFile);
306
+ const srcDir = join3(skillsDir, folder);
307
+ const targetDir = join3(cursorSkillsDir, folder);
308
+ await cp(srcDir, targetDir, { recursive: true });
309
+ count++;
310
+ } catch {
311
+ }
312
+ }
313
+ if (count > 0) {
314
+ console.log(chalk2.green(` Copied ${count} skills`));
315
+ }
316
+ } catch {
317
+ }
318
+ if (config.hooks) {
319
+ const cursorHooks = buildCursorHooks(config.hooks);
320
+ if (cursorHooks) {
321
+ await writeFile2(
322
+ join3(cursorDir, "hooks.json"),
323
+ JSON.stringify(cursorHooks, null, 2) + "\n",
324
+ "utf-8"
325
+ );
326
+ console.log(chalk2.green(" Generated .cursor/hooks.json"));
327
+ }
328
+ }
329
+ await generateCursorCommands(config, hubDir, cursorDir);
330
+ await generateVSCodeSettings(config, hubDir);
331
+ }
332
+ function buildCursorMcpEntry(mcp) {
333
+ if (mcp.url) {
334
+ return { url: mcp.url, ...mcp.env && { env: mcp.env } };
335
+ }
336
+ if (mcp.image) {
337
+ const args = ["run", "-i", "--rm"];
338
+ if (mcp.env) {
339
+ for (const [key, value] of Object.entries(mcp.env)) {
340
+ args.push("-e", `${key}=${value}`);
341
+ }
342
+ }
343
+ args.push(mcp.image);
344
+ return { command: "docker", args };
345
+ }
346
+ return {
347
+ command: "npx",
348
+ args: ["-y", mcp.package],
349
+ ...mcp.env && { env: mcp.env }
350
+ };
351
+ }
352
+ function buildClaudeCodeMcpEntry(mcp) {
353
+ if (mcp.url) {
354
+ return { type: "http", url: mcp.url };
355
+ }
356
+ if (mcp.image) {
357
+ const args = ["run", "-i", "--rm"];
358
+ if (mcp.env) {
359
+ for (const [key, value] of Object.entries(mcp.env)) {
360
+ args.push("-e", `${key}=${value}`);
361
+ }
362
+ }
363
+ args.push(mcp.image);
364
+ return { command: "docker", args };
365
+ }
366
+ return {
367
+ command: "npx",
368
+ args: ["-y", mcp.package],
369
+ ...mcp.env && { env: mcp.env }
370
+ };
371
+ }
372
+ function buildKiroMcpEntry(mcp) {
373
+ if (mcp.url) {
374
+ return { url: mcp.url, ...mcp.env && { env: mcp.env } };
375
+ }
376
+ if (mcp.image) {
377
+ const args = ["run", "-i", "--rm"];
378
+ if (mcp.env) {
379
+ for (const [key, value] of Object.entries(mcp.env)) {
380
+ args.push("-e", `${key}=${value}`);
381
+ }
382
+ }
383
+ args.push(mcp.image);
384
+ return { command: "docker", args };
385
+ }
386
+ return {
387
+ command: "npx",
388
+ args: ["-y", mcp.package],
389
+ ...mcp.env && { env: mcp.env }
390
+ };
391
+ }
392
+ function buildKiroSteeringContent(content, inclusion = "always", meta) {
393
+ const frontMatter = ["---", `inclusion: ${inclusion}`];
394
+ if (meta?.name) frontMatter.push(`name: ${meta.name}`);
395
+ if (meta?.description) frontMatter.push(`description: ${meta.description}`);
396
+ frontMatter.push("---");
397
+ return `${frontMatter.join("\n")}
398
+
399
+ ${content}`;
400
+ }
401
+ function buildKiroOrchestratorRule(config) {
402
+ const taskFolder = config.workflow?.task_folder || "./tasks/<TASK_ID>/";
403
+ const steps = config.workflow?.pipeline || [];
404
+ const prompt = config.workflow?.prompt;
405
+ const enforce = config.workflow?.enforce_workflow ?? false;
406
+ const sections = [];
407
+ sections.push(`# Orchestrator
408
+
409
+ ## Your Main Responsibility
410
+
411
+ You are the development orchestrator. Your job is to ensure that any feature or task requested by the user is completed end-to-end by following a structured pipeline. You work as a single agent but follow specialized instructions from steering files for each phase of development.
412
+
413
+ > **Note:** This workspace uses steering files in \`.kiro/steering/\` to provide role-specific instructions for each pipeline step. When a step says "follow the instructions from steering file X", read that file and apply its guidelines to the current task.`);
414
+ if (enforce) {
415
+ sections.push(`
416
+ ## STRICT WORKFLOW ENFORCEMENT
417
+
418
+ **YOU MUST FOLLOW THE PIPELINE DEFINED BELOW. NO EXCEPTIONS.**
419
+
420
+ - NEVER skip a pipeline step, even if the task seems simple or obvious.
421
+ - ALWAYS execute steps in the exact order defined. Do not reorder, merge, or parallelize steps unless the pipeline explicitly allows it.
422
+ - ALWAYS follow the designated steering file for each step. Do not improvise if a steering file is assigned.
423
+ - ALWAYS wait for a step to complete and validate its output before moving to the next step.
424
+ - If a step produces a document, READ the document and confirm it is complete before proceeding.
425
+ - If a step has unanswered questions or validation issues, RESOLVE them before advancing.
426
+ - NEVER jump directly to coding without completing refinement first.
427
+ - NEVER skip review or QA steps, even for small changes.
428
+ - If the user asks you to skip a step, explain why the pipeline exists and ask for explicit confirmation before proceeding.`);
429
+ }
430
+ if (prompt?.prepend) {
431
+ sections.push(`
432
+ ${prompt.prepend.trim()}`);
433
+ }
434
+ if (config.integrations?.linear) {
435
+ const linear = config.integrations.linear;
436
+ sections.push(`
437
+ ## Task Management
438
+
439
+ If the user doesn't have a task in their project management tool, create one using the Linear MCP.${linear.team ? ` Add it to the **${linear.team}** team.` : ""} Provide the link to the user so they can review and modify as needed.`);
440
+ }
441
+ sections.push(`
442
+ ## Repositories
443
+ `);
444
+ for (const repo of config.repos) {
445
+ const parts = [`- **${repo.path}**`];
446
+ if (repo.description) parts.push(`\u2014 ${repo.description}`);
447
+ else if (repo.tech) parts.push(`\u2014 ${repo.tech}`);
448
+ if (repo.skills?.length) parts.push(`(skills: ${repo.skills.join(", ")})`);
449
+ sections.push(parts.join(" "));
450
+ if (repo.commands) {
451
+ const cmds = Object.entries(repo.commands).filter(([, v]) => v).map(([k, v]) => `\`${k}\`: \`${v}\``).join(", ");
452
+ if (cmds) sections.push(` Commands: ${cmds}`);
453
+ }
454
+ }
455
+ if (prompt?.sections?.after_repositories) {
456
+ sections.push(`
457
+ ${prompt.sections.after_repositories.trim()}`);
458
+ }
459
+ const docStructure = buildDocumentStructure(steps, taskFolder);
460
+ sections.push(docStructure);
461
+ const pipelineSection = buildKiroPipelineSection(steps);
462
+ sections.push(pipelineSection);
463
+ if (prompt?.sections?.after_pipeline) {
464
+ sections.push(`
465
+ ${prompt.sections.after_pipeline.trim()}`);
466
+ }
467
+ if (config.integrations?.slack || config.integrations?.github) {
468
+ sections.push(buildDeliverySection(config));
469
+ }
470
+ if (prompt?.sections?.after_delivery) {
471
+ sections.push(`
472
+ ${prompt.sections.after_delivery.trim()}`);
473
+ }
474
+ if (config.memory) {
475
+ sections.push(`
476
+ ## Team Memory
477
+
478
+ This workspace has a team memory knowledge base available via the \`team-memory\` MCP.
479
+
480
+ **Before starting any task**, use \`search_memories\` to find relevant context \u2014 past decisions, conventions, known issues, and domain knowledge. This avoids repeating mistakes and ensures consistency with previous choices.
481
+
482
+ **After completing a task**, if you discovered something valuable (a decision, a gotcha, a convention, domain insight), use \`add_memory\` to capture it for the team.
483
+
484
+ Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_memories\`, \`archive_memory\`, \`remove_memory\`.`);
485
+ }
486
+ sections.push(`
487
+ ## Troubleshooting and Debugging
488
+
489
+ For bug reports or unexpected behavior, follow the debugging process from the \`agent-debugger.md\` steering file (if available), or:
490
+ 1. Collect context (symptoms, environment, timeline)
491
+ 2. Analyze logs and stack traces
492
+ 3. Form and test hypotheses systematically
493
+ 4. Identify the root cause
494
+ 5. Propose and implement the fix`);
495
+ if (prompt?.sections) {
496
+ const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
497
+ for (const [name, content] of Object.entries(prompt.sections)) {
498
+ if (reservedKeys.has(name)) continue;
499
+ const title = name.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
500
+ sections.push(`
501
+ ## ${title}
502
+
503
+ ${content.trim()}`);
504
+ }
505
+ }
506
+ if (prompt?.append) {
507
+ sections.push(`
508
+ ${prompt.append.trim()}`);
509
+ }
510
+ return sections.join("\n");
511
+ }
512
+ function buildKiroPipelineSection(steps) {
513
+ if (steps.length === 0) {
514
+ return `
515
+ ## Development Pipeline
516
+
517
+ Since Kiro does not support sub-agents, follow each step sequentially, applying the guidelines from the corresponding steering file:
518
+
519
+ 1. **Refinement** \u2014 Read and follow \`agent-refinement.md\` steering file to collect requirements. Write output to the task document.
520
+ 2. **Coding** \u2014 Follow the coding steering files (\`agent-coding-backend.md\`, \`agent-coding-frontend.md\`) to implement the feature.
521
+ 3. **Review** \u2014 Follow \`agent-code-reviewer.md\` to review the implementation.
522
+ 4. **QA** \u2014 Follow \`agent-qa-backend.md\` and/or \`agent-qa-frontend.md\` to test.
523
+ 5. **Delivery** \u2014 Create PRs and notify the team.`;
524
+ }
525
+ const parts = [`
526
+ ## Development Pipeline
527
+
528
+ Follow each step sequentially, applying the role-specific instructions from the corresponding steering file at each phase.
529
+ `];
530
+ for (const step of steps) {
531
+ if (step.actions) {
532
+ parts.push(`### Delivery`);
533
+ parts.push(`After all validations pass, execute these actions:`);
534
+ for (const action of step.actions) {
535
+ parts.push(`- ${formatAction(action)}`);
536
+ }
537
+ continue;
538
+ }
539
+ const stepTitle = step.step.charAt(0).toUpperCase() + step.step.slice(1);
540
+ parts.push(`### ${stepTitle}`);
541
+ if (step.mode === "plan") {
542
+ parts.push(`**This step is a planning phase.** Do NOT make any code changes. Focus on reading, analyzing, and collaborating with the user to define requirements before proceeding.`);
543
+ parts.push(``);
544
+ }
545
+ if (step.agent) {
546
+ parts.push(`Follow the instructions from the \`agent-${step.agent}.md\` steering file.${step.output ? ` Write output to \`${step.output}\`.` : ""}`);
547
+ if (step.step === "refinement") {
548
+ parts.push(`
549
+ After completing the refinement, validate with the user:
550
+ - If there are unanswered questions, ask the user one at a time
551
+ - If the user requests adjustments, revisit the refinement
552
+ - Do not proceed until the document is complete and approved by the user`);
553
+ }
554
+ }
555
+ if (Array.isArray(step.agents)) {
556
+ const agentList = step.agents.map((a) => {
557
+ if (typeof a === "string") return { agent: a };
558
+ return a;
559
+ });
560
+ parts.push(`Follow the instructions from these steering files sequentially:`);
561
+ for (const a of agentList) {
562
+ let line = `- \`agent-${a.agent}.md\``;
563
+ if (a.output) line += ` \u2192 write to \`${a.output}\``;
564
+ if (a.when) line += ` (when: ${a.when})`;
565
+ parts.push(line);
566
+ }
567
+ if (step.step === "coding" || step.step === "code" || step.step === "implementation") {
568
+ parts.push(`
569
+ If you encounter doubts during coding, write questions in the task document and validate with the user before proceeding.`);
570
+ }
571
+ if (step.step === "validation" || step.step === "review" || step.step === "qa") {
572
+ parts.push(`
573
+ If any validation step reveals issues requiring fixes, go back to the relevant coding step to address them.`);
574
+ }
575
+ }
576
+ parts.push("");
577
+ }
578
+ return parts.join("\n");
579
+ }
580
+ function buildOrchestratorRule(config) {
581
+ const taskFolder = config.workflow?.task_folder || "./tasks/<TASK_ID>/";
582
+ const steps = config.workflow?.pipeline || [];
583
+ const prompt = config.workflow?.prompt;
584
+ const enforce = config.workflow?.enforce_workflow ?? false;
585
+ const sections = [];
586
+ sections.push(`---
587
+ description: "Orchestrator agent \u2014 coordinates sub-agents through the development pipeline"
588
+ alwaysApply: true
589
+ ---
590
+
591
+ # Orchestrator
592
+
593
+ ## Your Main Responsibility
594
+
595
+ You are an agent orchestrator. Your job is to ensure that any feature or task requested by the user is completed end-to-end using specialized sub-agents.`);
596
+ if (enforce) {
597
+ sections.push(`
598
+ ## STRICT WORKFLOW ENFORCEMENT
599
+
600
+ **YOU MUST FOLLOW THE PIPELINE DEFINED BELOW. NO EXCEPTIONS.**
601
+
602
+ - NEVER skip a pipeline step, even if the task seems simple or obvious.
603
+ - ALWAYS execute steps in the exact order defined. Do not reorder, merge, or parallelize steps unless the pipeline explicitly allows it.
604
+ - ALWAYS call the designated sub-agent for each step. Do not attempt to perform a step yourself if an agent is assigned to it.
605
+ - ALWAYS wait for a step to complete and validate its output before moving to the next step.
606
+ - If a step produces a document, READ the document and confirm it is complete before proceeding.
607
+ - If a step has unanswered questions or validation issues, RESOLVE them before advancing.
608
+ - NEVER jump directly to coding without completing refinement first.
609
+ - NEVER skip review or QA steps, even for small changes.
610
+ - If the user asks you to skip a step, explain why the pipeline exists and ask for explicit confirmation before proceeding.`);
611
+ }
612
+ if (prompt?.prepend) {
613
+ sections.push(`
614
+ ${prompt.prepend.trim()}`);
615
+ }
616
+ if (config.integrations?.linear) {
617
+ const linear = config.integrations.linear;
618
+ sections.push(`
619
+ ## Task Management
620
+
621
+ If the user doesn't have a task in their project management tool, create one using the Linear MCP.${linear.team ? ` Add it to the **${linear.team}** team.` : ""} Provide the link to the user so they can review and modify as needed.`);
622
+ }
623
+ sections.push(`
624
+ ## Repositories
625
+ `);
626
+ for (const repo of config.repos) {
627
+ const parts = [`- **${repo.path}**`];
628
+ if (repo.description) parts.push(`\u2014 ${repo.description}`);
629
+ else if (repo.tech) parts.push(`\u2014 ${repo.tech}`);
630
+ if (repo.skills?.length) parts.push(`(skills: ${repo.skills.join(", ")})`);
631
+ sections.push(parts.join(" "));
632
+ if (repo.commands) {
633
+ const cmds = Object.entries(repo.commands).filter(([, v]) => v).map(([k, v]) => `\`${k}\`: \`${v}\``).join(", ");
634
+ if (cmds) sections.push(` Commands: ${cmds}`);
635
+ }
636
+ }
637
+ if (prompt?.sections?.after_repositories) {
638
+ sections.push(`
639
+ ${prompt.sections.after_repositories.trim()}`);
640
+ }
641
+ const docStructure = buildDocumentStructure(steps, taskFolder);
642
+ sections.push(docStructure);
643
+ const pipelineSection = buildPipelineSection(steps);
644
+ sections.push(pipelineSection);
645
+ if (prompt?.sections?.after_pipeline) {
646
+ sections.push(`
647
+ ${prompt.sections.after_pipeline.trim()}`);
648
+ }
649
+ if (config.integrations?.slack || config.integrations?.github) {
650
+ sections.push(buildDeliverySection(config));
651
+ }
652
+ if (prompt?.sections?.after_delivery) {
653
+ sections.push(`
654
+ ${prompt.sections.after_delivery.trim()}`);
655
+ }
656
+ if (config.memory) {
657
+ sections.push(`
658
+ ## Team Memory
659
+
660
+ This workspace has a team memory knowledge base available via the \`team-memory\` MCP.
661
+
662
+ **Before starting any task**, use \`search_memories\` to find relevant context \u2014 past decisions, conventions, known issues, and domain knowledge. This avoids repeating mistakes and ensures consistency with previous choices.
663
+
664
+ **After completing a task**, if you discovered something valuable (a decision, a gotcha, a convention, domain insight), use \`add_memory\` to capture it for the team.
665
+
666
+ Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_memories\`, \`archive_memory\`, \`remove_memory\`.`);
667
+ }
668
+ sections.push(`
669
+ ## Troubleshooting and Debugging
670
+
671
+ For bug reports or unexpected behavior, use the \`debugger\` agent directly.
672
+ It will:
673
+ 1. Collect context (symptoms, environment, timeline)
674
+ 2. Analyze logs and stack traces
675
+ 3. Form and test hypotheses systematically
676
+ 4. Identify the root cause
677
+ 5. Propose a solution or call coding agents to implement the fix`);
678
+ if (prompt?.sections) {
679
+ const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
680
+ for (const [name, content] of Object.entries(prompt.sections)) {
681
+ if (reservedKeys.has(name)) continue;
682
+ const title = name.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
683
+ sections.push(`
684
+ ## ${title}
685
+
686
+ ${content.trim()}`);
687
+ }
688
+ }
689
+ if (prompt?.append) {
690
+ sections.push(`
691
+ ${prompt.append.trim()}`);
692
+ }
693
+ return sections.join("\n");
694
+ }
695
+ function buildDocumentStructure(steps, taskFolder) {
696
+ const outputs = [];
697
+ for (const step of steps) {
698
+ if (step.output) {
699
+ outputs.push(step.output);
700
+ }
701
+ if (Array.isArray(step.agents)) {
702
+ for (const a of step.agents) {
703
+ if (typeof a === "object" && a.output) {
704
+ outputs.push(a.output);
705
+ }
706
+ }
707
+ }
708
+ }
709
+ if (outputs.length === 0) {
710
+ outputs.push("refinement.md", "code-backend.md", "code-frontend.md", "code-review.md", "qa-backend.md", "qa-frontend.md");
711
+ }
712
+ const tree = outputs.map((o) => `\u251C\u2500\u2500 ${o}`);
713
+ if (tree.length > 0) {
714
+ tree[tree.length - 1] = tree[tree.length - 1].replace("\u251C\u2500\u2500", "\u2514\u2500\u2500");
715
+ }
716
+ return `
717
+ ## Document Structure
718
+
719
+ All task documents are stored in \`${taskFolder}\`:
720
+
721
+ \`\`\`
722
+ ${taskFolder}
723
+ ${tree.join("\n")}
724
+ \`\`\``;
725
+ }
726
+ function buildPipelineSection(steps) {
727
+ if (steps.length === 0) {
728
+ return `
729
+ ## Development Pipeline
730
+
731
+ 1. Use the \`refinement\` agent to collect requirements
732
+ 2. Use \`coding-backend\` and/or \`coding-frontend\` agents to implement
733
+ 3. Use \`code-reviewer\` to review the implementation
734
+ 4. Use \`qa-backend\` and/or \`qa-frontend\` to test
735
+ 5. Create PRs and notify the team`;
736
+ }
737
+ const parts = [`
738
+ ## Development Pipeline
739
+ `];
740
+ for (const step of steps) {
741
+ if (step.actions) {
742
+ parts.push(`### Delivery`);
743
+ parts.push(`After all validations pass, execute these actions:`);
744
+ for (const action of step.actions) {
745
+ parts.push(`- ${formatAction(action)}`);
746
+ }
747
+ continue;
748
+ }
749
+ const stepTitle = step.step.charAt(0).toUpperCase() + step.step.slice(1);
750
+ parts.push(`### ${stepTitle}`);
751
+ if (step.mode === "plan") {
752
+ parts.push(`**Before starting this step, switch to Plan Mode** by calling \`SwitchMode\` with \`target_mode_id: "plan"\`. This ensures collaborative planning with the user in a read-only context before any implementation begins.`);
753
+ parts.push(``);
754
+ }
755
+ if (step.agent) {
756
+ parts.push(`Call the \`${step.agent}\` agent.${step.output ? ` It writes to \`${step.output}\`.` : ""}`);
757
+ if (step.step === "refinement") {
758
+ parts.push(`
759
+ After it runs, read the document and validate with the user:
760
+ - If there are unanswered questions, ask the user one at a time
761
+ - If the user requests adjustments, send back to the refinement agent
762
+ - Do not proceed until the document is complete and approved by the user`);
763
+ }
764
+ }
765
+ if (Array.isArray(step.agents)) {
766
+ const agentList = step.agents.map((a) => {
767
+ if (typeof a === "string") return { agent: a };
768
+ return a;
769
+ });
770
+ if (step.parallel) {
771
+ parts.push(`Call these agents${step.parallel ? " in parallel" : ""}:`);
772
+ } else {
773
+ parts.push(`Call these agents in sequence:`);
774
+ }
775
+ for (const a of agentList) {
776
+ let line = `- \`${a.agent}\``;
777
+ if (a.output) line += ` \u2192 writes to \`${a.output}\``;
778
+ if (a.when) line += ` (when: ${a.when})`;
779
+ parts.push(line);
780
+ }
781
+ if (step.step === "coding" || step.step === "code" || step.step === "implementation") {
782
+ parts.push(`
783
+ If any coding agent has doubts, they will write questions in their document. Apply the same Q&A logic as refinement \u2014 validate with the user before proceeding.`);
784
+ }
785
+ if (step.step === "validation" || step.step === "review" || step.step === "qa") {
786
+ parts.push(`
787
+ If any validation agent leaves comments requiring fixes, call the relevant coding agents again to address them.`);
788
+ }
789
+ }
790
+ if (step.mode === "plan") {
791
+ parts.push(`
792
+ **After this step is complete and approved**, switch back to Agent Mode to proceed with the next step.`);
793
+ }
794
+ parts.push("");
795
+ }
796
+ return parts.join("\n");
797
+ }
798
+ function buildDeliverySection(config) {
799
+ const parts = [`
800
+ ## Delivery Details
801
+ `];
802
+ if (config.integrations?.github) {
803
+ const gh = config.integrations.github;
804
+ parts.push(`### Pull Requests`);
805
+ parts.push(`For each repository with changes, push the branch and create a PR using the GitHub MCP.`);
806
+ if (gh.pr_branch_pattern) {
807
+ parts.push(`Branch naming pattern: \`${gh.pr_branch_pattern}\``);
808
+ }
809
+ }
810
+ if (config.integrations?.slack) {
811
+ const slack = config.integrations.slack;
812
+ if (slack.channels) {
813
+ parts.push(`
814
+ ### Slack Notifications`);
815
+ for (const [purpose, channel] of Object.entries(slack.channels)) {
816
+ parts.push(`- **${purpose}**: Post to \`${channel}\``);
817
+ }
818
+ }
819
+ if (slack.templates) {
820
+ parts.push(`
821
+ Message templates:`);
822
+ for (const [name, template] of Object.entries(slack.templates)) {
823
+ parts.push(`- **${name}**: \`${template}\``);
824
+ }
825
+ }
826
+ }
827
+ if (config.integrations?.linear) {
828
+ parts.push(`
829
+ ### Task Management`);
830
+ parts.push(`Update the Linear task status after PR creation.`);
831
+ }
832
+ return parts.join("\n");
833
+ }
834
+ function formatAction(action) {
835
+ const map = {
836
+ "create-pr": "Create pull requests for each repository with changes",
837
+ "notify-slack": "Send notification to the configured Slack channel",
838
+ "notify-slack-prs": "Send PR notification to the Slack PRs channel",
839
+ "update-linear": "Update the Linear task status",
840
+ "update-linear-status": "Update the Linear task status to Review",
841
+ "update-jira": "Update the Jira task status"
842
+ };
843
+ return map[action] || action;
844
+ }
845
+ async function generateClaudeCode(config, hubDir) {
846
+ const claudeDir = join3(hubDir, ".claude");
847
+ await mkdir2(join3(claudeDir, "agents"), { recursive: true });
848
+ const orchestratorRule = buildOrchestratorRule(config);
849
+ const cleanedOrchestrator = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
850
+ const claudeMdSections = [];
851
+ claudeMdSections.push(cleanedOrchestrator);
852
+ const agentsDir = resolve(hubDir, "agents");
853
+ try {
854
+ const agentFiles = await readdir2(agentsDir);
855
+ const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
856
+ for (const file of mdFiles) {
857
+ await copyFile(join3(agentsDir, file), join3(claudeDir, "agents", file));
858
+ }
859
+ console.log(chalk2.green(` Copied ${mdFiles.length} agents to .claude/agents/`));
860
+ } catch {
861
+ console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
862
+ }
863
+ const skillsDir = resolve(hubDir, "skills");
864
+ try {
865
+ const skillFolders = await readdir2(skillsDir);
866
+ const claudeSkillsDir = join3(claudeDir, "skills");
867
+ await mkdir2(claudeSkillsDir, { recursive: true });
868
+ let count = 0;
869
+ for (const folder of skillFolders) {
870
+ const skillFile = join3(skillsDir, folder, "SKILL.md");
871
+ try {
872
+ await readFile3(skillFile);
873
+ const srcDir = join3(skillsDir, folder);
874
+ const targetDir = join3(claudeSkillsDir, folder);
875
+ await cp(srcDir, targetDir, { recursive: true });
876
+ count++;
877
+ } catch {
878
+ }
879
+ }
880
+ if (count > 0) {
881
+ console.log(chalk2.green(` Copied ${count} skills to .claude/skills/`));
882
+ }
883
+ } catch {
884
+ }
885
+ await writeFile2(join3(hubDir, "CLAUDE.md"), claudeMdSections.join("\n"), "utf-8");
886
+ console.log(chalk2.green(" Generated CLAUDE.md"));
887
+ if (config.mcps?.length) {
888
+ const mcpJson = {};
889
+ for (const mcp of config.mcps) {
890
+ mcpJson[mcp.name] = buildClaudeCodeMcpEntry(mcp);
891
+ }
892
+ await writeFile2(
893
+ join3(hubDir, ".mcp.json"),
894
+ JSON.stringify({ mcpServers: mcpJson }, null, 2) + "\n",
895
+ "utf-8"
896
+ );
897
+ console.log(chalk2.green(" Generated .mcp.json"));
898
+ }
899
+ const mcpServerNames = config.mcps?.map((m) => m.name) || [];
900
+ const claudeSettings = {
901
+ $schema: "https://json.schemastore.org/claude-code-settings.json",
902
+ permissions: {
903
+ allow: [
904
+ "Read(*)",
905
+ "Edit(*)",
906
+ "Write(*)",
907
+ "Bash(git *)",
908
+ "Bash(npm *)",
909
+ "Bash(pnpm *)",
910
+ "Bash(npx *)",
911
+ "Bash(ls *)",
912
+ "Bash(echo *)",
913
+ "Bash(grep *)",
914
+ ...mcpServerNames.map((name) => `mcp__${name}__*`)
915
+ ],
916
+ deny: [
917
+ "Read(.env)",
918
+ "Read(.env.*)",
919
+ "Read(**/.env)",
920
+ "Read(**/.env.*)",
921
+ "Read(**/credentials*)",
922
+ "Read(**/secrets*)",
923
+ "Read(**/*.pem)",
924
+ "Read(**/*.key)"
925
+ ]
926
+ },
927
+ enableAllProjectMcpServers: true
928
+ };
929
+ if (config.hooks) {
930
+ const claudeHooks = buildClaudeHooks(config.hooks);
931
+ if (claudeHooks) {
932
+ claudeSettings.hooks = claudeHooks;
933
+ }
934
+ }
935
+ await writeFile2(
936
+ join3(claudeDir, "settings.json"),
937
+ JSON.stringify(claudeSettings, null, 2) + "\n",
938
+ "utf-8"
939
+ );
940
+ console.log(chalk2.green(" Generated .claude/settings.json"));
941
+ const gitignoreLines = buildGitignoreLines(config);
942
+ await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
943
+ console.log(chalk2.green(" Generated .gitignore"));
944
+ }
945
+ async function generateKiro(config, hubDir) {
946
+ const kiroDir = join3(hubDir, ".kiro");
947
+ const steeringDir = join3(kiroDir, "steering");
948
+ const settingsDir = join3(kiroDir, "settings");
949
+ await mkdir2(steeringDir, { recursive: true });
950
+ await mkdir2(settingsDir, { recursive: true });
951
+ const gitignoreLines = buildGitignoreLines(config);
952
+ await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
953
+ console.log(chalk2.green(" Generated .gitignore"));
954
+ const kiroRule = buildKiroOrchestratorRule(config);
955
+ const kiroOrchestrator = buildKiroSteeringContent(kiroRule);
956
+ await writeFile2(join3(steeringDir, "orchestrator.md"), kiroOrchestrator, "utf-8");
957
+ console.log(chalk2.green(" Generated .kiro/steering/orchestrator.md"));
958
+ await writeFile2(join3(hubDir, "AGENTS.md"), kiroRule + "\n", "utf-8");
959
+ console.log(chalk2.green(" Generated AGENTS.md"));
960
+ const agentsDir = resolve(hubDir, "agents");
961
+ try {
962
+ const agentFiles = await readdir2(agentsDir);
963
+ const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
964
+ for (const file of mdFiles) {
965
+ const agentContent = await readFile3(join3(agentsDir, file), "utf-8");
966
+ const agentName = file.replace(/\.md$/, "");
967
+ const steeringContent = buildKiroSteeringContent(agentContent, "auto", {
968
+ name: agentName,
969
+ description: `Role-specific instructions for the ${agentName} phase. Include when working on ${agentName}-related tasks.`
970
+ });
971
+ const steeringName = `agent-${file}`;
972
+ await writeFile2(join3(steeringDir, steeringName), steeringContent, "utf-8");
973
+ }
974
+ console.log(chalk2.green(` Copied ${mdFiles.length} agents as steering files`));
975
+ } catch {
976
+ console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
977
+ }
978
+ const skillsDir = resolve(hubDir, "skills");
979
+ try {
980
+ const skillFolders = await readdir2(skillsDir);
981
+ const kiroSkillsDir = join3(kiroDir, "skills");
982
+ await mkdir2(kiroSkillsDir, { recursive: true });
983
+ let count = 0;
984
+ for (const folder of skillFolders) {
985
+ const skillFile = join3(skillsDir, folder, "SKILL.md");
986
+ try {
987
+ await readFile3(skillFile);
988
+ const srcDir = join3(skillsDir, folder);
989
+ const targetDir = join3(kiroSkillsDir, folder);
990
+ await cp(srcDir, targetDir, { recursive: true });
991
+ count++;
992
+ } catch {
993
+ }
994
+ }
995
+ if (count > 0) {
996
+ console.log(chalk2.green(` Copied ${count} skills to .kiro/skills/`));
997
+ }
998
+ } catch {
999
+ }
1000
+ if (config.mcps?.length) {
1001
+ const mcpConfig = {};
1002
+ for (const mcp of config.mcps) {
1003
+ mcpConfig[mcp.name] = buildKiroMcpEntry(mcp);
1004
+ }
1005
+ await writeFile2(
1006
+ join3(settingsDir, "mcp.json"),
1007
+ JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
1008
+ "utf-8"
1009
+ );
1010
+ console.log(chalk2.green(" Generated .kiro/settings/mcp.json"));
1011
+ }
1012
+ if (config.hooks) {
1013
+ const hookNotes = [];
1014
+ for (const [event, entries] of Object.entries(config.hooks)) {
1015
+ const mapped = HOOK_EVENT_MAP[event]?.kiro;
1016
+ if (!mapped) continue;
1017
+ for (const entry of entries) {
1018
+ hookNotes.push(`- **${mapped}**: ${entry.type === "command" ? entry.command : entry.prompt}`);
1019
+ }
1020
+ }
1021
+ if (hookNotes.length > 0) {
1022
+ console.log(chalk2.yellow(` Note: Kiro hooks are managed via the Kiro panel UI.`));
1023
+ console.log(chalk2.yellow(` The following hooks should be configured manually:`));
1024
+ for (const note of hookNotes) {
1025
+ console.log(chalk2.yellow(` ${note}`));
1026
+ }
1027
+ }
1028
+ }
1029
+ await generateVSCodeSettings(config, hubDir);
1030
+ }
1031
+ async function generateVSCodeSettings(config, hubDir) {
1032
+ const vscodeDir = join3(hubDir, ".vscode");
1033
+ await mkdir2(vscodeDir, { recursive: true });
1034
+ const settingsPath = join3(vscodeDir, "settings.json");
1035
+ let existing = {};
1036
+ if (existsSync2(settingsPath)) {
1037
+ try {
1038
+ const raw = await readFile3(settingsPath, "utf-8");
1039
+ existing = JSON.parse(raw);
1040
+ } catch {
1041
+ existing = {};
1042
+ }
1043
+ }
1044
+ const managed = {
1045
+ "git.autoRepositoryDetection": true,
1046
+ "git.detectSubmodules": true,
1047
+ "git.detectSubmodulesLimit": Math.max(config.repos.length * 2, 20)
1048
+ };
1049
+ const merged = { ...existing, ...managed };
1050
+ await writeFile2(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1051
+ console.log(chalk2.green(" Generated .vscode/settings.json (git multi-repo detection)"));
1052
+ }
1053
+ function buildGitignoreLines(config) {
1054
+ const lines = [
1055
+ "node_modules/",
1056
+ ".DS_Store",
1057
+ "",
1058
+ "# Repositories (managed by hub)"
1059
+ ];
1060
+ for (const repo of config.repos) {
1061
+ lines.push(repo.path.replace("./", ""));
1062
+ }
1063
+ lines.push(
1064
+ "",
1065
+ "# Hub local cache",
1066
+ ".hub/",
1067
+ "",
1068
+ "# Docker volumes",
1069
+ "*_data/",
1070
+ "",
1071
+ "# Environment files",
1072
+ "*.env",
1073
+ "*.env.local",
1074
+ "!.env.example",
1075
+ "",
1076
+ "# Generated files",
1077
+ "docker-compose.yml",
1078
+ "",
1079
+ "# Task documents",
1080
+ "tasks/"
1081
+ );
1082
+ if (config.memory) {
1083
+ const memPath = config.memory.path || "memories";
1084
+ lines.push(
1085
+ "",
1086
+ "# Memory vector store (generated from markdown files)",
1087
+ `${memPath}/.lancedb/`
1088
+ );
1089
+ }
1090
+ return lines;
1091
+ }
1092
+ var generators = {
1093
+ cursor: { name: "Cursor", generate: generateCursor },
1094
+ "claude-code": { name: "Claude Code", generate: generateClaudeCode },
1095
+ kiro: { name: "Kiro", generate: generateKiro }
1096
+ };
1097
+ async function resolveEditor(opts) {
1098
+ const hubDir = process.cwd();
1099
+ if (opts.resetEditor) {
1100
+ const { editor: editor2 } = await inquirer.prompt([
1101
+ {
1102
+ type: "list",
1103
+ name: "editor",
1104
+ message: "Which editor do you use?",
1105
+ choices: Object.entries(generators).map(([key, gen]) => ({
1106
+ name: gen.name,
1107
+ value: key
1108
+ }))
1109
+ }
1110
+ ]);
1111
+ return editor2;
1112
+ }
1113
+ if (opts.editor) return opts.editor;
1114
+ const saved = await getSavedEditor(hubDir);
1115
+ if (saved) return saved;
1116
+ const { editor } = await inquirer.prompt([
1117
+ {
1118
+ type: "list",
1119
+ name: "editor",
1120
+ message: "No editor preference saved. Which editor do you use?",
1121
+ choices: Object.entries(generators).map(([key, gen]) => ({
1122
+ name: gen.name,
1123
+ value: key
1124
+ }))
1125
+ }
1126
+ ]);
1127
+ return editor;
1128
+ }
1129
+ var generateCommand = new Command("generate").description("Generate editor-specific configuration files from hub.yaml").option("-e, --editor <editor>", "Target editor (cursor, claude-code, kiro)").option("--reset-editor", "Reset saved editor preference and choose again").action(async (opts) => {
1130
+ const hubDir = process.cwd();
1131
+ const config = await loadHubConfig(hubDir);
1132
+ if (config.memory) {
1133
+ const hasMemoryMcp = config.mcps?.some(
1134
+ (m) => m.name === "team-memory" || m.package === "@arvoretech/memory-mcp"
1135
+ );
1136
+ if (!hasMemoryMcp) {
1137
+ console.log(chalk2.red(`
1138
+ Error: 'memory' is configured but no memory MCP is declared in 'mcps'.
1139
+ `));
1140
+ console.log(chalk2.yellow(` Add this to your hub.yaml:
1141
+ `));
1142
+ console.log(chalk2.dim(` mcps:`));
1143
+ console.log(chalk2.dim(` - name: team-memory`));
1144
+ console.log(chalk2.dim(` package: "@arvoretech/memory-mcp"`));
1145
+ console.log(chalk2.dim(` env:`));
1146
+ console.log(chalk2.dim(` MEMORY_PATH: ${config.memory.path || "./memories"}
1147
+ `));
1148
+ process.exit(1);
1149
+ }
1150
+ }
1151
+ const editorKey = await resolveEditor(opts);
1152
+ const generator = generators[editorKey];
1153
+ if (!generator) {
1154
+ console.log(
1155
+ chalk2.red(`Unknown editor: ${editorKey}. Available: ${Object.keys(generators).join(", ")}`)
1156
+ );
1157
+ return;
1158
+ }
1159
+ if (opts.editor || opts.resetEditor) {
1160
+ console.log(chalk2.dim(` Saving editor preference: ${generator.name}`));
1161
+ }
1162
+ console.log(chalk2.blue(`
1163
+ Generating ${generator.name} configuration
1164
+ `));
1165
+ await generator.generate(config, hubDir);
1166
+ await saveGenerateState(hubDir, editorKey);
1167
+ console.log(chalk2.green("\nDone!\n"));
1168
+ });
1169
+
1170
+ export {
1171
+ loadHubConfig,
1172
+ generators,
1173
+ generateCommand,
1174
+ checkAndAutoRegenerate
1175
+ };