@arvoretech/hub 0.16.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-6CIWB5FN.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) {
@@ -403,6 +734,13 @@ async function syncRemoteSources(config, hubDir, skillsDir, steeringDir) {
403
734
  console.log(chalk3.yellow(` ${result.errors.length} remote source(s) failed`));
404
735
  }
405
736
  }
737
+ function getRemoteSkillNames(config) {
738
+ const names = /* @__PURE__ */ new Set();
739
+ for (const source of config.remote_sources ?? []) {
740
+ if (source.type === "skill") names.add(source.name);
741
+ }
742
+ return names;
743
+ }
406
744
  function buildDesignSection(config) {
407
745
  const design = config.design;
408
746
  if (!design) return null;
@@ -464,9 +802,9 @@ function parseFrontMatter(content) {
464
802
  }
465
803
  async function readExistingMcpDisabledState(mcpJsonPath) {
466
804
  const disabledState = {};
467
- if (!existsSync2(mcpJsonPath)) return disabledState;
805
+ if (!existsSync3(mcpJsonPath)) return disabledState;
468
806
  try {
469
- const content = JSON.parse(await readFile3(mcpJsonPath, "utf-8"));
807
+ const content = JSON.parse(await readFile4(mcpJsonPath, "utf-8"));
470
808
  const servers = content.mcpServers || content.mcp || {};
471
809
  for (const [name, config] of Object.entries(servers)) {
472
810
  if (typeof config.disabled === "boolean") {
@@ -492,8 +830,8 @@ async function fetchHubDocsSkill(skillsDir) {
492
830
  return;
493
831
  }
494
832
  const content = await res.text();
495
- const hubSkillDir = join3(skillsDir, "hub-docs");
496
- await mkdir3(hubSkillDir, { recursive: true });
833
+ const hubSkillDir = join4(skillsDir, "hub-docs");
834
+ await mkdir4(hubSkillDir, { recursive: true });
497
835
  const skillContent = `---
498
836
  name: hub-docs
499
837
  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 +839,7 @@ triggers: [hub, rhm, hub.yaml, generate, scan, setup, orchestrator, multi-repo,
501
839
  ---
502
840
 
503
841
  ${content}`;
504
- await writeFile3(join3(hubSkillDir, "SKILL.md"), skillContent, "utf-8");
842
+ await writeFile4(join4(hubSkillDir, "SKILL.md"), skillContent, "utf-8");
505
843
  console.log(chalk3.green(" Fetched hub-docs skill from hub.arvore.com.br"));
506
844
  } catch {
507
845
  console.log(chalk3.yellow(` Could not fetch hub docs, skipping hub-docs skill`));
@@ -576,7 +914,7 @@ function buildClaudeHooks(hooks) {
576
914
  return claudeHooks;
577
915
  }
578
916
  async function generateEditorCommands(config, hubDir, targetDir, editorName) {
579
- const commandsDir = join3(targetDir, "commands");
917
+ const commandsDir = join4(targetDir, "commands");
580
918
  let count = 0;
581
919
  if (config.commands_dir) {
582
920
  const srcDir = resolve2(hubDir, config.commands_dir);
@@ -584,9 +922,9 @@ async function generateEditorCommands(config, hubDir, targetDir, editorName) {
584
922
  const files = await readdir2(srcDir);
585
923
  const mdFiles = files.filter((f) => f.endsWith(".md"));
586
924
  if (mdFiles.length > 0) {
587
- await mkdir3(commandsDir, { recursive: true });
925
+ await mkdir4(commandsDir, { recursive: true });
588
926
  for (const file of mdFiles) {
589
- await copyFile(join3(srcDir, file), join3(commandsDir, file));
927
+ await copyFile(join4(srcDir, file), join4(commandsDir, file));
590
928
  count++;
591
929
  }
592
930
  }
@@ -595,10 +933,10 @@ async function generateEditorCommands(config, hubDir, targetDir, editorName) {
595
933
  }
596
934
  }
597
935
  if (config.commands) {
598
- await mkdir3(commandsDir, { recursive: true });
936
+ await mkdir4(commandsDir, { recursive: true });
599
937
  for (const [name, filePath] of Object.entries(config.commands)) {
600
938
  const src = resolve2(hubDir, filePath);
601
- const dest = join3(commandsDir, name.endsWith(".md") ? name : `${name}.md`);
939
+ const dest = join4(commandsDir, name.endsWith(".md") ? name : `${name}.md`);
602
940
  try {
603
941
  await copyFile(src, dest);
604
942
  count++;
@@ -613,27 +951,27 @@ async function generateEditorCommands(config, hubDir, targetDir, editorName) {
613
951
  }
614
952
  async function writeManagedFile(filePath, managedLines) {
615
953
  const managedBlock = [HUB_MARKER_START, ...managedLines, HUB_MARKER_END].join("\n");
616
- if (existsSync2(filePath)) {
617
- const existing = await readFile3(filePath, "utf-8");
954
+ if (existsSync3(filePath)) {
955
+ const existing = await readFile4(filePath, "utf-8");
618
956
  const startIdx = existing.indexOf(HUB_MARKER_START);
619
957
  const endIdx = existing.indexOf(HUB_MARKER_END);
620
958
  if (startIdx !== -1 && endIdx !== -1) {
621
959
  const before = existing.substring(0, startIdx);
622
960
  const after = existing.substring(endIdx + HUB_MARKER_END.length);
623
- await writeFile3(filePath, before + managedBlock + after, "utf-8");
961
+ await writeFile4(filePath, before + managedBlock + after, "utf-8");
624
962
  return;
625
963
  }
626
- await writeFile3(filePath, managedBlock + "\n\n" + existing, "utf-8");
964
+ await writeFile4(filePath, managedBlock + "\n\n" + existing, "utf-8");
627
965
  return;
628
966
  }
629
- await writeFile3(filePath, managedBlock + "\n", "utf-8");
967
+ await writeFile4(filePath, managedBlock + "\n", "utf-8");
630
968
  }
631
969
  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 });
970
+ const cursorDir = join4(hubDir, ".cursor");
971
+ await mkdir4(join4(cursorDir, "rules"), { recursive: true });
972
+ await mkdir4(join4(cursorDir, "agents"), { recursive: true });
635
973
  const gitignoreLines = buildGitignoreLines(config);
636
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
974
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
637
975
  console.log(chalk3.green(" Generated .gitignore"));
638
976
  const cursorignoreLines = [
639
977
  "# Re-include repositories for AI context"
@@ -643,7 +981,7 @@ async function generateCursor(config, hubDir) {
643
981
  cursorignoreLines.push(`!${repoDir}/`);
644
982
  }
645
983
  cursorignoreLines.push("", "# Re-include tasks for agent collaboration", "!tasks/");
646
- await writeManagedFile(join3(hubDir, ".cursorignore"), cursorignoreLines);
984
+ await writeManagedFile(join4(hubDir, ".cursorignore"), cursorignoreLines);
647
985
  console.log(chalk3.green(" Generated .cursorignore"));
648
986
  if (config.mcps?.length) {
649
987
  const mcpConfig = {};
@@ -656,27 +994,36 @@ async function generateCursor(config, hubDir) {
656
994
  mcpConfig[mcp.name] = buildCursorMcpEntry(mcp);
657
995
  }
658
996
  }
659
- await writeFile3(
660
- join3(cursorDir, "mcp.json"),
997
+ const sandbox = getSandboxService(config);
998
+ if (sandbox && !mcpConfig["sandbox"]) {
999
+ mcpConfig["sandbox"] = buildSandboxMcpEntry(sandbox.port);
1000
+ }
1001
+ await writeFile4(
1002
+ join4(cursorDir, "mcp.json"),
661
1003
  JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
662
1004
  "utf-8"
663
1005
  );
664
1006
  console.log(chalk3.green(" Generated .cursor/mcp.json"));
665
1007
  }
666
1008
  const orchestratorRule = buildOrchestratorRule(config);
667
- await writeFile3(join3(cursorDir, "rules", "orchestrator.mdc"), orchestratorRule, "utf-8");
1009
+ await writeFile4(join4(cursorDir, "rules", "orchestrator.mdc"), orchestratorRule, "utf-8");
668
1010
  console.log(chalk3.green(" Generated .cursor/rules/orchestrator.mdc"));
669
1011
  const cleanedOrchestratorForAgents = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
670
1012
  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");
1013
+ const personaCursor = await loadPersona(hubDir);
1014
+ const personaSectionCursor = personaCursor ? buildPersonaSection(personaCursor) : "";
1015
+ const agentsMdCursor = [cleanedOrchestratorForAgents, skillsSectionCursor, personaSectionCursor].filter(Boolean).join("\n");
1016
+ await writeFile4(join4(hubDir, "AGENTS.md"), agentsMdCursor + "\n", "utf-8");
673
1017
  console.log(chalk3.green(" Generated AGENTS.md"));
1018
+ if (personaCursor) {
1019
+ console.log(chalk3.green(` Applied persona: ${personaCursor.name} (${personaCursor.role})`));
1020
+ }
674
1021
  const hubSteeringDirCursor = resolve2(hubDir, "steering");
675
1022
  try {
676
1023
  const steeringFiles = await readdir2(hubSteeringDirCursor);
677
1024
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
678
1025
  for (const file of mdFiles) {
679
- const raw = await readFile3(join3(hubSteeringDirCursor, file), "utf-8");
1026
+ const raw = await readFile4(join4(hubSteeringDirCursor, file), "utf-8");
680
1027
  const content = stripFrontMatter(raw);
681
1028
  const mdcName = file.replace(/\.md$/, ".mdc");
682
1029
  const mdcContent = `---
@@ -685,7 +1032,7 @@ alwaysApply: true
685
1032
  ---
686
1033
 
687
1034
  ${content}`;
688
- await writeFile3(join3(cursorDir, "rules", mdcName), mdcContent, "utf-8");
1035
+ await writeFile4(join4(cursorDir, "rules", mdcName), mdcContent, "utf-8");
689
1036
  }
690
1037
  if (mdFiles.length > 0) {
691
1038
  console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .cursor/rules/`));
@@ -696,25 +1043,35 @@ ${content}`;
696
1043
  try {
697
1044
  const agentFiles = await readdir2(agentsDir);
698
1045
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1046
+ const sandboxSvc = getSandboxService(config);
699
1047
  for (const file of mdFiles) {
700
- await copyFile(join3(agentsDir, file), join3(cursorDir, "agents", file));
1048
+ if (sandboxSvc) {
1049
+ const agentName = file.replace(/\.md$/, "");
1050
+ const agentContent = await readFile4(join4(agentsDir, file), "utf-8");
1051
+ const withSandbox = injectSandboxContext(agentName, agentContent, sandboxSvc.port);
1052
+ await writeFile4(join4(cursorDir, "agents", file), withSandbox, "utf-8");
1053
+ } else {
1054
+ await copyFile(join4(agentsDir, file), join4(cursorDir, "agents", file));
1055
+ }
701
1056
  }
702
1057
  console.log(chalk3.green(` Copied ${mdFiles.length} agent definitions`));
703
1058
  } catch {
704
1059
  console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
705
1060
  }
706
1061
  const skillsDir = resolve2(hubDir, "skills");
1062
+ const remoteSkillsCursor = getRemoteSkillNames(config);
707
1063
  try {
708
1064
  const skillFolders = await readdir2(skillsDir);
709
- const cursorSkillsDir = join3(cursorDir, "skills");
710
- await mkdir3(cursorSkillsDir, { recursive: true });
1065
+ const cursorSkillsDir = join4(cursorDir, "skills");
1066
+ await mkdir4(cursorSkillsDir, { recursive: true });
711
1067
  let count = 0;
712
1068
  for (const folder of skillFolders) {
713
- const skillFile = join3(skillsDir, folder, "SKILL.md");
1069
+ if (remoteSkillsCursor.has(folder)) continue;
1070
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
714
1071
  try {
715
- await readFile3(skillFile);
716
- const srcDir = join3(skillsDir, folder);
717
- const targetDir = join3(cursorSkillsDir, folder);
1072
+ await readFile4(skillFile);
1073
+ const srcDir = join4(skillsDir, folder);
1074
+ const targetDir = join4(cursorSkillsDir, folder);
718
1075
  await cp(srcDir, targetDir, { recursive: true });
719
1076
  count++;
720
1077
  } catch {
@@ -725,15 +1082,15 @@ ${content}`;
725
1082
  }
726
1083
  } catch {
727
1084
  }
728
- const cursorSkillsDirForDocs = join3(cursorDir, "skills");
729
- await mkdir3(cursorSkillsDirForDocs, { recursive: true });
1085
+ const cursorSkillsDirForDocs = join4(cursorDir, "skills");
1086
+ await mkdir4(cursorSkillsDirForDocs, { recursive: true });
730
1087
  await fetchHubDocsSkill(cursorSkillsDirForDocs);
731
- await syncRemoteSources(config, hubDir, join3(cursorDir, "skills"), join3(cursorDir, "rules"));
1088
+ await syncRemoteSources(config, hubDir, join4(cursorDir, "skills"), join4(cursorDir, "rules"));
732
1089
  if (config.hooks) {
733
1090
  const cursorHooks = buildCursorHooks(config.hooks);
734
1091
  if (cursorHooks) {
735
- await writeFile3(
736
- join3(cursorDir, "hooks.json"),
1092
+ await writeFile4(
1093
+ join4(cursorDir, "hooks.json"),
737
1094
  JSON.stringify(cursorHooks, null, 2) + "\n",
738
1095
  "utf-8"
739
1096
  );
@@ -743,6 +1100,14 @@ ${content}`;
743
1100
  await generateEditorCommands(config, hubDir, cursorDir, ".cursor/commands/");
744
1101
  await generateVSCodeSettings(config, hubDir);
745
1102
  }
1103
+ function buildSandboxMcpEntry(port) {
1104
+ return { url: `http://localhost:${port}/mcp` };
1105
+ }
1106
+ function getSandboxService(config) {
1107
+ const svc = config.services?.find((s) => s.type === "sandbox");
1108
+ if (!svc) return null;
1109
+ return { port: svc.port ?? 8080 };
1110
+ }
746
1111
  function buildProxyUpstreams(proxyMcp, allMcps) {
747
1112
  const upstreamNames = new Set(proxyMcp.upstreams || []);
748
1113
  const upstreamEntries = [];
@@ -876,6 +1241,24 @@ function buildKiroMcpEntry(mcp, mode = "editor") {
876
1241
  ...autoApprove && { autoApprove }
877
1242
  };
878
1243
  }
1244
+ var SANDBOX_AGENT_TARGETS = /* @__PURE__ */ new Set(["qa-frontend", "qa-backend", "coding-frontend", "coding-backend"]);
1245
+ function injectSandboxContext(agentName, content, sandboxPort) {
1246
+ if (!SANDBOX_AGENT_TARGETS.has(agentName)) return content;
1247
+ const section = `
1248
+ ## Sandbox Environment
1249
+
1250
+ A sandboxed execution environment is available via the \`sandbox\` MCP (http://localhost:${sandboxPort}/mcp).
1251
+
1252
+ Use it to:
1253
+ - Run shell commands: \`shell.exec\`
1254
+ - Read/write files: \`file.read\`, \`file.write\`
1255
+ - Control a real browser: \`browser.navigate\`, \`browser.screenshot\`, \`browser.click\`
1256
+ - Execute code: \`jupyter.execute\`
1257
+
1258
+ 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.
1259
+ `;
1260
+ return content.trimEnd() + "\n" + section;
1261
+ }
879
1262
  function buildKiroAgentContent(rawContent) {
880
1263
  const fmMatch = rawContent.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
881
1264
  if (!fmMatch) {
@@ -1179,9 +1562,9 @@ async function buildSkillsSection(hubDir, config) {
1179
1562
  try {
1180
1563
  const folders = await readdir2(skillsDir);
1181
1564
  for (const folder of folders) {
1182
- const skillPath = join3(skillsDir, folder, "SKILL.md");
1565
+ const skillPath = join4(skillsDir, folder, "SKILL.md");
1183
1566
  try {
1184
- const content = await readFile3(skillPath, "utf-8");
1567
+ const content = await readFile4(skillPath, "utf-8");
1185
1568
  const fm = parseFrontMatter(content);
1186
1569
  if (fm?.name) {
1187
1570
  skillEntries.push({
@@ -1504,18 +1887,18 @@ If any validation agent leaves comments requiring fixes, call the relevant codin
1504
1887
  return parts.join("\n");
1505
1888
  }
1506
1889
  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 });
1890
+ const opencodeDir = join4(hubDir, ".opencode");
1891
+ await mkdir4(join4(opencodeDir, "agents"), { recursive: true });
1892
+ await mkdir4(join4(opencodeDir, "rules"), { recursive: true });
1893
+ await mkdir4(join4(opencodeDir, "skills"), { recursive: true });
1894
+ await mkdir4(join4(opencodeDir, "commands"), { recursive: true });
1895
+ await mkdir4(join4(opencodeDir, "plugins"), { recursive: true });
1513
1896
  const gitignoreLines = buildGitignoreLines(config);
1514
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
1897
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
1515
1898
  console.log(chalk3.green(" Generated .gitignore"));
1516
1899
  if (config.repos.length > 0) {
1517
1900
  const ignoreContent = config.repos.map((r) => `!${r.name}`).join("\n") + "\n";
1518
- await writeFile3(join3(hubDir, ".ignore"), ignoreContent, "utf-8");
1901
+ await writeFile4(join4(hubDir, ".ignore"), ignoreContent, "utf-8");
1519
1902
  console.log(chalk3.green(" Generated .ignore"));
1520
1903
  }
1521
1904
  const orchestratorContent = buildOpenCodeOrchestratorRule(config);
@@ -1523,22 +1906,27 @@ async function generateOpenCode(config, hubDir) {
1523
1906
  "Development orchestrator. Delegates specialized work to subagents following a structured pipeline: refinement, coding, review, QA, and delivery.",
1524
1907
  orchestratorContent
1525
1908
  );
1526
- await writeFile3(join3(opencodeDir, "agents", "orchestrator.md"), orchestratorAgent, "utf-8");
1909
+ await writeFile4(join4(opencodeDir, "agents", "orchestrator.md"), orchestratorAgent, "utf-8");
1527
1910
  console.log(chalk3.green(" Generated .opencode/agents/orchestrator.md (primary agent)"));
1528
- await rm(join3(opencodeDir, "rules", "orchestrator.md")).catch(() => {
1911
+ await rm(join4(opencodeDir, "rules", "orchestrator.md")).catch(() => {
1529
1912
  });
1530
1913
  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");
1914
+ const personaOC = await loadPersona(hubDir);
1915
+ const personaSectionOC = personaOC ? buildPersonaSection(personaOC) : "";
1916
+ const agentsMdOC = [orchestratorContent, skillsSectionOC, personaSectionOC].filter(Boolean).join("\n");
1917
+ await writeFile4(join4(hubDir, "AGENTS.md"), agentsMdOC + "\n", "utf-8");
1533
1918
  console.log(chalk3.green(" Generated AGENTS.md"));
1919
+ if (personaOC) {
1920
+ console.log(chalk3.green(` Applied persona: ${personaOC.name} (${personaOC.role})`));
1921
+ }
1534
1922
  const hubSteeringDirOC = resolve2(hubDir, "steering");
1535
1923
  try {
1536
1924
  const steeringFiles = await readdir2(hubSteeringDirOC);
1537
1925
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
1538
1926
  for (const file of mdFiles) {
1539
- const raw = await readFile3(join3(hubSteeringDirOC, file), "utf-8");
1927
+ const raw = await readFile4(join4(hubSteeringDirOC, file), "utf-8");
1540
1928
  const content = stripFrontMatter(raw);
1541
- await writeFile3(join3(opencodeDir, "rules", file), content, "utf-8");
1929
+ await writeFile4(join4(opencodeDir, "rules", file), content, "utf-8");
1542
1930
  }
1543
1931
  if (mdFiles.length > 0) {
1544
1932
  console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .opencode/rules/`));
@@ -1563,8 +1951,8 @@ async function generateOpenCode(config, hubDir) {
1563
1951
  opencodeConfig.mcp = mcpConfig;
1564
1952
  }
1565
1953
  opencodeConfig.instructions = [".opencode/rules/*.md"];
1566
- await writeFile3(
1567
- join3(hubDir, "opencode.json"),
1954
+ await writeFile4(
1955
+ join4(hubDir, "opencode.json"),
1568
1956
  JSON.stringify(opencodeConfig, null, 2) + "\n",
1569
1957
  "utf-8"
1570
1958
  );
@@ -1575,24 +1963,26 @@ async function generateOpenCode(config, hubDir) {
1575
1963
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1576
1964
  for (const file of mdFiles) {
1577
1965
  if (file === "orchestrator.md") continue;
1578
- const content = await readFile3(join3(agentsDir, file), "utf-8");
1966
+ const content = await readFile4(join4(agentsDir, file), "utf-8");
1579
1967
  const agentName = file.replace(/\.md$/, "");
1580
1968
  const converted = buildOpenCodeAgentMarkdown(agentName, content);
1581
- await writeFile3(join3(opencodeDir, "agents", file), converted, "utf-8");
1969
+ await writeFile4(join4(opencodeDir, "agents", file), converted, "utf-8");
1582
1970
  }
1583
1971
  console.log(chalk3.green(` Copied ${mdFiles.length} agents to .opencode/agents/`));
1584
1972
  } catch {
1585
1973
  console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
1586
1974
  }
1587
1975
  const skillsDir = resolve2(hubDir, "skills");
1976
+ const remoteSkillsOC = getRemoteSkillNames(config);
1588
1977
  try {
1589
1978
  const skillFolders = await readdir2(skillsDir);
1590
1979
  let count = 0;
1591
1980
  for (const folder of skillFolders) {
1592
- const skillFile = join3(skillsDir, folder, "SKILL.md");
1981
+ if (remoteSkillsOC.has(folder)) continue;
1982
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
1593
1983
  try {
1594
- await readFile3(skillFile);
1595
- await cp(join3(skillsDir, folder), join3(opencodeDir, "skills", folder), { recursive: true });
1984
+ await readFile4(skillFile);
1985
+ await cp(join4(skillsDir, folder), join4(opencodeDir, "skills", folder), { recursive: true });
1596
1986
  count++;
1597
1987
  } catch {
1598
1988
  }
@@ -1602,13 +1992,13 @@ async function generateOpenCode(config, hubDir) {
1602
1992
  }
1603
1993
  } catch {
1604
1994
  }
1605
- await fetchHubDocsSkill(join3(opencodeDir, "skills"));
1606
- await syncRemoteSources(config, hubDir, join3(opencodeDir, "skills"), join3(opencodeDir, "rules"));
1995
+ await fetchHubDocsSkill(join4(opencodeDir, "skills"));
1996
+ await syncRemoteSources(config, hubDir, join4(opencodeDir, "skills"), join4(opencodeDir, "rules"));
1607
1997
  await generateEditorCommands(config, hubDir, opencodeDir, ".opencode/commands/");
1608
1998
  if (config.hooks) {
1609
1999
  const plugin = buildOpenCodeHooksPlugin(config.hooks);
1610
2000
  if (plugin) {
1611
- await writeFile3(join3(opencodeDir, "plugins", "hub-hooks.js"), plugin, "utf-8");
2001
+ await writeFile4(join4(opencodeDir, "plugins", "hub-hooks.js"), plugin, "utf-8");
1612
2002
  console.log(chalk3.green(" Generated .opencode/plugins/hub-hooks.js"));
1613
2003
  }
1614
2004
  }
@@ -2095,14 +2485,19 @@ function formatAction(action) {
2095
2485
  return map[action] || action;
2096
2486
  }
2097
2487
  async function generateClaudeCode(config, hubDir) {
2098
- const claudeDir = join3(hubDir, ".claude");
2099
- await mkdir3(join3(claudeDir, "agents"), { recursive: true });
2488
+ const claudeDir = join4(hubDir, ".claude");
2489
+ await mkdir4(join4(claudeDir, "agents"), { recursive: true });
2100
2490
  const orchestratorRule = buildOrchestratorRule(config);
2101
2491
  const cleanedOrchestrator = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
2102
2492
  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");
2493
+ const personaClaude = await loadPersona(hubDir);
2494
+ const personaSectionClaude = personaClaude ? buildPersonaSection(personaClaude) : "";
2495
+ const agentsMdClaude = [cleanedOrchestrator, skillsSectionClaude, personaSectionClaude].filter(Boolean).join("\n");
2496
+ await writeFile4(join4(hubDir, "AGENTS.md"), agentsMdClaude + "\n", "utf-8");
2105
2497
  console.log(chalk3.green(" Generated AGENTS.md"));
2498
+ if (personaClaude) {
2499
+ console.log(chalk3.green(` Applied persona: ${personaClaude.name} (${personaClaude.role})`));
2500
+ }
2106
2501
  const claudeMdSections = [];
2107
2502
  claudeMdSections.push(cleanedOrchestrator);
2108
2503
  const agentsDir = resolve2(hubDir, "agents");
@@ -2110,24 +2505,26 @@ async function generateClaudeCode(config, hubDir) {
2110
2505
  const agentFiles = await readdir2(agentsDir);
2111
2506
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
2112
2507
  for (const file of mdFiles) {
2113
- await copyFile(join3(agentsDir, file), join3(claudeDir, "agents", file));
2508
+ await copyFile(join4(agentsDir, file), join4(claudeDir, "agents", file));
2114
2509
  }
2115
2510
  console.log(chalk3.green(` Copied ${mdFiles.length} agents to .claude/agents/`));
2116
2511
  } catch {
2117
2512
  console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
2118
2513
  }
2119
2514
  const skillsDir = resolve2(hubDir, "skills");
2515
+ const remoteSkillsClaude = getRemoteSkillNames(config);
2120
2516
  try {
2121
2517
  const skillFolders = await readdir2(skillsDir);
2122
- const claudeSkillsDir = join3(claudeDir, "skills");
2123
- await mkdir3(claudeSkillsDir, { recursive: true });
2518
+ const claudeSkillsDir = join4(claudeDir, "skills");
2519
+ await mkdir4(claudeSkillsDir, { recursive: true });
2124
2520
  let count = 0;
2125
2521
  for (const folder of skillFolders) {
2126
- const skillFile = join3(skillsDir, folder, "SKILL.md");
2522
+ if (remoteSkillsClaude.has(folder)) continue;
2523
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
2127
2524
  try {
2128
- await readFile3(skillFile);
2129
- const srcDir = join3(skillsDir, folder);
2130
- const targetDir = join3(claudeSkillsDir, folder);
2525
+ await readFile4(skillFile);
2526
+ const srcDir = join4(skillsDir, folder);
2527
+ const targetDir = join4(claudeSkillsDir, folder);
2131
2528
  await cp(srcDir, targetDir, { recursive: true });
2132
2529
  count++;
2133
2530
  } catch {
@@ -2138,16 +2535,16 @@ async function generateClaudeCode(config, hubDir) {
2138
2535
  }
2139
2536
  } catch {
2140
2537
  }
2141
- const claudeSkillsDirForDocs = join3(claudeDir, "skills");
2142
- await mkdir3(claudeSkillsDirForDocs, { recursive: true });
2538
+ const claudeSkillsDirForDocs = join4(claudeDir, "skills");
2539
+ await mkdir4(claudeSkillsDirForDocs, { recursive: true });
2143
2540
  await fetchHubDocsSkill(claudeSkillsDirForDocs);
2144
- await syncRemoteSources(config, hubDir, join3(claudeDir, "skills"), join3(claudeDir, "steering"));
2541
+ await syncRemoteSources(config, hubDir, join4(claudeDir, "skills"), join4(claudeDir, "steering"));
2145
2542
  const hubSteeringDirClaude = resolve2(hubDir, "steering");
2146
2543
  try {
2147
2544
  const steeringFiles = await readdir2(hubSteeringDirClaude);
2148
2545
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
2149
2546
  for (const file of mdFiles) {
2150
- const raw = await readFile3(join3(hubSteeringDirClaude, file), "utf-8");
2547
+ const raw = await readFile4(join4(hubSteeringDirClaude, file), "utf-8");
2151
2548
  const content = stripFrontMatter(raw).trim();
2152
2549
  if (content) {
2153
2550
  claudeMdSections.push(content);
@@ -2158,7 +2555,7 @@ async function generateClaudeCode(config, hubDir) {
2158
2555
  }
2159
2556
  } catch {
2160
2557
  }
2161
- await writeFile3(join3(hubDir, "CLAUDE.md"), claudeMdSections.join("\n\n"), "utf-8");
2558
+ await writeFile4(join4(hubDir, "CLAUDE.md"), claudeMdSections.join("\n\n"), "utf-8");
2162
2559
  console.log(chalk3.green(" Generated CLAUDE.md"));
2163
2560
  if (config.mcps?.length) {
2164
2561
  const mcpJson = {};
@@ -2171,8 +2568,8 @@ async function generateClaudeCode(config, hubDir) {
2171
2568
  mcpJson[mcp.name] = buildClaudeCodeMcpEntry(mcp);
2172
2569
  }
2173
2570
  }
2174
- await writeFile3(
2175
- join3(hubDir, ".mcp.json"),
2571
+ await writeFile4(
2572
+ join4(hubDir, ".mcp.json"),
2176
2573
  JSON.stringify({ mcpServers: mcpJson }, null, 2) + "\n",
2177
2574
  "utf-8"
2178
2575
  );
@@ -2214,22 +2611,22 @@ async function generateClaudeCode(config, hubDir) {
2214
2611
  claudeSettings.hooks = claudeHooks;
2215
2612
  }
2216
2613
  }
2217
- await writeFile3(
2218
- join3(claudeDir, "settings.json"),
2614
+ await writeFile4(
2615
+ join4(claudeDir, "settings.json"),
2219
2616
  JSON.stringify(claudeSettings, null, 2) + "\n",
2220
2617
  "utf-8"
2221
2618
  );
2222
2619
  console.log(chalk3.green(" Generated .claude/settings.json"));
2223
2620
  const gitignoreLines = buildGitignoreLines(config);
2224
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
2621
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
2225
2622
  console.log(chalk3.green(" Generated .gitignore"));
2226
2623
  }
2227
2624
  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 });
2625
+ const kiroDir = join4(hubDir, ".kiro");
2626
+ const steeringDir = join4(kiroDir, "steering");
2627
+ const settingsDir = join4(kiroDir, "settings");
2628
+ await mkdir4(steeringDir, { recursive: true });
2629
+ await mkdir4(settingsDir, { recursive: true });
2233
2630
  let mode = await getKiroMode(hubDir);
2234
2631
  if (!mode) {
2235
2632
  const { kiroMode } = await inquirer.prompt([
@@ -2250,25 +2647,32 @@ async function generateKiro(config, hubDir) {
2250
2647
  console.log(chalk3.dim(` Using saved Kiro mode: ${mode}`));
2251
2648
  }
2252
2649
  const gitignoreLines = buildGitignoreLines(config);
2253
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
2650
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
2254
2651
  console.log(chalk3.green(" Generated .gitignore"));
2255
2652
  const kiroRule = buildKiroOrchestratorRule(config);
2256
2653
  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");
2654
+ const personaKiro = await loadPersona(hubDir);
2655
+ const personaSectionKiro = personaKiro ? buildPersonaSection(personaKiro) : "";
2656
+ const kiroRuleWithSkills = [kiroRule, skillsSection, personaSectionKiro].filter(Boolean).join("\n");
2657
+ await writeFile4(join4(hubDir, "AGENTS.md"), kiroRuleWithSkills + "\n", "utf-8");
2259
2658
  console.log(chalk3.green(" Generated AGENTS.md"));
2659
+ if (personaKiro) {
2660
+ console.log(chalk3.green(` Applied persona: ${personaKiro.name} (${personaKiro.role})`));
2661
+ }
2662
+ await rm(join4(steeringDir, "orchestrator.md")).catch(() => {
2663
+ });
2260
2664
  const hubSteeringDir = resolve2(hubDir, "steering");
2261
2665
  try {
2262
2666
  const steeringFiles = await readdir2(hubSteeringDir);
2263
2667
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
2264
2668
  for (const file of mdFiles) {
2265
- const raw = await readFile3(join3(hubSteeringDir, file), "utf-8");
2669
+ const raw = await readFile4(join4(hubSteeringDir, file), "utf-8");
2266
2670
  const content = stripFrontMatter(raw);
2267
- const destPath = join3(steeringDir, file);
2671
+ const destPath = join4(steeringDir, file);
2268
2672
  let inclusion = "always";
2269
2673
  let meta;
2270
- if (existsSync2(destPath)) {
2271
- const existingContent = await readFile3(destPath, "utf-8");
2674
+ if (existsSync3(destPath)) {
2675
+ const existingContent = await readFile4(destPath, "utf-8");
2272
2676
  const existingFm = parseFrontMatter(existingContent);
2273
2677
  if (existingFm) {
2274
2678
  if (existingFm.inclusion === "auto" || existingFm.inclusion === "manual" || existingFm.inclusion === "fileMatch") {
@@ -2293,7 +2697,7 @@ async function generateKiro(config, hubDir) {
2293
2697
  }
2294
2698
  }
2295
2699
  const kiroSteering = buildKiroSteeringContent(content, inclusion, meta);
2296
- await writeFile3(destPath, kiroSteering, "utf-8");
2700
+ await writeFile4(destPath, kiroSteering, "utf-8");
2297
2701
  }
2298
2702
  if (mdFiles.length > 0) {
2299
2703
  console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
@@ -2302,31 +2706,36 @@ async function generateKiro(config, hubDir) {
2302
2706
  }
2303
2707
  const agentsDir = resolve2(hubDir, "agents");
2304
2708
  try {
2305
- const kiroAgentsDir = join3(kiroDir, "agents");
2306
- await mkdir3(kiroAgentsDir, { recursive: true });
2709
+ const kiroAgentsDir = join4(kiroDir, "agents");
2710
+ await mkdir4(kiroAgentsDir, { recursive: true });
2307
2711
  const agentFiles = await readdir2(agentsDir);
2308
2712
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
2309
2713
  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");
2714
+ const agentContent = await readFile4(join4(agentsDir, file), "utf-8");
2715
+ const agentName = file.replace(/\.md$/, "");
2716
+ const sandboxSvc = getSandboxService(config);
2717
+ const withSandbox = sandboxSvc ? injectSandboxContext(agentName, agentContent, sandboxSvc.port) : agentContent;
2718
+ const kiroAgent = buildKiroAgentContent(withSandbox);
2719
+ await writeFile4(join4(kiroAgentsDir, file), kiroAgent, "utf-8");
2313
2720
  }
2314
2721
  console.log(chalk3.green(` Copied ${mdFiles.length} agents to .kiro/agents/`));
2315
2722
  } catch {
2316
2723
  console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
2317
2724
  }
2318
2725
  const skillsDir = resolve2(hubDir, "skills");
2726
+ const remoteSkillsKiro = getRemoteSkillNames(config);
2319
2727
  try {
2320
2728
  const skillFolders = await readdir2(skillsDir);
2321
- const kiroSkillsDir = join3(kiroDir, "skills");
2322
- await mkdir3(kiroSkillsDir, { recursive: true });
2729
+ const kiroSkillsDir = join4(kiroDir, "skills");
2730
+ await mkdir4(kiroSkillsDir, { recursive: true });
2323
2731
  let count = 0;
2324
2732
  for (const folder of skillFolders) {
2325
- const skillFile = join3(skillsDir, folder, "SKILL.md");
2733
+ if (remoteSkillsKiro.has(folder)) continue;
2734
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
2326
2735
  try {
2327
- await readFile3(skillFile);
2328
- const srcDir = join3(skillsDir, folder);
2329
- const targetDir = join3(kiroSkillsDir, folder);
2736
+ await readFile4(skillFile);
2737
+ const srcDir = join4(skillsDir, folder);
2738
+ const targetDir = join4(kiroSkillsDir, folder);
2330
2739
  await cp(srcDir, targetDir, { recursive: true });
2331
2740
  count++;
2332
2741
  } catch {
@@ -2337,10 +2746,10 @@ async function generateKiro(config, hubDir) {
2337
2746
  }
2338
2747
  } catch {
2339
2748
  }
2340
- const kiroSkillsDirForDocs = join3(kiroDir, "skills");
2341
- await mkdir3(kiroSkillsDirForDocs, { recursive: true });
2749
+ const kiroSkillsDirForDocs = join4(kiroDir, "skills");
2750
+ await mkdir4(kiroSkillsDirForDocs, { recursive: true });
2342
2751
  await fetchHubDocsSkill(kiroSkillsDirForDocs);
2343
- await syncRemoteSources(config, hubDir, join3(kiroDir, "skills"), steeringDir);
2752
+ await syncRemoteSources(config, hubDir, join4(kiroDir, "skills"), steeringDir);
2344
2753
  if (config.mcps?.length) {
2345
2754
  const mcpConfig = {};
2346
2755
  const upstreamSet = getUpstreamNames(config.mcps);
@@ -2353,10 +2762,14 @@ async function generateKiro(config, hubDir) {
2353
2762
  mcpConfig[mcp.name] = buildKiroMcpEntry(mcp, mode);
2354
2763
  }
2355
2764
  }
2356
- const mcpJsonPath = join3(settingsDir, "mcp.json");
2765
+ const sandbox = getSandboxService(config);
2766
+ if (sandbox && !mcpConfig["sandbox"]) {
2767
+ mcpConfig["sandbox"] = buildSandboxMcpEntry(sandbox.port);
2768
+ }
2769
+ const mcpJsonPath = join4(settingsDir, "mcp.json");
2357
2770
  const disabledState = await readExistingMcpDisabledState(mcpJsonPath);
2358
2771
  applyDisabledState(mcpConfig, disabledState);
2359
- await writeFile3(
2772
+ await writeFile4(
2360
2773
  mcpJsonPath,
2361
2774
  JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
2362
2775
  "utf-8"
@@ -2383,13 +2796,13 @@ async function generateKiro(config, hubDir) {
2383
2796
  await generateVSCodeSettings(config, hubDir);
2384
2797
  }
2385
2798
  async function generateVSCodeSettings(config, hubDir) {
2386
- const vscodeDir = join3(hubDir, ".vscode");
2387
- await mkdir3(vscodeDir, { recursive: true });
2388
- const settingsPath = join3(vscodeDir, "settings.json");
2799
+ const vscodeDir = join4(hubDir, ".vscode");
2800
+ await mkdir4(vscodeDir, { recursive: true });
2801
+ const settingsPath = join4(vscodeDir, "settings.json");
2389
2802
  let existing = {};
2390
- if (existsSync2(settingsPath)) {
2803
+ if (existsSync3(settingsPath)) {
2391
2804
  try {
2392
- const raw = await readFile3(settingsPath, "utf-8");
2805
+ const raw = await readFile4(settingsPath, "utf-8");
2393
2806
  existing = JSON.parse(raw);
2394
2807
  } catch {
2395
2808
  existing = {};
@@ -2401,14 +2814,14 @@ async function generateVSCodeSettings(config, hubDir) {
2401
2814
  "git.detectSubmodulesLimit": Math.max(config.repos.length * 2, 20)
2402
2815
  };
2403
2816
  const merged = { ...existing, ...managed };
2404
- await writeFile3(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
2817
+ await writeFile4(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
2405
2818
  console.log(chalk3.green(" Generated .vscode/settings.json (git multi-repo detection)"));
2406
2819
  const workspaceFile = `${config.name}.code-workspace`;
2407
- const workspacePath = join3(hubDir, workspaceFile);
2820
+ const workspacePath = join4(hubDir, workspaceFile);
2408
2821
  let existingWorkspace = {};
2409
- if (existsSync2(workspacePath)) {
2822
+ if (existsSync3(workspacePath)) {
2410
2823
  try {
2411
- const raw = await readFile3(workspacePath, "utf-8");
2824
+ const raw = await readFile4(workspacePath, "utf-8");
2412
2825
  existingWorkspace = JSON.parse(raw);
2413
2826
  } catch {
2414
2827
  existingWorkspace = {};
@@ -2418,7 +2831,7 @@ async function generateVSCodeSettings(config, hubDir) {
2418
2831
  const existing2 = files.find((f) => f.endsWith(".code-workspace"));
2419
2832
  if (existing2) {
2420
2833
  try {
2421
- const raw = await readFile3(join3(hubDir, existing2), "utf-8");
2834
+ const raw = await readFile4(join4(hubDir, existing2), "utf-8");
2422
2835
  existingWorkspace = JSON.parse(raw);
2423
2836
  } catch {
2424
2837
  existingWorkspace = {};
@@ -2454,7 +2867,7 @@ async function generateVSCodeSettings(config, hubDir) {
2454
2867
  folders,
2455
2868
  settings: existingWorkspace.settings || {}
2456
2869
  };
2457
- await writeFile3(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
2870
+ await writeFile4(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
2458
2871
  console.log(chalk3.green(` Generated ${workspaceFile}`));
2459
2872
  }
2460
2873
  function extractEnvVarsByMcp(mcps) {
@@ -2501,7 +2914,7 @@ async function generateEnvExample(config, hubDir) {
2501
2914
  totalVars++;
2502
2915
  }
2503
2916
  if (totalVars === 0) return;
2504
- await writeFile3(join3(hubDir, ".env.example"), lines.join("\n") + "\n", "utf-8");
2917
+ await writeFile4(join4(hubDir, ".env.example"), lines.join("\n") + "\n", "utf-8");
2505
2918
  console.log(chalk3.green(` Generated .env.example (${totalVars} vars)`));
2506
2919
  }
2507
2920
  function buildGitignoreLines(config) {
@@ -2591,7 +3004,7 @@ async function resolveEditor(opts) {
2591
3004
  ]);
2592
3005
  return editor;
2593
3006
  }
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) => {
3007
+ 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
3008
  const hubDir = process.cwd();
2596
3009
  if (opts.check) {
2597
3010
  const result = await checkOutdated(hubDir);
@@ -2656,6 +3069,10 @@ Generating ${generator.name} configuration
2656
3069
  });
2657
3070
 
2658
3071
  export {
3072
+ colors,
3073
+ symbols,
3074
+ horizontalLine,
3075
+ personaCommand,
2659
3076
  generators,
2660
3077
  generateCommand,
2661
3078
  checkAndAutoRegenerate