@arvoretech/hub 0.16.0 → 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-YJEPLTSQ.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,27 +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"));
669
1004
  const cleanedOrchestratorForAgents = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
670
1005
  const skillsSectionCursor = await buildSkillsSection(hubDir, config);
671
- const agentsMdCursor = skillsSectionCursor ? cleanedOrchestratorForAgents + "\n" + skillsSectionCursor : cleanedOrchestratorForAgents;
672
- await writeFile3(join3(hubDir, "AGENTS.md"), agentsMdCursor + "\n", "utf-8");
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");
673
1010
  console.log(chalk3.green(" Generated AGENTS.md"));
1011
+ if (personaCursor) {
1012
+ console.log(chalk3.green(` Applied persona: ${personaCursor.name} (${personaCursor.role})`));
1013
+ }
674
1014
  const hubSteeringDirCursor = resolve2(hubDir, "steering");
675
1015
  try {
676
1016
  const steeringFiles = await readdir2(hubSteeringDirCursor);
677
1017
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
678
1018
  for (const file of mdFiles) {
679
- const raw = await readFile3(join3(hubSteeringDirCursor, file), "utf-8");
1019
+ const raw = await readFile4(join4(hubSteeringDirCursor, file), "utf-8");
680
1020
  const content = stripFrontMatter(raw);
681
1021
  const mdcName = file.replace(/\.md$/, ".mdc");
682
1022
  const mdcContent = `---
@@ -685,7 +1025,7 @@ alwaysApply: true
685
1025
  ---
686
1026
 
687
1027
  ${content}`;
688
- await writeFile3(join3(cursorDir, "rules", mdcName), mdcContent, "utf-8");
1028
+ await writeFile4(join4(cursorDir, "rules", mdcName), mdcContent, "utf-8");
689
1029
  }
690
1030
  if (mdFiles.length > 0) {
691
1031
  console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .cursor/rules/`));
@@ -696,8 +1036,16 @@ ${content}`;
696
1036
  try {
697
1037
  const agentFiles = await readdir2(agentsDir);
698
1038
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1039
+ const sandboxSvc = getSandboxService(config);
699
1040
  for (const file of mdFiles) {
700
- 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
+ }
701
1049
  }
702
1050
  console.log(chalk3.green(` Copied ${mdFiles.length} agent definitions`));
703
1051
  } catch {
@@ -706,15 +1054,15 @@ ${content}`;
706
1054
  const skillsDir = resolve2(hubDir, "skills");
707
1055
  try {
708
1056
  const skillFolders = await readdir2(skillsDir);
709
- const cursorSkillsDir = join3(cursorDir, "skills");
710
- await mkdir3(cursorSkillsDir, { recursive: true });
1057
+ const cursorSkillsDir = join4(cursorDir, "skills");
1058
+ await mkdir4(cursorSkillsDir, { recursive: true });
711
1059
  let count = 0;
712
1060
  for (const folder of skillFolders) {
713
- const skillFile = join3(skillsDir, folder, "SKILL.md");
1061
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
714
1062
  try {
715
- await readFile3(skillFile);
716
- const srcDir = join3(skillsDir, folder);
717
- const targetDir = join3(cursorSkillsDir, folder);
1063
+ await readFile4(skillFile);
1064
+ const srcDir = join4(skillsDir, folder);
1065
+ const targetDir = join4(cursorSkillsDir, folder);
718
1066
  await cp(srcDir, targetDir, { recursive: true });
719
1067
  count++;
720
1068
  } catch {
@@ -725,15 +1073,15 @@ ${content}`;
725
1073
  }
726
1074
  } catch {
727
1075
  }
728
- const cursorSkillsDirForDocs = join3(cursorDir, "skills");
729
- await mkdir3(cursorSkillsDirForDocs, { recursive: true });
1076
+ const cursorSkillsDirForDocs = join4(cursorDir, "skills");
1077
+ await mkdir4(cursorSkillsDirForDocs, { recursive: true });
730
1078
  await fetchHubDocsSkill(cursorSkillsDirForDocs);
731
- await syncRemoteSources(config, hubDir, join3(cursorDir, "skills"), join3(cursorDir, "rules"));
1079
+ await syncRemoteSources(config, hubDir, join4(cursorDir, "skills"), join4(cursorDir, "rules"));
732
1080
  if (config.hooks) {
733
1081
  const cursorHooks = buildCursorHooks(config.hooks);
734
1082
  if (cursorHooks) {
735
- await writeFile3(
736
- join3(cursorDir, "hooks.json"),
1083
+ await writeFile4(
1084
+ join4(cursorDir, "hooks.json"),
737
1085
  JSON.stringify(cursorHooks, null, 2) + "\n",
738
1086
  "utf-8"
739
1087
  );
@@ -743,6 +1091,14 @@ ${content}`;
743
1091
  await generateEditorCommands(config, hubDir, cursorDir, ".cursor/commands/");
744
1092
  await generateVSCodeSettings(config, hubDir);
745
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
+ }
746
1102
  function buildProxyUpstreams(proxyMcp, allMcps) {
747
1103
  const upstreamNames = new Set(proxyMcp.upstreams || []);
748
1104
  const upstreamEntries = [];
@@ -876,6 +1232,24 @@ function buildKiroMcpEntry(mcp, mode = "editor") {
876
1232
  ...autoApprove && { autoApprove }
877
1233
  };
878
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
+ }
879
1253
  function buildKiroAgentContent(rawContent) {
880
1254
  const fmMatch = rawContent.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
881
1255
  if (!fmMatch) {
@@ -1179,9 +1553,9 @@ async function buildSkillsSection(hubDir, config) {
1179
1553
  try {
1180
1554
  const folders = await readdir2(skillsDir);
1181
1555
  for (const folder of folders) {
1182
- const skillPath = join3(skillsDir, folder, "SKILL.md");
1556
+ const skillPath = join4(skillsDir, folder, "SKILL.md");
1183
1557
  try {
1184
- const content = await readFile3(skillPath, "utf-8");
1558
+ const content = await readFile4(skillPath, "utf-8");
1185
1559
  const fm = parseFrontMatter(content);
1186
1560
  if (fm?.name) {
1187
1561
  skillEntries.push({
@@ -1504,18 +1878,18 @@ If any validation agent leaves comments requiring fixes, call the relevant codin
1504
1878
  return parts.join("\n");
1505
1879
  }
1506
1880
  async function generateOpenCode(config, hubDir) {
1507
- const opencodeDir = join3(hubDir, ".opencode");
1508
- await mkdir3(join3(opencodeDir, "agents"), { recursive: true });
1509
- await mkdir3(join3(opencodeDir, "rules"), { recursive: true });
1510
- await mkdir3(join3(opencodeDir, "skills"), { recursive: true });
1511
- await mkdir3(join3(opencodeDir, "commands"), { recursive: true });
1512
- 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 });
1513
1887
  const gitignoreLines = buildGitignoreLines(config);
1514
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
1888
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
1515
1889
  console.log(chalk3.green(" Generated .gitignore"));
1516
1890
  if (config.repos.length > 0) {
1517
1891
  const ignoreContent = config.repos.map((r) => `!${r.name}`).join("\n") + "\n";
1518
- await writeFile3(join3(hubDir, ".ignore"), ignoreContent, "utf-8");
1892
+ await writeFile4(join4(hubDir, ".ignore"), ignoreContent, "utf-8");
1519
1893
  console.log(chalk3.green(" Generated .ignore"));
1520
1894
  }
1521
1895
  const orchestratorContent = buildOpenCodeOrchestratorRule(config);
@@ -1523,22 +1897,27 @@ async function generateOpenCode(config, hubDir) {
1523
1897
  "Development orchestrator. Delegates specialized work to subagents following a structured pipeline: refinement, coding, review, QA, and delivery.",
1524
1898
  orchestratorContent
1525
1899
  );
1526
- await writeFile3(join3(opencodeDir, "agents", "orchestrator.md"), orchestratorAgent, "utf-8");
1900
+ await writeFile4(join4(opencodeDir, "agents", "orchestrator.md"), orchestratorAgent, "utf-8");
1527
1901
  console.log(chalk3.green(" Generated .opencode/agents/orchestrator.md (primary agent)"));
1528
- await rm(join3(opencodeDir, "rules", "orchestrator.md")).catch(() => {
1902
+ await rm(join4(opencodeDir, "rules", "orchestrator.md")).catch(() => {
1529
1903
  });
1530
1904
  const skillsSectionOC = await buildSkillsSection(hubDir, config);
1531
- const agentsMdOC = skillsSectionOC ? orchestratorContent + "\n" + skillsSectionOC : orchestratorContent;
1532
- await writeFile3(join3(hubDir, "AGENTS.md"), agentsMdOC + "\n", "utf-8");
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");
1533
1909
  console.log(chalk3.green(" Generated AGENTS.md"));
1910
+ if (personaOC) {
1911
+ console.log(chalk3.green(` Applied persona: ${personaOC.name} (${personaOC.role})`));
1912
+ }
1534
1913
  const hubSteeringDirOC = resolve2(hubDir, "steering");
1535
1914
  try {
1536
1915
  const steeringFiles = await readdir2(hubSteeringDirOC);
1537
1916
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
1538
1917
  for (const file of mdFiles) {
1539
- const raw = await readFile3(join3(hubSteeringDirOC, file), "utf-8");
1918
+ const raw = await readFile4(join4(hubSteeringDirOC, file), "utf-8");
1540
1919
  const content = stripFrontMatter(raw);
1541
- await writeFile3(join3(opencodeDir, "rules", file), content, "utf-8");
1920
+ await writeFile4(join4(opencodeDir, "rules", file), content, "utf-8");
1542
1921
  }
1543
1922
  if (mdFiles.length > 0) {
1544
1923
  console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .opencode/rules/`));
@@ -1563,8 +1942,8 @@ async function generateOpenCode(config, hubDir) {
1563
1942
  opencodeConfig.mcp = mcpConfig;
1564
1943
  }
1565
1944
  opencodeConfig.instructions = [".opencode/rules/*.md"];
1566
- await writeFile3(
1567
- join3(hubDir, "opencode.json"),
1945
+ await writeFile4(
1946
+ join4(hubDir, "opencode.json"),
1568
1947
  JSON.stringify(opencodeConfig, null, 2) + "\n",
1569
1948
  "utf-8"
1570
1949
  );
@@ -1575,10 +1954,10 @@ async function generateOpenCode(config, hubDir) {
1575
1954
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1576
1955
  for (const file of mdFiles) {
1577
1956
  if (file === "orchestrator.md") continue;
1578
- const content = await readFile3(join3(agentsDir, file), "utf-8");
1957
+ const content = await readFile4(join4(agentsDir, file), "utf-8");
1579
1958
  const agentName = file.replace(/\.md$/, "");
1580
1959
  const converted = buildOpenCodeAgentMarkdown(agentName, content);
1581
- await writeFile3(join3(opencodeDir, "agents", file), converted, "utf-8");
1960
+ await writeFile4(join4(opencodeDir, "agents", file), converted, "utf-8");
1582
1961
  }
1583
1962
  console.log(chalk3.green(` Copied ${mdFiles.length} agents to .opencode/agents/`));
1584
1963
  } catch {
@@ -1589,10 +1968,10 @@ async function generateOpenCode(config, hubDir) {
1589
1968
  const skillFolders = await readdir2(skillsDir);
1590
1969
  let count = 0;
1591
1970
  for (const folder of skillFolders) {
1592
- const skillFile = join3(skillsDir, folder, "SKILL.md");
1971
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
1593
1972
  try {
1594
- await readFile3(skillFile);
1595
- 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 });
1596
1975
  count++;
1597
1976
  } catch {
1598
1977
  }
@@ -1602,13 +1981,13 @@ async function generateOpenCode(config, hubDir) {
1602
1981
  }
1603
1982
  } catch {
1604
1983
  }
1605
- await fetchHubDocsSkill(join3(opencodeDir, "skills"));
1606
- 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"));
1607
1986
  await generateEditorCommands(config, hubDir, opencodeDir, ".opencode/commands/");
1608
1987
  if (config.hooks) {
1609
1988
  const plugin = buildOpenCodeHooksPlugin(config.hooks);
1610
1989
  if (plugin) {
1611
- await writeFile3(join3(opencodeDir, "plugins", "hub-hooks.js"), plugin, "utf-8");
1990
+ await writeFile4(join4(opencodeDir, "plugins", "hub-hooks.js"), plugin, "utf-8");
1612
1991
  console.log(chalk3.green(" Generated .opencode/plugins/hub-hooks.js"));
1613
1992
  }
1614
1993
  }
@@ -2095,14 +2474,19 @@ function formatAction(action) {
2095
2474
  return map[action] || action;
2096
2475
  }
2097
2476
  async function generateClaudeCode(config, hubDir) {
2098
- const claudeDir = join3(hubDir, ".claude");
2099
- await mkdir3(join3(claudeDir, "agents"), { recursive: true });
2477
+ const claudeDir = join4(hubDir, ".claude");
2478
+ await mkdir4(join4(claudeDir, "agents"), { recursive: true });
2100
2479
  const orchestratorRule = buildOrchestratorRule(config);
2101
2480
  const cleanedOrchestrator = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
2102
2481
  const skillsSectionClaude = await buildSkillsSection(hubDir, config);
2103
- const agentsMdClaude = skillsSectionClaude ? cleanedOrchestrator + "\n" + skillsSectionClaude : cleanedOrchestrator;
2104
- await writeFile3(join3(hubDir, "AGENTS.md"), agentsMdClaude + "\n", "utf-8");
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");
2105
2486
  console.log(chalk3.green(" Generated AGENTS.md"));
2487
+ if (personaClaude) {
2488
+ console.log(chalk3.green(` Applied persona: ${personaClaude.name} (${personaClaude.role})`));
2489
+ }
2106
2490
  const claudeMdSections = [];
2107
2491
  claudeMdSections.push(cleanedOrchestrator);
2108
2492
  const agentsDir = resolve2(hubDir, "agents");
@@ -2110,7 +2494,7 @@ async function generateClaudeCode(config, hubDir) {
2110
2494
  const agentFiles = await readdir2(agentsDir);
2111
2495
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
2112
2496
  for (const file of mdFiles) {
2113
- await copyFile(join3(agentsDir, file), join3(claudeDir, "agents", file));
2497
+ await copyFile(join4(agentsDir, file), join4(claudeDir, "agents", file));
2114
2498
  }
2115
2499
  console.log(chalk3.green(` Copied ${mdFiles.length} agents to .claude/agents/`));
2116
2500
  } catch {
@@ -2119,15 +2503,15 @@ async function generateClaudeCode(config, hubDir) {
2119
2503
  const skillsDir = resolve2(hubDir, "skills");
2120
2504
  try {
2121
2505
  const skillFolders = await readdir2(skillsDir);
2122
- const claudeSkillsDir = join3(claudeDir, "skills");
2123
- await mkdir3(claudeSkillsDir, { recursive: true });
2506
+ const claudeSkillsDir = join4(claudeDir, "skills");
2507
+ await mkdir4(claudeSkillsDir, { recursive: true });
2124
2508
  let count = 0;
2125
2509
  for (const folder of skillFolders) {
2126
- const skillFile = join3(skillsDir, folder, "SKILL.md");
2510
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
2127
2511
  try {
2128
- await readFile3(skillFile);
2129
- const srcDir = join3(skillsDir, folder);
2130
- const targetDir = join3(claudeSkillsDir, folder);
2512
+ await readFile4(skillFile);
2513
+ const srcDir = join4(skillsDir, folder);
2514
+ const targetDir = join4(claudeSkillsDir, folder);
2131
2515
  await cp(srcDir, targetDir, { recursive: true });
2132
2516
  count++;
2133
2517
  } catch {
@@ -2138,16 +2522,16 @@ async function generateClaudeCode(config, hubDir) {
2138
2522
  }
2139
2523
  } catch {
2140
2524
  }
2141
- const claudeSkillsDirForDocs = join3(claudeDir, "skills");
2142
- await mkdir3(claudeSkillsDirForDocs, { recursive: true });
2525
+ const claudeSkillsDirForDocs = join4(claudeDir, "skills");
2526
+ await mkdir4(claudeSkillsDirForDocs, { recursive: true });
2143
2527
  await fetchHubDocsSkill(claudeSkillsDirForDocs);
2144
- await syncRemoteSources(config, hubDir, join3(claudeDir, "skills"), join3(claudeDir, "steering"));
2528
+ await syncRemoteSources(config, hubDir, join4(claudeDir, "skills"), join4(claudeDir, "steering"));
2145
2529
  const hubSteeringDirClaude = resolve2(hubDir, "steering");
2146
2530
  try {
2147
2531
  const steeringFiles = await readdir2(hubSteeringDirClaude);
2148
2532
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
2149
2533
  for (const file of mdFiles) {
2150
- const raw = await readFile3(join3(hubSteeringDirClaude, file), "utf-8");
2534
+ const raw = await readFile4(join4(hubSteeringDirClaude, file), "utf-8");
2151
2535
  const content = stripFrontMatter(raw).trim();
2152
2536
  if (content) {
2153
2537
  claudeMdSections.push(content);
@@ -2158,7 +2542,7 @@ async function generateClaudeCode(config, hubDir) {
2158
2542
  }
2159
2543
  } catch {
2160
2544
  }
2161
- 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");
2162
2546
  console.log(chalk3.green(" Generated CLAUDE.md"));
2163
2547
  if (config.mcps?.length) {
2164
2548
  const mcpJson = {};
@@ -2171,8 +2555,8 @@ async function generateClaudeCode(config, hubDir) {
2171
2555
  mcpJson[mcp.name] = buildClaudeCodeMcpEntry(mcp);
2172
2556
  }
2173
2557
  }
2174
- await writeFile3(
2175
- join3(hubDir, ".mcp.json"),
2558
+ await writeFile4(
2559
+ join4(hubDir, ".mcp.json"),
2176
2560
  JSON.stringify({ mcpServers: mcpJson }, null, 2) + "\n",
2177
2561
  "utf-8"
2178
2562
  );
@@ -2214,22 +2598,22 @@ async function generateClaudeCode(config, hubDir) {
2214
2598
  claudeSettings.hooks = claudeHooks;
2215
2599
  }
2216
2600
  }
2217
- await writeFile3(
2218
- join3(claudeDir, "settings.json"),
2601
+ await writeFile4(
2602
+ join4(claudeDir, "settings.json"),
2219
2603
  JSON.stringify(claudeSettings, null, 2) + "\n",
2220
2604
  "utf-8"
2221
2605
  );
2222
2606
  console.log(chalk3.green(" Generated .claude/settings.json"));
2223
2607
  const gitignoreLines = buildGitignoreLines(config);
2224
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
2608
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
2225
2609
  console.log(chalk3.green(" Generated .gitignore"));
2226
2610
  }
2227
2611
  async function generateKiro(config, hubDir) {
2228
- const kiroDir = join3(hubDir, ".kiro");
2229
- const steeringDir = join3(kiroDir, "steering");
2230
- const settingsDir = join3(kiroDir, "settings");
2231
- await mkdir3(steeringDir, { recursive: true });
2232
- 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 });
2233
2617
  let mode = await getKiroMode(hubDir);
2234
2618
  if (!mode) {
2235
2619
  const { kiroMode } = await inquirer.prompt([
@@ -2250,25 +2634,32 @@ async function generateKiro(config, hubDir) {
2250
2634
  console.log(chalk3.dim(` Using saved Kiro mode: ${mode}`));
2251
2635
  }
2252
2636
  const gitignoreLines = buildGitignoreLines(config);
2253
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
2637
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
2254
2638
  console.log(chalk3.green(" Generated .gitignore"));
2255
2639
  const kiroRule = buildKiroOrchestratorRule(config);
2256
2640
  const skillsSection = await buildSkillsSection(hubDir, config);
2257
- const kiroRuleWithSkills = skillsSection ? kiroRule + "\n" + skillsSection : kiroRule;
2258
- await writeFile3(join3(hubDir, "AGENTS.md"), kiroRuleWithSkills + "\n", "utf-8");
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");
2259
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
+ });
2260
2651
  const hubSteeringDir = resolve2(hubDir, "steering");
2261
2652
  try {
2262
2653
  const steeringFiles = await readdir2(hubSteeringDir);
2263
2654
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
2264
2655
  for (const file of mdFiles) {
2265
- const raw = await readFile3(join3(hubSteeringDir, file), "utf-8");
2656
+ const raw = await readFile4(join4(hubSteeringDir, file), "utf-8");
2266
2657
  const content = stripFrontMatter(raw);
2267
- const destPath = join3(steeringDir, file);
2658
+ const destPath = join4(steeringDir, file);
2268
2659
  let inclusion = "always";
2269
2660
  let meta;
2270
- if (existsSync2(destPath)) {
2271
- const existingContent = await readFile3(destPath, "utf-8");
2661
+ if (existsSync3(destPath)) {
2662
+ const existingContent = await readFile4(destPath, "utf-8");
2272
2663
  const existingFm = parseFrontMatter(existingContent);
2273
2664
  if (existingFm) {
2274
2665
  if (existingFm.inclusion === "auto" || existingFm.inclusion === "manual" || existingFm.inclusion === "fileMatch") {
@@ -2293,7 +2684,7 @@ async function generateKiro(config, hubDir) {
2293
2684
  }
2294
2685
  }
2295
2686
  const kiroSteering = buildKiroSteeringContent(content, inclusion, meta);
2296
- await writeFile3(destPath, kiroSteering, "utf-8");
2687
+ await writeFile4(destPath, kiroSteering, "utf-8");
2297
2688
  }
2298
2689
  if (mdFiles.length > 0) {
2299
2690
  console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
@@ -2302,14 +2693,17 @@ async function generateKiro(config, hubDir) {
2302
2693
  }
2303
2694
  const agentsDir = resolve2(hubDir, "agents");
2304
2695
  try {
2305
- const kiroAgentsDir = join3(kiroDir, "agents");
2306
- await mkdir3(kiroAgentsDir, { recursive: true });
2696
+ const kiroAgentsDir = join4(kiroDir, "agents");
2697
+ await mkdir4(kiroAgentsDir, { recursive: true });
2307
2698
  const agentFiles = await readdir2(agentsDir);
2308
2699
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
2309
2700
  for (const file of mdFiles) {
2310
- const agentContent = await readFile3(join3(agentsDir, file), "utf-8");
2311
- const kiroAgent = buildKiroAgentContent(agentContent);
2312
- 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");
2313
2707
  }
2314
2708
  console.log(chalk3.green(` Copied ${mdFiles.length} agents to .kiro/agents/`));
2315
2709
  } catch {
@@ -2318,15 +2712,15 @@ async function generateKiro(config, hubDir) {
2318
2712
  const skillsDir = resolve2(hubDir, "skills");
2319
2713
  try {
2320
2714
  const skillFolders = await readdir2(skillsDir);
2321
- const kiroSkillsDir = join3(kiroDir, "skills");
2322
- await mkdir3(kiroSkillsDir, { recursive: true });
2715
+ const kiroSkillsDir = join4(kiroDir, "skills");
2716
+ await mkdir4(kiroSkillsDir, { recursive: true });
2323
2717
  let count = 0;
2324
2718
  for (const folder of skillFolders) {
2325
- const skillFile = join3(skillsDir, folder, "SKILL.md");
2719
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
2326
2720
  try {
2327
- await readFile3(skillFile);
2328
- const srcDir = join3(skillsDir, folder);
2329
- const targetDir = join3(kiroSkillsDir, folder);
2721
+ await readFile4(skillFile);
2722
+ const srcDir = join4(skillsDir, folder);
2723
+ const targetDir = join4(kiroSkillsDir, folder);
2330
2724
  await cp(srcDir, targetDir, { recursive: true });
2331
2725
  count++;
2332
2726
  } catch {
@@ -2337,10 +2731,10 @@ async function generateKiro(config, hubDir) {
2337
2731
  }
2338
2732
  } catch {
2339
2733
  }
2340
- const kiroSkillsDirForDocs = join3(kiroDir, "skills");
2341
- await mkdir3(kiroSkillsDirForDocs, { recursive: true });
2734
+ const kiroSkillsDirForDocs = join4(kiroDir, "skills");
2735
+ await mkdir4(kiroSkillsDirForDocs, { recursive: true });
2342
2736
  await fetchHubDocsSkill(kiroSkillsDirForDocs);
2343
- await syncRemoteSources(config, hubDir, join3(kiroDir, "skills"), steeringDir);
2737
+ await syncRemoteSources(config, hubDir, join4(kiroDir, "skills"), steeringDir);
2344
2738
  if (config.mcps?.length) {
2345
2739
  const mcpConfig = {};
2346
2740
  const upstreamSet = getUpstreamNames(config.mcps);
@@ -2353,10 +2747,14 @@ async function generateKiro(config, hubDir) {
2353
2747
  mcpConfig[mcp.name] = buildKiroMcpEntry(mcp, mode);
2354
2748
  }
2355
2749
  }
2356
- 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");
2357
2755
  const disabledState = await readExistingMcpDisabledState(mcpJsonPath);
2358
2756
  applyDisabledState(mcpConfig, disabledState);
2359
- await writeFile3(
2757
+ await writeFile4(
2360
2758
  mcpJsonPath,
2361
2759
  JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
2362
2760
  "utf-8"
@@ -2383,13 +2781,13 @@ async function generateKiro(config, hubDir) {
2383
2781
  await generateVSCodeSettings(config, hubDir);
2384
2782
  }
2385
2783
  async function generateVSCodeSettings(config, hubDir) {
2386
- const vscodeDir = join3(hubDir, ".vscode");
2387
- await mkdir3(vscodeDir, { recursive: true });
2388
- 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");
2389
2787
  let existing = {};
2390
- if (existsSync2(settingsPath)) {
2788
+ if (existsSync3(settingsPath)) {
2391
2789
  try {
2392
- const raw = await readFile3(settingsPath, "utf-8");
2790
+ const raw = await readFile4(settingsPath, "utf-8");
2393
2791
  existing = JSON.parse(raw);
2394
2792
  } catch {
2395
2793
  existing = {};
@@ -2401,14 +2799,14 @@ async function generateVSCodeSettings(config, hubDir) {
2401
2799
  "git.detectSubmodulesLimit": Math.max(config.repos.length * 2, 20)
2402
2800
  };
2403
2801
  const merged = { ...existing, ...managed };
2404
- await writeFile3(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
2802
+ await writeFile4(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
2405
2803
  console.log(chalk3.green(" Generated .vscode/settings.json (git multi-repo detection)"));
2406
2804
  const workspaceFile = `${config.name}.code-workspace`;
2407
- const workspacePath = join3(hubDir, workspaceFile);
2805
+ const workspacePath = join4(hubDir, workspaceFile);
2408
2806
  let existingWorkspace = {};
2409
- if (existsSync2(workspacePath)) {
2807
+ if (existsSync3(workspacePath)) {
2410
2808
  try {
2411
- const raw = await readFile3(workspacePath, "utf-8");
2809
+ const raw = await readFile4(workspacePath, "utf-8");
2412
2810
  existingWorkspace = JSON.parse(raw);
2413
2811
  } catch {
2414
2812
  existingWorkspace = {};
@@ -2418,7 +2816,7 @@ async function generateVSCodeSettings(config, hubDir) {
2418
2816
  const existing2 = files.find((f) => f.endsWith(".code-workspace"));
2419
2817
  if (existing2) {
2420
2818
  try {
2421
- const raw = await readFile3(join3(hubDir, existing2), "utf-8");
2819
+ const raw = await readFile4(join4(hubDir, existing2), "utf-8");
2422
2820
  existingWorkspace = JSON.parse(raw);
2423
2821
  } catch {
2424
2822
  existingWorkspace = {};
@@ -2454,7 +2852,7 @@ async function generateVSCodeSettings(config, hubDir) {
2454
2852
  folders,
2455
2853
  settings: existingWorkspace.settings || {}
2456
2854
  };
2457
- await writeFile3(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
2855
+ await writeFile4(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
2458
2856
  console.log(chalk3.green(` Generated ${workspaceFile}`));
2459
2857
  }
2460
2858
  function extractEnvVarsByMcp(mcps) {
@@ -2501,7 +2899,7 @@ async function generateEnvExample(config, hubDir) {
2501
2899
  totalVars++;
2502
2900
  }
2503
2901
  if (totalVars === 0) return;
2504
- 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");
2505
2903
  console.log(chalk3.green(` Generated .env.example (${totalVars} vars)`));
2506
2904
  }
2507
2905
  function buildGitignoreLines(config) {
@@ -2591,7 +2989,7 @@ async function resolveEditor(opts) {
2591
2989
  ]);
2592
2990
  return editor;
2593
2991
  }
2594
- 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) => {
2595
2993
  const hubDir = process.cwd();
2596
2994
  if (opts.check) {
2597
2995
  const result = await checkOutdated(hubDir);
@@ -2656,6 +3054,10 @@ Generating ${generator.name} configuration
2656
3054
  });
2657
3055
 
2658
3056
  export {
3057
+ colors,
3058
+ symbols,
3059
+ horizontalLine,
3060
+ personaCommand,
2659
3061
  generators,
2660
3062
  generateCommand,
2661
3063
  checkAndAutoRegenerate