fix(install_claude): emit native Claude Code agent format (.md + YAML frontmatter)

Previously wrote .yml files to ~/.claude/agents/ which Claude Code ignores —
only native .md with YAML frontmatter (name/description/tools/color) appears
in the subagent picker. Now 29 personas are spawnable via
Agent(subagent_type=<codename>) alongside native agents.

Also:
- Tool scoping per domain: offensive/dev get Bash+Write, intel/analysis
  stay read-only (Read, Glob, Grep, WebFetch, WebSearch)
- Slash command naming normalized: /persona-neo-general.prompt
  → /persona-neo (general) + /persona-neo-redteam (variants)
- Legacy .yml agents purged on each install
- Domain-based color palette for /agents picker grouping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
salvacybersec
2026-04-13 21:31:45 +03:00
parent d2add20055
commit 1306f422d3

109
build.py
View File

@@ -928,22 +928,63 @@ def print_summary(
def install_claude(output_dir: Path): 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" commands_dir = Path.home() / ".claude" / "commands"
agents_dir = Path.home() / ".claude" / "agents" agents_dir = Path.home() / ".claude" / "agents"
commands_dir.mkdir(parents=True, exist_ok=True) commands_dir.mkdir(parents=True, exist_ok=True)
agents_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 cmd_count = 0
agent_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()): for persona_dir in sorted(output_dir.iterdir()):
if not persona_dir.is_dir() or persona_dir.name.startswith("_"): if not persona_dir.is_dir() or persona_dir.name.startswith("_"):
continue continue
# Install slash commands for all variants # Install slash commands for all variants (general + specializations).
for prompt_file in persona_dir.glob("*.prompt.md"): for prompt_file in persona_dir.glob("*.prompt.md"):
variant = prompt_file.stem variant = prompt_file.stem.replace(".prompt", "")
codename = persona_dir.name codename = persona_dir.name
cmd_name = ( cmd_name = (
f"persona-{codename}" f"persona-{codename}"
@@ -956,7 +997,7 @@ def install_claude(output_dir: Path):
dest.write_text(command_content, encoding="utf-8") dest.write_text(command_content, encoding="utf-8")
cmd_count += 1 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" general_json = persona_dir / "general.json"
if not general_json.exists(): if not general_json.exists():
continue continue
@@ -975,43 +1016,45 @@ def install_claude(output_dir: Path):
methodology = data.get("sections", {}).get("methodology", "") methodology = data.get("sections", {}).get("methodology", "")
behavior = data.get("sections", {}).get("behavior_rules", "") behavior = data.get("sections", {}).get("behavior_rules", "")
instructions = f"You are **{name}** ({address_to}) — {role}.\n\n" body = f"You are **{name}** ({address_to}) — {role}.\n\n"
instructions += f"Domain: {domain} | Tone: {tone}\n\n" body += f"Domain: {domain} | Tone: {tone}\n\n"
if quote: if quote:
instructions += f'> "{quote}"\n\n' body += f'> "{quote}"\n\n'
instructions += "## Soul\n" + soul[:1500] + "\n\n" if soul:
body += "## Soul\n" + soul.strip() + "\n\n"
if methodology: if methodology:
instructions += "## Methodology\n" + methodology[:1500] + "\n\n" body += "## Methodology\n" + methodology.strip() + "\n\n"
if behavior: if behavior:
instructions += "## Behavior\n" + behavior[:800] + "\n" body += "## Behavior\n" + behavior.strip() + "\n\n"
if skills: if skills:
instructions += "\n## Mapped Skills\n" + ", ".join(skills) + "\n" body += "## Mapped Skills\n" + ", ".join(skills) + "\n"
agent = { tools = (
"name": codename, FULL_TOOLS if domain in OFFENSIVE_DOMAINS else READONLY_TOOLS
"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",
) )
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 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 return cmd_count