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:
109
build.py
109
build.py
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user