diff --git a/build.py b/build.py index 87a1eee..1d987f0 100755 --- a/build.py +++ b/build.py @@ -928,22 +928,63 @@ def print_summary( def install_claude(output_dir: Path): - """Install personas to Claude Code as slash commands + agents.""" + """Install personas to Claude Code as slash commands + native subagents. + + Native Claude Code agents are .md files with YAML frontmatter under + ~/.claude/agents/. Tool permissions are scoped per domain: + - offensive/dev/engineering: full toolset incl. Bash/Write + - intel/analysis/history/law: read + search + web only + """ commands_dir = Path.home() / ".claude" / "commands" agents_dir = Path.home() / ".claude" / "agents" commands_dir.mkdir(parents=True, exist_ok=True) agents_dir.mkdir(parents=True, exist_ok=True) + # Domain → tool scope. "full" gets Bash + Write; "readonly" does not. + OFFENSIVE_DOMAINS = { + "cybersecurity", + "engineering", + "devops", + "software-development", + "ai-ml", + } + + FULL_TOOLS = ( + "Read, Edit, Write, Bash, Glob, Grep, WebFetch, WebSearch" + ) + READONLY_TOOLS = "Read, Glob, Grep, WebFetch, WebSearch" + + # Color palette per domain family for visual grouping in /agents picker. + DOMAIN_COLOR = { + "cybersecurity": "red", + "intelligence": "cyan", + "military": "orange", + "law": "yellow", + "economics": "green", + "politics": "purple", + "history": "blue", + "linguistics": "pink", + "media": "pink", + "engineering": "green", + "academia": "blue", + } + cmd_count = 0 agent_count = 0 + orphans_removed = 0 + + # Purge legacy .yml agents from previous build (wrong format, unused by CC). + for stale in agents_dir.glob("*.yml"): + stale.unlink() + orphans_removed += 1 for persona_dir in sorted(output_dir.iterdir()): if not persona_dir.is_dir() or persona_dir.name.startswith("_"): continue - # Install slash commands for all variants + # Install slash commands for all variants (general + specializations). for prompt_file in persona_dir.glob("*.prompt.md"): - variant = prompt_file.stem + variant = prompt_file.stem.replace(".prompt", "") codename = persona_dir.name cmd_name = ( f"persona-{codename}" @@ -956,7 +997,7 @@ def install_claude(output_dir: Path): dest.write_text(command_content, encoding="utf-8") cmd_count += 1 - # Install agent .yml for general variant (appears in /agents menu) + # Install native subagent for the general variant only. general_json = persona_dir / "general.json" if not general_json.exists(): continue @@ -975,43 +1016,45 @@ def install_claude(output_dir: Path): methodology = data.get("sections", {}).get("methodology", "") behavior = data.get("sections", {}).get("behavior_rules", "") - instructions = f"You are **{name}** ({address_to}) — {role}.\n\n" - instructions += f"Domain: {domain} | Tone: {tone}\n\n" + body = f"You are **{name}** ({address_to}) — {role}.\n\n" + body += f"Domain: {domain} | Tone: {tone}\n\n" if quote: - instructions += f'> "{quote}"\n\n' - instructions += "## Soul\n" + soul[:1500] + "\n\n" + body += f'> "{quote}"\n\n' + if soul: + body += "## Soul\n" + soul.strip() + "\n\n" if methodology: - instructions += "## Methodology\n" + methodology[:1500] + "\n\n" + body += "## Methodology\n" + methodology.strip() + "\n\n" if behavior: - instructions += "## Behavior\n" + behavior[:800] + "\n" + body += "## Behavior\n" + behavior.strip() + "\n\n" if skills: - instructions += "\n## Mapped Skills\n" + ", ".join(skills) + "\n" + body += "## Mapped Skills\n" + ", ".join(skills) + "\n" - agent = { - "name": codename, - "description": f"{name} ({address_to}) — {role}. {domain}.", - "instructions": instructions, - "allowedTools": [ - "Read(*)", - "Edit(*)", - "Write(*)", - "Bash(*)", - "Glob(*)", - "Grep(*)", - "WebFetch(*)", - "WebSearch(*)", - ], - } - agent_file = agents_dir / f"{codename}.yml" - agent_file.write_text( - yaml.dump( - agent, allow_unicode=True, default_flow_style=False, sort_keys=False - ), - encoding="utf-8", + tools = ( + FULL_TOOLS if domain in OFFENSIVE_DOMAINS else READONLY_TOOLS ) + color = DOMAIN_COLOR.get(domain, "gray") + + # Single-line description keeps YAML frontmatter safe. + desc = f"{name} ({address_to}) — {role}. Domain: {domain}.".replace( + "\n", " " + ) + + frontmatter = ( + "---\n" + f"name: {codename}\n" + f"description: {desc}\n" + f"tools: {tools}\n" + f"color: {color}\n" + "---\n\n" + ) + agent_file = agents_dir / f"{codename}.md" + agent_file.write_text(frontmatter + body, encoding="utf-8") agent_count += 1 - print(f" Claude: {cmd_count} commands + {agent_count} agents installed") + print( + f" Claude: {cmd_count} commands + {agent_count} agents installed " + f"({orphans_removed} legacy .yml purged)" + ) return cmd_count