@arvoretech/hub 0.13.4 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,10 +3,10 @@ import {
3
3
  } from "./chunk-VMN4KGAK.js";
4
4
 
5
5
  // src/commands/generate.ts
6
- import { Command } from "commander";
7
- import { existsSync as existsSync2 } from "fs";
8
- import { mkdir as mkdir3, writeFile as writeFile3, readdir as readdir2, copyFile, readFile as readFile3, cp, rm } from "fs/promises";
9
- import { join as join3, resolve as resolve2 } from "path";
6
+ import { Command as Command2 } from "commander";
7
+ import { existsSync as existsSync3 } from "fs";
8
+ import { mkdir as mkdir4, writeFile as writeFile4, readdir as readdir2, copyFile, readFile as readFile4, cp, rm } from "fs/promises";
9
+ import { join as join4, resolve as resolve2 } from "path";
10
10
  import chalk3 from "chalk";
11
11
  import inquirer from "inquirer";
12
12
 
@@ -117,7 +117,7 @@ async function checkAndAutoRegenerate(hubDir) {
117
117
  return;
118
118
  }
119
119
  console.log(chalk.yellow("\n Detected outdated configs, auto-regenerating..."));
120
- const { generators: generators2 } = await import("./generate-OIIS6DBO.js");
120
+ const { generators: generators2 } = await import("./generate-TBAEF7R5.js");
121
121
  const generator = generators2[result.editor];
122
122
  if (!generator) {
123
123
  console.log(chalk.red(` Unknown editor '${result.editor}' in cache. Run 'hub generate' manually.`));
@@ -390,6 +390,337 @@ async function fetchRemoteSources(sources, hubDir, skillsDir, steeringDir) {
390
390
  return { skills: skillCount, steering: steeringCount, errors };
391
391
  }
392
392
 
393
+ // src/commands/persona.ts
394
+ import { Command } from "commander";
395
+ import { join as join3 } from "path";
396
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
397
+ import { existsSync as existsSync2 } from "fs";
398
+ import React from "react";
399
+ import { render } from "ink";
400
+
401
+ // src/tui/PersonaApp.tsx
402
+ import { useState, useCallback } from "react";
403
+ import { Box, Text, useInput, useStdout, useApp } from "ink";
404
+ import TextInput from "ink-text-input";
405
+
406
+ // src/tui/theme.ts
407
+ var colors = {
408
+ brand: "#22c55e",
409
+ brandDim: "#16a34a",
410
+ blue: "#3b82f6",
411
+ purple: "#a78bfa",
412
+ muted: "#6b7280",
413
+ dim: "#4b5563",
414
+ warning: "#eab308",
415
+ error: "#ef4444",
416
+ white: "#ffffff"
417
+ };
418
+ var symbols = {
419
+ check: "\u2713",
420
+ cross: "\u2717",
421
+ arrow: "\u276F",
422
+ dot: "\u25CF",
423
+ circle: "\u25CB",
424
+ tree: "\u{1F333}",
425
+ line: "\u2500",
426
+ corner: "\u256D",
427
+ cornerEnd: "\u2570",
428
+ vertical: "\u2502",
429
+ cornerRight: "\u256E",
430
+ cornerEndRight: "\u256F"
431
+ };
432
+ function horizontalLine(width) {
433
+ return symbols.line.repeat(width);
434
+ }
435
+
436
+ // src/tui/PersonaApp.tsx
437
+ import { jsx, jsxs } from "react/jsx-runtime";
438
+ var TECHNICAL_LEVELS = [
439
+ { value: "non-technical", label: "Non-technical", description: "No coding experience \u2014 CEO, PM, designer, etc." },
440
+ { value: "beginner", label: "Beginner", description: "Learning to code or just started working with devs" },
441
+ { value: "intermediate", label: "Intermediate", description: "Comfortable with code, still learning some tools" },
442
+ { value: "advanced", label: "Advanced", description: "Experienced developer, knows the stack well" }
443
+ ];
444
+ var STEP_INFO = {
445
+ name: { title: "What's your name?", subtitle: "So the AI knows who it's talking to." },
446
+ role: { title: "What's your role?", subtitle: "e.g. CEO, Product Manager, Designer, Backend Dev, QA Engineer..." },
447
+ technical_level: { title: "How technical are you?", subtitle: "This changes how the AI explains things to you." },
448
+ context: { title: "Anything else the AI should know about you?", subtitle: `e.g. "I focus on business metrics", "I only work on the mobile app", "I review PRs but don't code"` },
449
+ language: { title: "What language should the AI use?", subtitle: "e.g. English, Portugu\xEAs, Espa\xF1ol..." }
450
+ };
451
+ function PersonaApp({ existing, onComplete }) {
452
+ const { stdout } = useStdout();
453
+ const { exit } = useApp();
454
+ const width = Math.min(stdout?.columns || 80, 80);
455
+ const CLEAR2 = "\x1B[2J\x1B[H";
456
+ const [step, setStep] = useState("name");
457
+ const [data, setData] = useState({
458
+ name: existing?.name || "",
459
+ role: existing?.role || "",
460
+ context: existing?.context || "",
461
+ technical_level: existing?.technical_level || "intermediate",
462
+ language: existing?.language || "English"
463
+ });
464
+ const [inputValue, setInputValue] = useState(existing?.name || "");
465
+ const [levelCursor, setLevelCursor] = useState(
466
+ TECHNICAL_LEVELS.findIndex((l) => l.value === (existing?.technical_level || "intermediate"))
467
+ );
468
+ const goTo = useCallback((next) => {
469
+ stdout?.write(CLEAR2);
470
+ setStep(next);
471
+ }, [stdout]);
472
+ const handleTextSubmit = useCallback((value, field, next) => {
473
+ if (!value.trim()) return;
474
+ setData((prev) => ({ ...prev, [field]: value.trim() }));
475
+ setInputValue("");
476
+ goTo(next);
477
+ }, [goTo]);
478
+ useInput((input, key) => {
479
+ if (step === "technical_level") {
480
+ if (key.upArrow) setLevelCursor((p) => p > 0 ? p - 1 : TECHNICAL_LEVELS.length - 1);
481
+ if (key.downArrow) setLevelCursor((p) => p < TECHNICAL_LEVELS.length - 1 ? p + 1 : 0);
482
+ if (key.return) {
483
+ setData((prev) => ({ ...prev, technical_level: TECHNICAL_LEVELS[levelCursor].value }));
484
+ setInputValue(data.context || "");
485
+ goTo("context");
486
+ }
487
+ }
488
+ if (step === "review") {
489
+ if (key.return || input === "y") {
490
+ onComplete(data);
491
+ goTo("done");
492
+ }
493
+ if (input === "b" || key.leftArrow) {
494
+ setInputValue(data.name);
495
+ goTo("name");
496
+ }
497
+ }
498
+ if (step === "done" && key.return) {
499
+ exit();
500
+ }
501
+ });
502
+ const info = STEP_INFO[step];
503
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
504
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
505
+ /* @__PURE__ */ jsxs(Text, { color: colors.brand, bold: true, children: [
506
+ symbols.tree,
507
+ " Repo Hub"
508
+ ] }),
509
+ /* @__PURE__ */ jsxs(Text, { color: colors.dim, children: [
510
+ " ",
511
+ symbols.line,
512
+ " Persona Setup"
513
+ ] })
514
+ ] }),
515
+ info && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 1, children: [
516
+ /* @__PURE__ */ jsx(Text, { color: colors.white, bold: true, children: info.title }),
517
+ /* @__PURE__ */ jsx(Text, { color: colors.dim, children: info.subtitle })
518
+ ] }),
519
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [
520
+ step === "name" && /* @__PURE__ */ jsxs(Box, { children: [
521
+ /* @__PURE__ */ jsx(Text, { color: colors.brand, bold: true, children: "\u276F " }),
522
+ /* @__PURE__ */ jsx(
523
+ TextInput,
524
+ {
525
+ value: inputValue,
526
+ onChange: setInputValue,
527
+ onSubmit: (v) => {
528
+ handleTextSubmit(v, "name", "role");
529
+ setInputValue(data.role || "");
530
+ },
531
+ placeholder: "Jo\xE3o"
532
+ }
533
+ )
534
+ ] }),
535
+ step === "role" && /* @__PURE__ */ jsxs(Box, { children: [
536
+ /* @__PURE__ */ jsx(Text, { color: colors.brand, bold: true, children: "\u276F " }),
537
+ /* @__PURE__ */ jsx(
538
+ TextInput,
539
+ {
540
+ value: inputValue,
541
+ onChange: setInputValue,
542
+ onSubmit: (v) => {
543
+ handleTextSubmit(v, "role", "technical_level");
544
+ },
545
+ placeholder: "CEO"
546
+ }
547
+ )
548
+ ] }),
549
+ step === "technical_level" && /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: TECHNICAL_LEVELS.map((level, i) => {
550
+ const active = i === levelCursor;
551
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
552
+ /* @__PURE__ */ jsxs(Box, { children: [
553
+ /* @__PURE__ */ jsx(Text, { color: active ? colors.brand : colors.dim, children: active ? `${symbols.arrow} ` : " " }),
554
+ /* @__PURE__ */ jsx(Text, { color: active ? colors.white : colors.muted, bold: active, children: level.label })
555
+ ] }),
556
+ /* @__PURE__ */ jsx(Box, { paddingLeft: 4, children: /* @__PURE__ */ jsx(Text, { color: colors.dim, children: level.description }) })
557
+ ] }, level.value);
558
+ }) }),
559
+ step === "context" && /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { children: [
560
+ /* @__PURE__ */ jsx(Text, { color: colors.brand, bold: true, children: "\u276F " }),
561
+ /* @__PURE__ */ jsx(
562
+ TextInput,
563
+ {
564
+ value: inputValue,
565
+ onChange: setInputValue,
566
+ onSubmit: (v) => {
567
+ setData((prev) => ({ ...prev, context: v.trim() }));
568
+ setInputValue(data.language || "English");
569
+ goTo("language");
570
+ },
571
+ placeholder: "(optional \u2014 press Enter to skip)"
572
+ }
573
+ )
574
+ ] }) }),
575
+ step === "language" && /* @__PURE__ */ jsxs(Box, { children: [
576
+ /* @__PURE__ */ jsx(Text, { color: colors.brand, bold: true, children: "\u276F " }),
577
+ /* @__PURE__ */ jsx(
578
+ TextInput,
579
+ {
580
+ value: inputValue,
581
+ onChange: setInputValue,
582
+ onSubmit: (v) => {
583
+ handleTextSubmit(v || "English", "language", "review");
584
+ },
585
+ placeholder: "English"
586
+ }
587
+ )
588
+ ] }),
589
+ step === "review" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
590
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 1, borderStyle: "round", borderColor: colors.dim, paddingRight: 1, children: [
591
+ /* @__PURE__ */ jsx(ReviewRow, { label: "Name", value: data.name }),
592
+ /* @__PURE__ */ jsx(ReviewRow, { label: "Role", value: data.role }),
593
+ /* @__PURE__ */ jsx(ReviewRow, { label: "Level", value: TECHNICAL_LEVELS.find((l) => l.value === data.technical_level)?.label || data.technical_level }),
594
+ data.context && /* @__PURE__ */ jsx(ReviewRow, { label: "Context", value: data.context }),
595
+ /* @__PURE__ */ jsx(ReviewRow, { label: "Language", value: data.language })
596
+ ] }),
597
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: colors.dim, children: [
598
+ /* @__PURE__ */ jsx(Text, { color: colors.brand, children: "enter" }),
599
+ " save ",
600
+ /* @__PURE__ */ jsx(Text, { color: colors.muted, children: "b" }),
601
+ " start over"
602
+ ] }) })
603
+ ] }),
604
+ step === "done" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "center", children: [
605
+ /* @__PURE__ */ jsxs(Text, { color: colors.brand, bold: true, children: [
606
+ symbols.check,
607
+ " Persona saved"
608
+ ] }),
609
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: colors.dim, children: [
610
+ "Run ",
611
+ /* @__PURE__ */ jsx(Text, { color: colors.white, bold: true, children: "hub generate" }),
612
+ " to apply it to your AI agent."
613
+ ] }) }),
614
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: colors.dim, children: [
615
+ "Press ",
616
+ /* @__PURE__ */ jsx(Text, { color: colors.brand, bold: true, children: "Enter" }),
617
+ " to exit"
618
+ ] }) })
619
+ ] })
620
+ ] })
621
+ ] });
622
+ }
623
+ function ReviewRow({ label, value }) {
624
+ return /* @__PURE__ */ jsxs(Box, { children: [
625
+ /* @__PURE__ */ jsx(Box, { width: 10, children: /* @__PURE__ */ jsx(Text, { color: colors.muted, children: label }) }),
626
+ /* @__PURE__ */ jsx(Text, { color: colors.white, bold: true, children: value })
627
+ ] });
628
+ }
629
+
630
+ // src/commands/persona.ts
631
+ import { stringify } from "yaml";
632
+ var ENTER_ALT_SCREEN = "\x1B[?1049h";
633
+ var EXIT_ALT_SCREEN = "\x1B[?1049l";
634
+ var CLEAR = "\x1B[2J\x1B[H";
635
+ var HIDE_CURSOR = "\x1B[?25l";
636
+ var SHOW_CURSOR = "\x1B[?25h";
637
+ function getPersonaPath(hubDir) {
638
+ return join3(hubDir, ".hub", "persona.yaml");
639
+ }
640
+ async function loadPersona(hubDir) {
641
+ const personaPath = getPersonaPath(hubDir);
642
+ if (!existsSync2(personaPath)) return null;
643
+ try {
644
+ const { parse } = await import("yaml");
645
+ const content = await readFile3(personaPath, "utf-8");
646
+ return parse(content);
647
+ } catch {
648
+ return null;
649
+ }
650
+ }
651
+ function buildPersonaSection(persona) {
652
+ const lines = [];
653
+ lines.push(`
654
+ ## User Persona
655
+ `);
656
+ lines.push(`You are talking to **${persona.name}**, who is a **${persona.role}**.`);
657
+ if (persona.technical_level === "non-technical") {
658
+ lines.push(`
659
+ ${persona.name} is not technical. Adapt your communication:
660
+ - Never use jargon, acronyms, or technical terms without explaining them in plain language first.
661
+ - Explain decisions in terms of business impact, user experience, and outcomes \u2014 not implementation details.
662
+ - When showing progress, focus on what changed for the user/product, not what code was modified.
663
+ - If you need to mention something technical, use analogies and simple language.
664
+ - Keep responses short and focused on what matters to them.
665
+ - When asking questions, frame them as business/product decisions, not technical choices.
666
+ - Never show code snippets, terminal output, or file paths unless explicitly asked.`);
667
+ } else if (persona.technical_level === "beginner") {
668
+ lines.push(`
669
+ ${persona.name} is learning and not deeply technical yet. Adapt your communication:
670
+ - Explain technical concepts briefly when you first mention them.
671
+ - Avoid deep implementation details unless asked.
672
+ - Use simple language but don't shy away from introducing technical terms with context.
673
+ - When showing code or commands, briefly explain what they do.
674
+ - Be encouraging and patient \u2014 frame things as learning opportunities.`);
675
+ } else if (persona.technical_level === "intermediate") {
676
+ lines.push(`
677
+ ${persona.name} is comfortable with code but may not know every tool or pattern. Adapt your communication:
678
+ - Use technical language normally but explain niche or advanced concepts when relevant.
679
+ - Show code and commands without excessive explanation, but add context for non-obvious decisions.
680
+ - Focus on the "why" behind architectural choices.`);
681
+ } else {
682
+ lines.push(`
683
+ ${persona.name} is an experienced developer. Communicate directly:
684
+ - Be concise and technical. Skip basic explanations.
685
+ - Focus on trade-offs, edge cases, and non-obvious implications.
686
+ - Show code directly without hand-holding.`);
687
+ }
688
+ if (persona.context) {
689
+ lines.push(`
690
+ Additional context about ${persona.name}: ${persona.context}`);
691
+ }
692
+ if (persona.language && persona.language.toLowerCase() !== "english") {
693
+ lines.push(`
694
+ Always communicate with ${persona.name} in **${persona.language}**.`);
695
+ }
696
+ return lines.join("\n");
697
+ }
698
+ var personaCommand = new Command("persona").description("Set up your personal AI profile \u2014 adapts how the agent communicates with you").action(async () => {
699
+ const hubDir = process.cwd();
700
+ const hubPath = join3(hubDir, ".hub");
701
+ await mkdir3(hubPath, { recursive: true });
702
+ const existing = await loadPersona(hubDir);
703
+ process.stdout.write(ENTER_ALT_SCREEN + CLEAR + HIDE_CURSOR);
704
+ const cleanup = () => {
705
+ process.stdout.write(SHOW_CURSOR + EXIT_ALT_SCREEN);
706
+ };
707
+ process.on("SIGINT", () => {
708
+ cleanup();
709
+ process.exit(0);
710
+ });
711
+ const { waitUntilExit } = render(
712
+ React.createElement(PersonaApp, {
713
+ existing: existing ?? void 0,
714
+ onComplete: async (persona) => {
715
+ const personaPath = getPersonaPath(hubDir);
716
+ await writeFile3(personaPath, stringify(persona), "utf-8");
717
+ }
718
+ })
719
+ );
720
+ await waitUntilExit();
721
+ cleanup();
722
+ });
723
+
393
724
  // src/commands/generate.ts
394
725
  var HUB_DOCS_URL = "https://hub.arvore.com.br/llms-full.txt";
395
726
  async function syncRemoteSources(config, hubDir, skillsDir, steeringDir) {
@@ -464,9 +795,9 @@ function parseFrontMatter(content) {
464
795
  }
465
796
  async function readExistingMcpDisabledState(mcpJsonPath) {
466
797
  const disabledState = {};
467
- if (!existsSync2(mcpJsonPath)) return disabledState;
798
+ if (!existsSync3(mcpJsonPath)) return disabledState;
468
799
  try {
469
- const content = JSON.parse(await readFile3(mcpJsonPath, "utf-8"));
800
+ const content = JSON.parse(await readFile4(mcpJsonPath, "utf-8"));
470
801
  const servers = content.mcpServers || content.mcp || {};
471
802
  for (const [name, config] of Object.entries(servers)) {
472
803
  if (typeof config.disabled === "boolean") {
@@ -492,8 +823,8 @@ async function fetchHubDocsSkill(skillsDir) {
492
823
  return;
493
824
  }
494
825
  const content = await res.text();
495
- const hubSkillDir = join3(skillsDir, "hub-docs");
496
- await mkdir3(hubSkillDir, { recursive: true });
826
+ const hubSkillDir = join4(skillsDir, "hub-docs");
827
+ await mkdir4(hubSkillDir, { recursive: true });
497
828
  const skillContent = `---
498
829
  name: hub-docs
499
830
  description: Repo Hub (rhm) documentation. Use when working with hub.yaml, hub CLI commands, agent orchestration, MCP configuration, skills, workflows, or multi-repo workspace setup.
@@ -501,7 +832,7 @@ triggers: [hub, rhm, hub.yaml, generate, scan, setup, orchestrator, multi-repo,
501
832
  ---
502
833
 
503
834
  ${content}`;
504
- await writeFile3(join3(hubSkillDir, "SKILL.md"), skillContent, "utf-8");
835
+ await writeFile4(join4(hubSkillDir, "SKILL.md"), skillContent, "utf-8");
505
836
  console.log(chalk3.green(" Fetched hub-docs skill from hub.arvore.com.br"));
506
837
  } catch {
507
838
  console.log(chalk3.yellow(` Could not fetch hub docs, skipping hub-docs skill`));
@@ -576,7 +907,7 @@ function buildClaudeHooks(hooks) {
576
907
  return claudeHooks;
577
908
  }
578
909
  async function generateEditorCommands(config, hubDir, targetDir, editorName) {
579
- const commandsDir = join3(targetDir, "commands");
910
+ const commandsDir = join4(targetDir, "commands");
580
911
  let count = 0;
581
912
  if (config.commands_dir) {
582
913
  const srcDir = resolve2(hubDir, config.commands_dir);
@@ -584,9 +915,9 @@ async function generateEditorCommands(config, hubDir, targetDir, editorName) {
584
915
  const files = await readdir2(srcDir);
585
916
  const mdFiles = files.filter((f) => f.endsWith(".md"));
586
917
  if (mdFiles.length > 0) {
587
- await mkdir3(commandsDir, { recursive: true });
918
+ await mkdir4(commandsDir, { recursive: true });
588
919
  for (const file of mdFiles) {
589
- await copyFile(join3(srcDir, file), join3(commandsDir, file));
920
+ await copyFile(join4(srcDir, file), join4(commandsDir, file));
590
921
  count++;
591
922
  }
592
923
  }
@@ -595,10 +926,10 @@ async function generateEditorCommands(config, hubDir, targetDir, editorName) {
595
926
  }
596
927
  }
597
928
  if (config.commands) {
598
- await mkdir3(commandsDir, { recursive: true });
929
+ await mkdir4(commandsDir, { recursive: true });
599
930
  for (const [name, filePath] of Object.entries(config.commands)) {
600
931
  const src = resolve2(hubDir, filePath);
601
- const dest = join3(commandsDir, name.endsWith(".md") ? name : `${name}.md`);
932
+ const dest = join4(commandsDir, name.endsWith(".md") ? name : `${name}.md`);
602
933
  try {
603
934
  await copyFile(src, dest);
604
935
  count++;
@@ -613,27 +944,27 @@ async function generateEditorCommands(config, hubDir, targetDir, editorName) {
613
944
  }
614
945
  async function writeManagedFile(filePath, managedLines) {
615
946
  const managedBlock = [HUB_MARKER_START, ...managedLines, HUB_MARKER_END].join("\n");
616
- if (existsSync2(filePath)) {
617
- const existing = await readFile3(filePath, "utf-8");
947
+ if (existsSync3(filePath)) {
948
+ const existing = await readFile4(filePath, "utf-8");
618
949
  const startIdx = existing.indexOf(HUB_MARKER_START);
619
950
  const endIdx = existing.indexOf(HUB_MARKER_END);
620
951
  if (startIdx !== -1 && endIdx !== -1) {
621
952
  const before = existing.substring(0, startIdx);
622
953
  const after = existing.substring(endIdx + HUB_MARKER_END.length);
623
- await writeFile3(filePath, before + managedBlock + after, "utf-8");
954
+ await writeFile4(filePath, before + managedBlock + after, "utf-8");
624
955
  return;
625
956
  }
626
- await writeFile3(filePath, managedBlock + "\n\n" + existing, "utf-8");
957
+ await writeFile4(filePath, managedBlock + "\n\n" + existing, "utf-8");
627
958
  return;
628
959
  }
629
- await writeFile3(filePath, managedBlock + "\n", "utf-8");
960
+ await writeFile4(filePath, managedBlock + "\n", "utf-8");
630
961
  }
631
962
  async function generateCursor(config, hubDir) {
632
- const cursorDir = join3(hubDir, ".cursor");
633
- await mkdir3(join3(cursorDir, "rules"), { recursive: true });
634
- await mkdir3(join3(cursorDir, "agents"), { recursive: true });
963
+ const cursorDir = join4(hubDir, ".cursor");
964
+ await mkdir4(join4(cursorDir, "rules"), { recursive: true });
965
+ await mkdir4(join4(cursorDir, "agents"), { recursive: true });
635
966
  const gitignoreLines = buildGitignoreLines(config);
636
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
967
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
637
968
  console.log(chalk3.green(" Generated .gitignore"));
638
969
  const cursorignoreLines = [
639
970
  "# Re-include repositories for AI context"
@@ -643,7 +974,7 @@ async function generateCursor(config, hubDir) {
643
974
  cursorignoreLines.push(`!${repoDir}/`);
644
975
  }
645
976
  cursorignoreLines.push("", "# Re-include tasks for agent collaboration", "!tasks/");
646
- await writeManagedFile(join3(hubDir, ".cursorignore"), cursorignoreLines);
977
+ await writeManagedFile(join4(hubDir, ".cursorignore"), cursorignoreLines);
647
978
  console.log(chalk3.green(" Generated .cursorignore"));
648
979
  if (config.mcps?.length) {
649
980
  const mcpConfig = {};
@@ -656,22 +987,36 @@ async function generateCursor(config, hubDir) {
656
987
  mcpConfig[mcp.name] = buildCursorMcpEntry(mcp);
657
988
  }
658
989
  }
659
- await writeFile3(
660
- join3(cursorDir, "mcp.json"),
990
+ const sandbox = getSandboxService(config);
991
+ if (sandbox && !mcpConfig["sandbox"]) {
992
+ mcpConfig["sandbox"] = buildSandboxMcpEntry(sandbox.port);
993
+ }
994
+ await writeFile4(
995
+ join4(cursorDir, "mcp.json"),
661
996
  JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
662
997
  "utf-8"
663
998
  );
664
999
  console.log(chalk3.green(" Generated .cursor/mcp.json"));
665
1000
  }
666
1001
  const orchestratorRule = buildOrchestratorRule(config);
667
- await writeFile3(join3(cursorDir, "rules", "orchestrator.mdc"), orchestratorRule, "utf-8");
1002
+ await writeFile4(join4(cursorDir, "rules", "orchestrator.mdc"), orchestratorRule, "utf-8");
668
1003
  console.log(chalk3.green(" Generated .cursor/rules/orchestrator.mdc"));
1004
+ const cleanedOrchestratorForAgents = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
1005
+ const skillsSectionCursor = await buildSkillsSection(hubDir, config);
1006
+ const personaCursor = await loadPersona(hubDir);
1007
+ const personaSectionCursor = personaCursor ? buildPersonaSection(personaCursor) : "";
1008
+ const agentsMdCursor = [cleanedOrchestratorForAgents, skillsSectionCursor, personaSectionCursor].filter(Boolean).join("\n");
1009
+ await writeFile4(join4(hubDir, "AGENTS.md"), agentsMdCursor + "\n", "utf-8");
1010
+ console.log(chalk3.green(" Generated AGENTS.md"));
1011
+ if (personaCursor) {
1012
+ console.log(chalk3.green(` Applied persona: ${personaCursor.name} (${personaCursor.role})`));
1013
+ }
669
1014
  const hubSteeringDirCursor = resolve2(hubDir, "steering");
670
1015
  try {
671
1016
  const steeringFiles = await readdir2(hubSteeringDirCursor);
672
1017
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
673
1018
  for (const file of mdFiles) {
674
- const raw = await readFile3(join3(hubSteeringDirCursor, file), "utf-8");
1019
+ const raw = await readFile4(join4(hubSteeringDirCursor, file), "utf-8");
675
1020
  const content = stripFrontMatter(raw);
676
1021
  const mdcName = file.replace(/\.md$/, ".mdc");
677
1022
  const mdcContent = `---
@@ -680,7 +1025,7 @@ alwaysApply: true
680
1025
  ---
681
1026
 
682
1027
  ${content}`;
683
- await writeFile3(join3(cursorDir, "rules", mdcName), mdcContent, "utf-8");
1028
+ await writeFile4(join4(cursorDir, "rules", mdcName), mdcContent, "utf-8");
684
1029
  }
685
1030
  if (mdFiles.length > 0) {
686
1031
  console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .cursor/rules/`));
@@ -691,8 +1036,16 @@ ${content}`;
691
1036
  try {
692
1037
  const agentFiles = await readdir2(agentsDir);
693
1038
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1039
+ const sandboxSvc = getSandboxService(config);
694
1040
  for (const file of mdFiles) {
695
- await copyFile(join3(agentsDir, file), join3(cursorDir, "agents", file));
1041
+ if (sandboxSvc) {
1042
+ const agentName = file.replace(/\.md$/, "");
1043
+ const agentContent = await readFile4(join4(agentsDir, file), "utf-8");
1044
+ const withSandbox = injectSandboxContext(agentName, agentContent, sandboxSvc.port);
1045
+ await writeFile4(join4(cursorDir, "agents", file), withSandbox, "utf-8");
1046
+ } else {
1047
+ await copyFile(join4(agentsDir, file), join4(cursorDir, "agents", file));
1048
+ }
696
1049
  }
697
1050
  console.log(chalk3.green(` Copied ${mdFiles.length} agent definitions`));
698
1051
  } catch {
@@ -701,15 +1054,15 @@ ${content}`;
701
1054
  const skillsDir = resolve2(hubDir, "skills");
702
1055
  try {
703
1056
  const skillFolders = await readdir2(skillsDir);
704
- const cursorSkillsDir = join3(cursorDir, "skills");
705
- await mkdir3(cursorSkillsDir, { recursive: true });
1057
+ const cursorSkillsDir = join4(cursorDir, "skills");
1058
+ await mkdir4(cursorSkillsDir, { recursive: true });
706
1059
  let count = 0;
707
1060
  for (const folder of skillFolders) {
708
- const skillFile = join3(skillsDir, folder, "SKILL.md");
1061
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
709
1062
  try {
710
- await readFile3(skillFile);
711
- const srcDir = join3(skillsDir, folder);
712
- const targetDir = join3(cursorSkillsDir, folder);
1063
+ await readFile4(skillFile);
1064
+ const srcDir = join4(skillsDir, folder);
1065
+ const targetDir = join4(cursorSkillsDir, folder);
713
1066
  await cp(srcDir, targetDir, { recursive: true });
714
1067
  count++;
715
1068
  } catch {
@@ -720,15 +1073,15 @@ ${content}`;
720
1073
  }
721
1074
  } catch {
722
1075
  }
723
- const cursorSkillsDirForDocs = join3(cursorDir, "skills");
724
- await mkdir3(cursorSkillsDirForDocs, { recursive: true });
1076
+ const cursorSkillsDirForDocs = join4(cursorDir, "skills");
1077
+ await mkdir4(cursorSkillsDirForDocs, { recursive: true });
725
1078
  await fetchHubDocsSkill(cursorSkillsDirForDocs);
726
- await syncRemoteSources(config, hubDir, join3(cursorDir, "skills"), join3(cursorDir, "rules"));
1079
+ await syncRemoteSources(config, hubDir, join4(cursorDir, "skills"), join4(cursorDir, "rules"));
727
1080
  if (config.hooks) {
728
1081
  const cursorHooks = buildCursorHooks(config.hooks);
729
1082
  if (cursorHooks) {
730
- await writeFile3(
731
- join3(cursorDir, "hooks.json"),
1083
+ await writeFile4(
1084
+ join4(cursorDir, "hooks.json"),
732
1085
  JSON.stringify(cursorHooks, null, 2) + "\n",
733
1086
  "utf-8"
734
1087
  );
@@ -738,6 +1091,14 @@ ${content}`;
738
1091
  await generateEditorCommands(config, hubDir, cursorDir, ".cursor/commands/");
739
1092
  await generateVSCodeSettings(config, hubDir);
740
1093
  }
1094
+ function buildSandboxMcpEntry(port) {
1095
+ return { url: `http://localhost:${port}/mcp` };
1096
+ }
1097
+ function getSandboxService(config) {
1098
+ const svc = config.services?.find((s) => s.type === "sandbox");
1099
+ if (!svc) return null;
1100
+ return { port: svc.port ?? 8080 };
1101
+ }
741
1102
  function buildProxyUpstreams(proxyMcp, allMcps) {
742
1103
  const upstreamNames = new Set(proxyMcp.upstreams || []);
743
1104
  const upstreamEntries = [];
@@ -871,6 +1232,24 @@ function buildKiroMcpEntry(mcp, mode = "editor") {
871
1232
  ...autoApprove && { autoApprove }
872
1233
  };
873
1234
  }
1235
+ var SANDBOX_AGENT_TARGETS = /* @__PURE__ */ new Set(["qa-frontend", "qa-backend", "coding-frontend", "coding-backend"]);
1236
+ function injectSandboxContext(agentName, content, sandboxPort) {
1237
+ if (!SANDBOX_AGENT_TARGETS.has(agentName)) return content;
1238
+ const section = `
1239
+ ## Sandbox Environment
1240
+
1241
+ A sandboxed execution environment is available via the \`sandbox\` MCP (http://localhost:${sandboxPort}/mcp).
1242
+
1243
+ Use it to:
1244
+ - Run shell commands: \`shell.exec\`
1245
+ - Read/write files: \`file.read\`, \`file.write\`
1246
+ - Control a real browser: \`browser.navigate\`, \`browser.screenshot\`, \`browser.click\`
1247
+ - Execute code: \`jupyter.execute\`
1248
+
1249
+ The sandbox workspace is mounted at \`/home/gem/workspace\`. Prefer running builds, tests, and browser interactions inside the sandbox rather than on the host machine.
1250
+ `;
1251
+ return content.trimEnd() + "\n" + section;
1252
+ }
874
1253
  function buildKiroAgentContent(rawContent) {
875
1254
  const fmMatch = rawContent.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
876
1255
  if (!fmMatch) {
@@ -1074,6 +1453,49 @@ You can communicate with agents from other developers on the team via the \`agen
1074
1453
  - Read the thread before replying to avoid repeating what others said
1075
1454
  - When starting a task that touches shared code, check recent threads for relevant context`;
1076
1455
  }
1456
+ function hasKanbanMcp(mcps) {
1457
+ if (!mcps) return false;
1458
+ const proxyMcp = mcps.find((m) => m.upstreams && m.upstreams.length > 0);
1459
+ const directMatch = mcps.some((m) => m.name === "kanban" || m.package === "@arvoretech/kanban-mcp");
1460
+ const upstreamMatch = proxyMcp?.upstreams?.includes("kanban") ?? false;
1461
+ return directMatch || upstreamMatch;
1462
+ }
1463
+ function buildKanbanSection(mcps) {
1464
+ if (!hasKanbanMcp(mcps)) return "";
1465
+ return `
1466
+ ## Kanban Board
1467
+
1468
+ This workspace has a persistent kanban board via the \`kanban\` MCP. Use it to organize work, track progress across sessions, and coordinate with other chats.
1469
+
1470
+ **When to use the kanban:**
1471
+ - At the start of a task, check the board for existing cards and active sessions
1472
+ - Break complex features into cards before starting implementation
1473
+ - Claim cards you're working on so other sessions can see
1474
+ - Release cards when done (default status: review)
1475
+ - Search for related cards before creating duplicates
1476
+
1477
+ **Workflow:**
1478
+ 1. \`list_boards\` / \`get_board\` \u2014 See what's on the board and who's working on what
1479
+ 2. \`create_card\` \u2014 Add new tasks to the appropriate column
1480
+ 3. \`claim_card\` \u2014 Mark a card as being worked on by this session
1481
+ 4. \`move_card\` \u2014 Move cards between columns as work progresses
1482
+ 5. \`release_card\` \u2014 Release when done, with status and detail (e.g. "PR #123 created")
1483
+ 6. \`search_cards\` \u2014 Find cards by meaning (semantic search)
1484
+
1485
+ **Available tools:** \`list_boards\`, \`create_board\`, \`get_board\`, \`get_card\`, \`create_card\`, \`update_card\`, \`move_card\`, \`claim_card\`, \`release_card\`, \`search_cards\`, \`archive_card\`, \`delete_card\`.
1486
+
1487
+ **Multi-session coordination:**
1488
+ - Always \`claim_card\` before starting work \u2014 other sessions will see it's taken
1489
+ - If a card is already claimed, pick another or use \`force: true\` to override stale sessions
1490
+ - Use \`get_board\` to see active sessions with duration (helps identify abandoned claims)
1491
+ - When finishing, \`release_card\` with a meaningful detail so the next session has context
1492
+
1493
+ **Best practices:**
1494
+ - Use subtasks (\`parent_card_id\`) to break down large cards
1495
+ - Tag cards consistently for easy filtering
1496
+ - Set priority to help triage (urgent > high > medium > low)
1497
+ - Check the board at the start of every session \u2014 don't start from zero`;
1498
+ }
1077
1499
  function buildMcpToolsSection(mcps) {
1078
1500
  if (!mcps || mcps.length === 0) return "";
1079
1501
  const proxyMcp = mcps.find((m) => m.upstreams && m.upstreams.length > 0);
@@ -1125,6 +1547,141 @@ ${mcp.instructions.trim()}`);
1125
1547
  }
1126
1548
  return lines.join("\n");
1127
1549
  }
1550
+ async function buildSkillsSection(hubDir, config) {
1551
+ const skillsDir = resolve2(hubDir, "skills");
1552
+ const skillEntries = [];
1553
+ try {
1554
+ const folders = await readdir2(skillsDir);
1555
+ for (const folder of folders) {
1556
+ const skillPath = join4(skillsDir, folder, "SKILL.md");
1557
+ try {
1558
+ const content = await readFile4(skillPath, "utf-8");
1559
+ const fm = parseFrontMatter(content);
1560
+ if (fm?.name) {
1561
+ skillEntries.push({
1562
+ name: fm.name,
1563
+ description: fm.description || ""
1564
+ });
1565
+ }
1566
+ } catch {
1567
+ }
1568
+ }
1569
+ } catch {
1570
+ return null;
1571
+ }
1572
+ if (skillEntries.length === 0) return null;
1573
+ const repoSkillMap = /* @__PURE__ */ new Map();
1574
+ for (const repo of config.repos) {
1575
+ if (repo.skills?.length) {
1576
+ for (const skill of repo.skills) {
1577
+ const repos = repoSkillMap.get(skill) || [];
1578
+ repos.push(repo.path);
1579
+ repoSkillMap.set(skill, repos);
1580
+ }
1581
+ }
1582
+ }
1583
+ const parts = [];
1584
+ parts.push(`
1585
+ ## Skills
1586
+
1587
+ This workspace has skills that provide specialized knowledge for specific domains and repositories.
1588
+ Consult the relevant skill before working in an unfamiliar area \u2014 they contain patterns, conventions, and project-specific guidance.
1589
+
1590
+ | Skill | Description | Repositories |
1591
+ |-------|-------------|--------------|`);
1592
+ for (const entry of skillEntries) {
1593
+ const repos = repoSkillMap.get(entry.name);
1594
+ const repoCol = repos ? repos.map((r) => `\`${r}\``).join(", ") : "\u2014";
1595
+ const desc = entry.description.replace(/\|/g, "\\|").split(".")[0].trim();
1596
+ parts.push(`| \`${entry.name}\` | ${desc} | ${repoCol} |`);
1597
+ }
1598
+ parts.push(`
1599
+ When to consult a skill:
1600
+ - Before writing code in a repository that has an associated skill
1601
+ - When making architecture or pattern decisions in a specific domain
1602
+ - When unsure about project conventions, libraries, or testing approaches
1603
+ - When the user's request touches a domain covered by an available skill
1604
+
1605
+ Additional context sources:
1606
+ - Use documentation MCPs to check library and framework docs before implementing
1607
+ - Use database MCPs to understand schema, query data, and verify state
1608
+ - Use package registry MCPs to verify security and versions before installing dependencies
1609
+ - Use the repository CLI commands (build, test, lint) to validate changes after implementation
1610
+ - Use monitoring MCPs for production debugging and log analysis when available`);
1611
+ return parts.join("\n");
1612
+ }
1613
+ function buildCoreBehaviorSections() {
1614
+ const sections = [];
1615
+ sections.push(`
1616
+ ## Core Behavior
1617
+
1618
+ Be concise, clear, direct, and useful.
1619
+ Prefer technical accuracy over reassurance.
1620
+ Do not use hype, flattery, or exaggerated validation.
1621
+ Do not repeatedly apologize when something unexpected happens \u2014 explain what happened and continue.
1622
+ Do not claim actions were performed unless they were actually performed.
1623
+ Never invent facts, code behavior, file contents, tool capabilities, or execution outcomes.
1624
+ Focus on completing the user's task, not on narrating unnecessary process.`);
1625
+ sections.push(`
1626
+ ## Working Style
1627
+
1628
+ Prefer the simplest solution that fully satisfies the request.
1629
+ Avoid over-engineering, speculative abstractions, premature generalization, and cleanup outside the requested scope.
1630
+ Prefer editing existing files over creating new files.
1631
+ Prefer minimal, reversible changes over broad rewrites unless the task explicitly requires a rewrite.
1632
+ Ask the user questions only when a real ambiguity materially affects the solution.
1633
+ Bias toward finding the answer yourself when the available context and tools are sufficient.`);
1634
+ sections.push(`
1635
+ ## Search, Reading, and Investigation
1636
+
1637
+ If you are unsure how to satisfy the user's request, gather more information before answering.
1638
+ Prefer discovering answers yourself over asking the user for information that is likely available in the workspace, files, memories, or tools.
1639
+
1640
+ When reading code or documents:
1641
+ - Read enough surrounding context to avoid missing critical behavior
1642
+ - Do not propose modifications to code you have not inspected
1643
+ - If partial views may hide important logic, continue reading before deciding
1644
+
1645
+ For broader exploration:
1646
+ - Use lightweight search first
1647
+ - Escalate to deeper exploration or subagents only when the task is broad, ambiguous, or likely to require several search passes`);
1648
+ sections.push(`
1649
+ ## Code Changes
1650
+
1651
+ When making code changes:
1652
+ - Ensure the produced code is runnable and internally consistent
1653
+ - Add required imports, wiring, dependencies, and integration points
1654
+ - Preserve the project's existing patterns unless there is a strong reason to change them
1655
+ - Read the relevant files or sections before modifying existing code
1656
+ - Understand the surrounding code paths and conventions
1657
+ - Prefer small, precise edits
1658
+
1659
+ If you introduce errors:
1660
+ - Try to fix them
1661
+ - Do not get stuck in unbounded retry loops (max 3 attempts on the same issue)
1662
+ - If repeated fixes fail, explain the remaining problem clearly
1663
+
1664
+ Never assume a library is available \u2014 check the dependency file or neighboring code first.
1665
+ When creating a new component, look at existing components to understand conventions.`);
1666
+ sections.push(`
1667
+ ## Security and Safety
1668
+
1669
+ Never hardcode secrets, credentials, tokens, or API keys.
1670
+ Flag security risks when noticed.
1671
+ Avoid introducing vulnerabilities such as command injection, SQL injection, XSS, insecure secret handling, broken auth flows, unsafe deserialization, SSRF, or privilege escalation.
1672
+ Do not expose secrets in code, tests, examples, or logs.`);
1673
+ sections.push(`
1674
+ ## Git and Operational Discipline
1675
+
1676
+ Do not commit, push, open pull requests, or notify external systems unless the user asked for it or the workspace flow explicitly requires it.
1677
+
1678
+ When handling git work:
1679
+ - Inspect status and diff before committing
1680
+ - Follow existing repository commit conventions
1681
+ - Prefer specific file staging over indiscriminate staging
1682
+ - Do not use destructive git commands without explicit user authorization`);
1683
+ return sections;
1684
+ }
1128
1685
  function buildOpenCodeOrchestratorRule(config) {
1129
1686
  const taskFolder = config.workflow?.task_folder || "./tasks/<TASK_ID>/";
1130
1687
  const steps = config.workflow?.pipeline || [];
@@ -1218,6 +1775,8 @@ Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_mem
1218
1775
  if (agentTeamsSectionOpenCode) sections.push(agentTeamsSectionOpenCode);
1219
1776
  const agentTeamsChatSectionOpenCode = buildAgentTeamsChatSection(config.mcps);
1220
1777
  if (agentTeamsChatSectionOpenCode) sections.push(agentTeamsChatSectionOpenCode);
1778
+ const kanbanSectionOpenCode = buildKanbanSection(config.mcps);
1779
+ if (kanbanSectionOpenCode) sections.push(kanbanSectionOpenCode);
1221
1780
  sections.push(`
1222
1781
  ## Troubleshooting and Debugging
1223
1782
 
@@ -1228,6 +1787,7 @@ It will:
1228
1787
  3. Form and test hypotheses systematically
1229
1788
  4. Identify the root cause
1230
1789
  5. Propose a solution or call coding agents to implement the fix`);
1790
+ sections.push(...buildCoreBehaviorSections());
1231
1791
  if (prompt?.sections) {
1232
1792
  const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
1233
1793
  for (const [name, content] of Object.entries(prompt.sections)) {
@@ -1318,18 +1878,18 @@ If any validation agent leaves comments requiring fixes, call the relevant codin
1318
1878
  return parts.join("\n");
1319
1879
  }
1320
1880
  async function generateOpenCode(config, hubDir) {
1321
- const opencodeDir = join3(hubDir, ".opencode");
1322
- await mkdir3(join3(opencodeDir, "agents"), { recursive: true });
1323
- await mkdir3(join3(opencodeDir, "rules"), { recursive: true });
1324
- await mkdir3(join3(opencodeDir, "skills"), { recursive: true });
1325
- await mkdir3(join3(opencodeDir, "commands"), { recursive: true });
1326
- await mkdir3(join3(opencodeDir, "plugins"), { recursive: true });
1881
+ const opencodeDir = join4(hubDir, ".opencode");
1882
+ await mkdir4(join4(opencodeDir, "agents"), { recursive: true });
1883
+ await mkdir4(join4(opencodeDir, "rules"), { recursive: true });
1884
+ await mkdir4(join4(opencodeDir, "skills"), { recursive: true });
1885
+ await mkdir4(join4(opencodeDir, "commands"), { recursive: true });
1886
+ await mkdir4(join4(opencodeDir, "plugins"), { recursive: true });
1327
1887
  const gitignoreLines = buildGitignoreLines(config);
1328
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
1888
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
1329
1889
  console.log(chalk3.green(" Generated .gitignore"));
1330
1890
  if (config.repos.length > 0) {
1331
1891
  const ignoreContent = config.repos.map((r) => `!${r.name}`).join("\n") + "\n";
1332
- await writeFile3(join3(hubDir, ".ignore"), ignoreContent, "utf-8");
1892
+ await writeFile4(join4(hubDir, ".ignore"), ignoreContent, "utf-8");
1333
1893
  console.log(chalk3.green(" Generated .ignore"));
1334
1894
  }
1335
1895
  const orchestratorContent = buildOpenCodeOrchestratorRule(config);
@@ -1337,18 +1897,27 @@ async function generateOpenCode(config, hubDir) {
1337
1897
  "Development orchestrator. Delegates specialized work to subagents following a structured pipeline: refinement, coding, review, QA, and delivery.",
1338
1898
  orchestratorContent
1339
1899
  );
1340
- await writeFile3(join3(opencodeDir, "agents", "orchestrator.md"), orchestratorAgent, "utf-8");
1900
+ await writeFile4(join4(opencodeDir, "agents", "orchestrator.md"), orchestratorAgent, "utf-8");
1341
1901
  console.log(chalk3.green(" Generated .opencode/agents/orchestrator.md (primary agent)"));
1342
- await rm(join3(opencodeDir, "rules", "orchestrator.md")).catch(() => {
1902
+ await rm(join4(opencodeDir, "rules", "orchestrator.md")).catch(() => {
1343
1903
  });
1904
+ const skillsSectionOC = await buildSkillsSection(hubDir, config);
1905
+ const personaOC = await loadPersona(hubDir);
1906
+ const personaSectionOC = personaOC ? buildPersonaSection(personaOC) : "";
1907
+ const agentsMdOC = [orchestratorContent, skillsSectionOC, personaSectionOC].filter(Boolean).join("\n");
1908
+ await writeFile4(join4(hubDir, "AGENTS.md"), agentsMdOC + "\n", "utf-8");
1909
+ console.log(chalk3.green(" Generated AGENTS.md"));
1910
+ if (personaOC) {
1911
+ console.log(chalk3.green(` Applied persona: ${personaOC.name} (${personaOC.role})`));
1912
+ }
1344
1913
  const hubSteeringDirOC = resolve2(hubDir, "steering");
1345
1914
  try {
1346
1915
  const steeringFiles = await readdir2(hubSteeringDirOC);
1347
1916
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
1348
1917
  for (const file of mdFiles) {
1349
- const raw = await readFile3(join3(hubSteeringDirOC, file), "utf-8");
1918
+ const raw = await readFile4(join4(hubSteeringDirOC, file), "utf-8");
1350
1919
  const content = stripFrontMatter(raw);
1351
- await writeFile3(join3(opencodeDir, "rules", file), content, "utf-8");
1920
+ await writeFile4(join4(opencodeDir, "rules", file), content, "utf-8");
1352
1921
  }
1353
1922
  if (mdFiles.length > 0) {
1354
1923
  console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .opencode/rules/`));
@@ -1373,8 +1942,8 @@ async function generateOpenCode(config, hubDir) {
1373
1942
  opencodeConfig.mcp = mcpConfig;
1374
1943
  }
1375
1944
  opencodeConfig.instructions = [".opencode/rules/*.md"];
1376
- await writeFile3(
1377
- join3(hubDir, "opencode.json"),
1945
+ await writeFile4(
1946
+ join4(hubDir, "opencode.json"),
1378
1947
  JSON.stringify(opencodeConfig, null, 2) + "\n",
1379
1948
  "utf-8"
1380
1949
  );
@@ -1385,10 +1954,10 @@ async function generateOpenCode(config, hubDir) {
1385
1954
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1386
1955
  for (const file of mdFiles) {
1387
1956
  if (file === "orchestrator.md") continue;
1388
- const content = await readFile3(join3(agentsDir, file), "utf-8");
1957
+ const content = await readFile4(join4(agentsDir, file), "utf-8");
1389
1958
  const agentName = file.replace(/\.md$/, "");
1390
1959
  const converted = buildOpenCodeAgentMarkdown(agentName, content);
1391
- await writeFile3(join3(opencodeDir, "agents", file), converted, "utf-8");
1960
+ await writeFile4(join4(opencodeDir, "agents", file), converted, "utf-8");
1392
1961
  }
1393
1962
  console.log(chalk3.green(` Copied ${mdFiles.length} agents to .opencode/agents/`));
1394
1963
  } catch {
@@ -1399,10 +1968,10 @@ async function generateOpenCode(config, hubDir) {
1399
1968
  const skillFolders = await readdir2(skillsDir);
1400
1969
  let count = 0;
1401
1970
  for (const folder of skillFolders) {
1402
- const skillFile = join3(skillsDir, folder, "SKILL.md");
1971
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
1403
1972
  try {
1404
- await readFile3(skillFile);
1405
- await cp(join3(skillsDir, folder), join3(opencodeDir, "skills", folder), { recursive: true });
1973
+ await readFile4(skillFile);
1974
+ await cp(join4(skillsDir, folder), join4(opencodeDir, "skills", folder), { recursive: true });
1406
1975
  count++;
1407
1976
  } catch {
1408
1977
  }
@@ -1412,13 +1981,13 @@ async function generateOpenCode(config, hubDir) {
1412
1981
  }
1413
1982
  } catch {
1414
1983
  }
1415
- await fetchHubDocsSkill(join3(opencodeDir, "skills"));
1416
- await syncRemoteSources(config, hubDir, join3(opencodeDir, "skills"), join3(opencodeDir, "rules"));
1984
+ await fetchHubDocsSkill(join4(opencodeDir, "skills"));
1985
+ await syncRemoteSources(config, hubDir, join4(opencodeDir, "skills"), join4(opencodeDir, "rules"));
1417
1986
  await generateEditorCommands(config, hubDir, opencodeDir, ".opencode/commands/");
1418
1987
  if (config.hooks) {
1419
1988
  const plugin = buildOpenCodeHooksPlugin(config.hooks);
1420
1989
  if (plugin) {
1421
- await writeFile3(join3(opencodeDir, "plugins", "hub-hooks.js"), plugin, "utf-8");
1990
+ await writeFile4(join4(opencodeDir, "plugins", "hub-hooks.js"), plugin, "utf-8");
1422
1991
  console.log(chalk3.green(" Generated .opencode/plugins/hub-hooks.js"));
1423
1992
  }
1424
1993
  }
@@ -1528,6 +2097,8 @@ Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_mem
1528
2097
  if (agentTeamsSectionKiro) sections.push(agentTeamsSectionKiro);
1529
2098
  const agentTeamsChatSectionKiro = buildAgentTeamsChatSection(config.mcps);
1530
2099
  if (agentTeamsChatSectionKiro) sections.push(agentTeamsChatSectionKiro);
2100
+ const kanbanSectionKiro = buildKanbanSection(config.mcps);
2101
+ if (kanbanSectionKiro) sections.push(kanbanSectionKiro);
1531
2102
  sections.push(`
1532
2103
  ## Troubleshooting and Debugging
1533
2104
 
@@ -1537,6 +2108,7 @@ For bug reports or unexpected behavior, follow the debugging process from the \`
1537
2108
  3. Form and test hypotheses systematically
1538
2109
  4. Identify the root cause
1539
2110
  5. Propose and implement the fix`);
2111
+ sections.push(...buildCoreBehaviorSections());
1540
2112
  if (prompt?.sections) {
1541
2113
  const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
1542
2114
  for (const [name, content] of Object.entries(prompt.sections)) {
@@ -1720,6 +2292,8 @@ Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_mem
1720
2292
  if (agentTeamsSectionCursor) sections.push(agentTeamsSectionCursor);
1721
2293
  const agentTeamsChatSectionCursor = buildAgentTeamsChatSection(config.mcps);
1722
2294
  if (agentTeamsChatSectionCursor) sections.push(agentTeamsChatSectionCursor);
2295
+ const kanbanSectionCursor = buildKanbanSection(config.mcps);
2296
+ if (kanbanSectionCursor) sections.push(kanbanSectionCursor);
1723
2297
  sections.push(`
1724
2298
  ## Troubleshooting and Debugging
1725
2299
 
@@ -1730,6 +2304,7 @@ It will:
1730
2304
  3. Form and test hypotheses systematically
1731
2305
  4. Identify the root cause
1732
2306
  5. Propose a solution or call coding agents to implement the fix`);
2307
+ sections.push(...buildCoreBehaviorSections());
1733
2308
  if (prompt?.sections) {
1734
2309
  const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
1735
2310
  for (const [name, content] of Object.entries(prompt.sections)) {
@@ -1899,10 +2474,19 @@ function formatAction(action) {
1899
2474
  return map[action] || action;
1900
2475
  }
1901
2476
  async function generateClaudeCode(config, hubDir) {
1902
- const claudeDir = join3(hubDir, ".claude");
1903
- await mkdir3(join3(claudeDir, "agents"), { recursive: true });
2477
+ const claudeDir = join4(hubDir, ".claude");
2478
+ await mkdir4(join4(claudeDir, "agents"), { recursive: true });
1904
2479
  const orchestratorRule = buildOrchestratorRule(config);
1905
2480
  const cleanedOrchestrator = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
2481
+ const skillsSectionClaude = await buildSkillsSection(hubDir, config);
2482
+ const personaClaude = await loadPersona(hubDir);
2483
+ const personaSectionClaude = personaClaude ? buildPersonaSection(personaClaude) : "";
2484
+ const agentsMdClaude = [cleanedOrchestrator, skillsSectionClaude, personaSectionClaude].filter(Boolean).join("\n");
2485
+ await writeFile4(join4(hubDir, "AGENTS.md"), agentsMdClaude + "\n", "utf-8");
2486
+ console.log(chalk3.green(" Generated AGENTS.md"));
2487
+ if (personaClaude) {
2488
+ console.log(chalk3.green(` Applied persona: ${personaClaude.name} (${personaClaude.role})`));
2489
+ }
1906
2490
  const claudeMdSections = [];
1907
2491
  claudeMdSections.push(cleanedOrchestrator);
1908
2492
  const agentsDir = resolve2(hubDir, "agents");
@@ -1910,7 +2494,7 @@ async function generateClaudeCode(config, hubDir) {
1910
2494
  const agentFiles = await readdir2(agentsDir);
1911
2495
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1912
2496
  for (const file of mdFiles) {
1913
- await copyFile(join3(agentsDir, file), join3(claudeDir, "agents", file));
2497
+ await copyFile(join4(agentsDir, file), join4(claudeDir, "agents", file));
1914
2498
  }
1915
2499
  console.log(chalk3.green(` Copied ${mdFiles.length} agents to .claude/agents/`));
1916
2500
  } catch {
@@ -1919,15 +2503,15 @@ async function generateClaudeCode(config, hubDir) {
1919
2503
  const skillsDir = resolve2(hubDir, "skills");
1920
2504
  try {
1921
2505
  const skillFolders = await readdir2(skillsDir);
1922
- const claudeSkillsDir = join3(claudeDir, "skills");
1923
- await mkdir3(claudeSkillsDir, { recursive: true });
2506
+ const claudeSkillsDir = join4(claudeDir, "skills");
2507
+ await mkdir4(claudeSkillsDir, { recursive: true });
1924
2508
  let count = 0;
1925
2509
  for (const folder of skillFolders) {
1926
- const skillFile = join3(skillsDir, folder, "SKILL.md");
2510
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
1927
2511
  try {
1928
- await readFile3(skillFile);
1929
- const srcDir = join3(skillsDir, folder);
1930
- const targetDir = join3(claudeSkillsDir, folder);
2512
+ await readFile4(skillFile);
2513
+ const srcDir = join4(skillsDir, folder);
2514
+ const targetDir = join4(claudeSkillsDir, folder);
1931
2515
  await cp(srcDir, targetDir, { recursive: true });
1932
2516
  count++;
1933
2517
  } catch {
@@ -1938,16 +2522,16 @@ async function generateClaudeCode(config, hubDir) {
1938
2522
  }
1939
2523
  } catch {
1940
2524
  }
1941
- const claudeSkillsDirForDocs = join3(claudeDir, "skills");
1942
- await mkdir3(claudeSkillsDirForDocs, { recursive: true });
2525
+ const claudeSkillsDirForDocs = join4(claudeDir, "skills");
2526
+ await mkdir4(claudeSkillsDirForDocs, { recursive: true });
1943
2527
  await fetchHubDocsSkill(claudeSkillsDirForDocs);
1944
- await syncRemoteSources(config, hubDir, join3(claudeDir, "skills"), join3(claudeDir, "steering"));
2528
+ await syncRemoteSources(config, hubDir, join4(claudeDir, "skills"), join4(claudeDir, "steering"));
1945
2529
  const hubSteeringDirClaude = resolve2(hubDir, "steering");
1946
2530
  try {
1947
2531
  const steeringFiles = await readdir2(hubSteeringDirClaude);
1948
2532
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
1949
2533
  for (const file of mdFiles) {
1950
- const raw = await readFile3(join3(hubSteeringDirClaude, file), "utf-8");
2534
+ const raw = await readFile4(join4(hubSteeringDirClaude, file), "utf-8");
1951
2535
  const content = stripFrontMatter(raw).trim();
1952
2536
  if (content) {
1953
2537
  claudeMdSections.push(content);
@@ -1958,7 +2542,7 @@ async function generateClaudeCode(config, hubDir) {
1958
2542
  }
1959
2543
  } catch {
1960
2544
  }
1961
- await writeFile3(join3(hubDir, "CLAUDE.md"), claudeMdSections.join("\n\n"), "utf-8");
2545
+ await writeFile4(join4(hubDir, "CLAUDE.md"), claudeMdSections.join("\n\n"), "utf-8");
1962
2546
  console.log(chalk3.green(" Generated CLAUDE.md"));
1963
2547
  if (config.mcps?.length) {
1964
2548
  const mcpJson = {};
@@ -1971,8 +2555,8 @@ async function generateClaudeCode(config, hubDir) {
1971
2555
  mcpJson[mcp.name] = buildClaudeCodeMcpEntry(mcp);
1972
2556
  }
1973
2557
  }
1974
- await writeFile3(
1975
- join3(hubDir, ".mcp.json"),
2558
+ await writeFile4(
2559
+ join4(hubDir, ".mcp.json"),
1976
2560
  JSON.stringify({ mcpServers: mcpJson }, null, 2) + "\n",
1977
2561
  "utf-8"
1978
2562
  );
@@ -2014,22 +2598,22 @@ async function generateClaudeCode(config, hubDir) {
2014
2598
  claudeSettings.hooks = claudeHooks;
2015
2599
  }
2016
2600
  }
2017
- await writeFile3(
2018
- join3(claudeDir, "settings.json"),
2601
+ await writeFile4(
2602
+ join4(claudeDir, "settings.json"),
2019
2603
  JSON.stringify(claudeSettings, null, 2) + "\n",
2020
2604
  "utf-8"
2021
2605
  );
2022
2606
  console.log(chalk3.green(" Generated .claude/settings.json"));
2023
2607
  const gitignoreLines = buildGitignoreLines(config);
2024
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
2608
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
2025
2609
  console.log(chalk3.green(" Generated .gitignore"));
2026
2610
  }
2027
2611
  async function generateKiro(config, hubDir) {
2028
- const kiroDir = join3(hubDir, ".kiro");
2029
- const steeringDir = join3(kiroDir, "steering");
2030
- const settingsDir = join3(kiroDir, "settings");
2031
- await mkdir3(steeringDir, { recursive: true });
2032
- await mkdir3(settingsDir, { recursive: true });
2612
+ const kiroDir = join4(hubDir, ".kiro");
2613
+ const steeringDir = join4(kiroDir, "steering");
2614
+ const settingsDir = join4(kiroDir, "settings");
2615
+ await mkdir4(steeringDir, { recursive: true });
2616
+ await mkdir4(settingsDir, { recursive: true });
2033
2617
  let mode = await getKiroMode(hubDir);
2034
2618
  if (!mode) {
2035
2619
  const { kiroMode } = await inquirer.prompt([
@@ -2050,26 +2634,32 @@ async function generateKiro(config, hubDir) {
2050
2634
  console.log(chalk3.dim(` Using saved Kiro mode: ${mode}`));
2051
2635
  }
2052
2636
  const gitignoreLines = buildGitignoreLines(config);
2053
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
2637
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
2054
2638
  console.log(chalk3.green(" Generated .gitignore"));
2055
2639
  const kiroRule = buildKiroOrchestratorRule(config);
2056
- const kiroOrchestrator = buildKiroSteeringContent(kiroRule, "always", { name: "orchestrator" });
2057
- await writeFile3(join3(steeringDir, "orchestrator.md"), kiroOrchestrator, "utf-8");
2058
- console.log(chalk3.green(" Generated .kiro/steering/orchestrator.md"));
2059
- await writeFile3(join3(hubDir, "AGENTS.md"), kiroRule + "\n", "utf-8");
2640
+ const skillsSection = await buildSkillsSection(hubDir, config);
2641
+ const personaKiro = await loadPersona(hubDir);
2642
+ const personaSectionKiro = personaKiro ? buildPersonaSection(personaKiro) : "";
2643
+ const kiroRuleWithSkills = [kiroRule, skillsSection, personaSectionKiro].filter(Boolean).join("\n");
2644
+ await writeFile4(join4(hubDir, "AGENTS.md"), kiroRuleWithSkills + "\n", "utf-8");
2060
2645
  console.log(chalk3.green(" Generated AGENTS.md"));
2646
+ if (personaKiro) {
2647
+ console.log(chalk3.green(` Applied persona: ${personaKiro.name} (${personaKiro.role})`));
2648
+ }
2649
+ await rm(join4(steeringDir, "orchestrator.md")).catch(() => {
2650
+ });
2061
2651
  const hubSteeringDir = resolve2(hubDir, "steering");
2062
2652
  try {
2063
2653
  const steeringFiles = await readdir2(hubSteeringDir);
2064
2654
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
2065
2655
  for (const file of mdFiles) {
2066
- const raw = await readFile3(join3(hubSteeringDir, file), "utf-8");
2656
+ const raw = await readFile4(join4(hubSteeringDir, file), "utf-8");
2067
2657
  const content = stripFrontMatter(raw);
2068
- const destPath = join3(steeringDir, file);
2658
+ const destPath = join4(steeringDir, file);
2069
2659
  let inclusion = "always";
2070
2660
  let meta;
2071
- if (existsSync2(destPath)) {
2072
- const existingContent = await readFile3(destPath, "utf-8");
2661
+ if (existsSync3(destPath)) {
2662
+ const existingContent = await readFile4(destPath, "utf-8");
2073
2663
  const existingFm = parseFrontMatter(existingContent);
2074
2664
  if (existingFm) {
2075
2665
  if (existingFm.inclusion === "auto" || existingFm.inclusion === "manual" || existingFm.inclusion === "fileMatch") {
@@ -2094,7 +2684,7 @@ async function generateKiro(config, hubDir) {
2094
2684
  }
2095
2685
  }
2096
2686
  const kiroSteering = buildKiroSteeringContent(content, inclusion, meta);
2097
- await writeFile3(destPath, kiroSteering, "utf-8");
2687
+ await writeFile4(destPath, kiroSteering, "utf-8");
2098
2688
  }
2099
2689
  if (mdFiles.length > 0) {
2100
2690
  console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
@@ -2103,14 +2693,17 @@ async function generateKiro(config, hubDir) {
2103
2693
  }
2104
2694
  const agentsDir = resolve2(hubDir, "agents");
2105
2695
  try {
2106
- const kiroAgentsDir = join3(kiroDir, "agents");
2107
- await mkdir3(kiroAgentsDir, { recursive: true });
2696
+ const kiroAgentsDir = join4(kiroDir, "agents");
2697
+ await mkdir4(kiroAgentsDir, { recursive: true });
2108
2698
  const agentFiles = await readdir2(agentsDir);
2109
2699
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
2110
2700
  for (const file of mdFiles) {
2111
- const agentContent = await readFile3(join3(agentsDir, file), "utf-8");
2112
- const kiroAgent = buildKiroAgentContent(agentContent);
2113
- await writeFile3(join3(kiroAgentsDir, file), kiroAgent, "utf-8");
2701
+ const agentContent = await readFile4(join4(agentsDir, file), "utf-8");
2702
+ const agentName = file.replace(/\.md$/, "");
2703
+ const sandboxSvc = getSandboxService(config);
2704
+ const withSandbox = sandboxSvc ? injectSandboxContext(agentName, agentContent, sandboxSvc.port) : agentContent;
2705
+ const kiroAgent = buildKiroAgentContent(withSandbox);
2706
+ await writeFile4(join4(kiroAgentsDir, file), kiroAgent, "utf-8");
2114
2707
  }
2115
2708
  console.log(chalk3.green(` Copied ${mdFiles.length} agents to .kiro/agents/`));
2116
2709
  } catch {
@@ -2119,15 +2712,15 @@ async function generateKiro(config, hubDir) {
2119
2712
  const skillsDir = resolve2(hubDir, "skills");
2120
2713
  try {
2121
2714
  const skillFolders = await readdir2(skillsDir);
2122
- const kiroSkillsDir = join3(kiroDir, "skills");
2123
- await mkdir3(kiroSkillsDir, { recursive: true });
2715
+ const kiroSkillsDir = join4(kiroDir, "skills");
2716
+ await mkdir4(kiroSkillsDir, { recursive: true });
2124
2717
  let count = 0;
2125
2718
  for (const folder of skillFolders) {
2126
- const skillFile = join3(skillsDir, folder, "SKILL.md");
2719
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
2127
2720
  try {
2128
- await readFile3(skillFile);
2129
- const srcDir = join3(skillsDir, folder);
2130
- const targetDir = join3(kiroSkillsDir, folder);
2721
+ await readFile4(skillFile);
2722
+ const srcDir = join4(skillsDir, folder);
2723
+ const targetDir = join4(kiroSkillsDir, folder);
2131
2724
  await cp(srcDir, targetDir, { recursive: true });
2132
2725
  count++;
2133
2726
  } catch {
@@ -2138,10 +2731,10 @@ async function generateKiro(config, hubDir) {
2138
2731
  }
2139
2732
  } catch {
2140
2733
  }
2141
- const kiroSkillsDirForDocs = join3(kiroDir, "skills");
2142
- await mkdir3(kiroSkillsDirForDocs, { recursive: true });
2734
+ const kiroSkillsDirForDocs = join4(kiroDir, "skills");
2735
+ await mkdir4(kiroSkillsDirForDocs, { recursive: true });
2143
2736
  await fetchHubDocsSkill(kiroSkillsDirForDocs);
2144
- await syncRemoteSources(config, hubDir, join3(kiroDir, "skills"), steeringDir);
2737
+ await syncRemoteSources(config, hubDir, join4(kiroDir, "skills"), steeringDir);
2145
2738
  if (config.mcps?.length) {
2146
2739
  const mcpConfig = {};
2147
2740
  const upstreamSet = getUpstreamNames(config.mcps);
@@ -2154,10 +2747,14 @@ async function generateKiro(config, hubDir) {
2154
2747
  mcpConfig[mcp.name] = buildKiroMcpEntry(mcp, mode);
2155
2748
  }
2156
2749
  }
2157
- const mcpJsonPath = join3(settingsDir, "mcp.json");
2750
+ const sandbox = getSandboxService(config);
2751
+ if (sandbox && !mcpConfig["sandbox"]) {
2752
+ mcpConfig["sandbox"] = buildSandboxMcpEntry(sandbox.port);
2753
+ }
2754
+ const mcpJsonPath = join4(settingsDir, "mcp.json");
2158
2755
  const disabledState = await readExistingMcpDisabledState(mcpJsonPath);
2159
2756
  applyDisabledState(mcpConfig, disabledState);
2160
- await writeFile3(
2757
+ await writeFile4(
2161
2758
  mcpJsonPath,
2162
2759
  JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
2163
2760
  "utf-8"
@@ -2184,13 +2781,13 @@ async function generateKiro(config, hubDir) {
2184
2781
  await generateVSCodeSettings(config, hubDir);
2185
2782
  }
2186
2783
  async function generateVSCodeSettings(config, hubDir) {
2187
- const vscodeDir = join3(hubDir, ".vscode");
2188
- await mkdir3(vscodeDir, { recursive: true });
2189
- const settingsPath = join3(vscodeDir, "settings.json");
2784
+ const vscodeDir = join4(hubDir, ".vscode");
2785
+ await mkdir4(vscodeDir, { recursive: true });
2786
+ const settingsPath = join4(vscodeDir, "settings.json");
2190
2787
  let existing = {};
2191
- if (existsSync2(settingsPath)) {
2788
+ if (existsSync3(settingsPath)) {
2192
2789
  try {
2193
- const raw = await readFile3(settingsPath, "utf-8");
2790
+ const raw = await readFile4(settingsPath, "utf-8");
2194
2791
  existing = JSON.parse(raw);
2195
2792
  } catch {
2196
2793
  existing = {};
@@ -2202,14 +2799,14 @@ async function generateVSCodeSettings(config, hubDir) {
2202
2799
  "git.detectSubmodulesLimit": Math.max(config.repos.length * 2, 20)
2203
2800
  };
2204
2801
  const merged = { ...existing, ...managed };
2205
- await writeFile3(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
2802
+ await writeFile4(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
2206
2803
  console.log(chalk3.green(" Generated .vscode/settings.json (git multi-repo detection)"));
2207
2804
  const workspaceFile = `${config.name}.code-workspace`;
2208
- const workspacePath = join3(hubDir, workspaceFile);
2805
+ const workspacePath = join4(hubDir, workspaceFile);
2209
2806
  let existingWorkspace = {};
2210
- if (existsSync2(workspacePath)) {
2807
+ if (existsSync3(workspacePath)) {
2211
2808
  try {
2212
- const raw = await readFile3(workspacePath, "utf-8");
2809
+ const raw = await readFile4(workspacePath, "utf-8");
2213
2810
  existingWorkspace = JSON.parse(raw);
2214
2811
  } catch {
2215
2812
  existingWorkspace = {};
@@ -2219,7 +2816,7 @@ async function generateVSCodeSettings(config, hubDir) {
2219
2816
  const existing2 = files.find((f) => f.endsWith(".code-workspace"));
2220
2817
  if (existing2) {
2221
2818
  try {
2222
- const raw = await readFile3(join3(hubDir, existing2), "utf-8");
2819
+ const raw = await readFile4(join4(hubDir, existing2), "utf-8");
2223
2820
  existingWorkspace = JSON.parse(raw);
2224
2821
  } catch {
2225
2822
  existingWorkspace = {};
@@ -2255,7 +2852,7 @@ async function generateVSCodeSettings(config, hubDir) {
2255
2852
  folders,
2256
2853
  settings: existingWorkspace.settings || {}
2257
2854
  };
2258
- await writeFile3(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
2855
+ await writeFile4(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
2259
2856
  console.log(chalk3.green(` Generated ${workspaceFile}`));
2260
2857
  }
2261
2858
  function extractEnvVarsByMcp(mcps) {
@@ -2302,7 +2899,7 @@ async function generateEnvExample(config, hubDir) {
2302
2899
  totalVars++;
2303
2900
  }
2304
2901
  if (totalVars === 0) return;
2305
- await writeFile3(join3(hubDir, ".env.example"), lines.join("\n") + "\n", "utf-8");
2902
+ await writeFile4(join4(hubDir, ".env.example"), lines.join("\n") + "\n", "utf-8");
2306
2903
  console.log(chalk3.green(` Generated .env.example (${totalVars} vars)`));
2307
2904
  }
2308
2905
  function buildGitignoreLines(config) {
@@ -2392,7 +2989,7 @@ async function resolveEditor(opts) {
2392
2989
  ]);
2393
2990
  return editor;
2394
2991
  }
2395
- var generateCommand = new Command("generate").description("Generate editor-specific configuration files from hub.yaml").option("-e, --editor <editor>", "Target editor (cursor, claude-code, kiro, opencode)").option("--reset-editor", "Reset saved editor preference and choose again").option("--check", "Check if generated configs are outdated (exit code 1 if outdated)").action(async (opts) => {
2992
+ var generateCommand = new Command2("generate").description("Generate editor-specific configuration files from hub.yaml").option("-e, --editor <editor>", "Target editor (cursor, claude-code, kiro, opencode)").option("--reset-editor", "Reset saved editor preference and choose again").option("--check", "Check if generated configs are outdated (exit code 1 if outdated)").action(async (opts) => {
2396
2993
  const hubDir = process.cwd();
2397
2994
  if (opts.check) {
2398
2995
  const result = await checkOutdated(hubDir);
@@ -2457,6 +3054,10 @@ Generating ${generator.name} configuration
2457
3054
  });
2458
3055
 
2459
3056
  export {
3057
+ colors,
3058
+ symbols,
3059
+ horizontalLine,
3060
+ personaCommand,
2460
3061
  generators,
2461
3062
  generateCommand,
2462
3063
  checkAndAutoRegenerate