Files
personas/build.py
salvacybersec a043f3abcc feat: Paperclip company agents + enhanced install
Paperclip install now deploys 52 agents (29 persona + 23 company) + 73 skills:
- Company agents from paperclip-docs: Odin (CEO), Thor (CTO), Freya (CMO),
  Frigg (COO), Heimd (PM), Bragi (Lead Dev), Vali (Backend), Vidar (Frontend),
  Heimdall (DevOps), Njord (SysAdmin), Forseti (QA), Tyr (Security),
  Eir (UI/UX), Kvasir (Data/ML), Loki (Social), Idunn (Content),
  Mimir (Research), Saga (SEO), Baldur (Support), Hermod (HR),
  Modir (Operations), Dvalin (Finance)
- Skills now include both shared-skills (42) + paperclip-skills (31) = 73

Source definitions stored at personas/_shared/paperclip-agents/
Each agent has: SOUL.md + hermes-config.yaml + AGENTS.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 21:33:27 +03:00

836 lines
34 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Build script: Generate .yaml, .json, .prompt.md from persona .md files.
Supports config.yaml for dynamic variable injection and user-specific customization.
New users: copy config.example.yaml → config.yaml and customize.
"""
import json
import re
import sys
from pathlib import Path
try:
import yaml
except ImportError:
print("PyYAML required: pip install pyyaml")
sys.exit(1)
def load_config(root: Path) -> dict:
"""Load config.yaml if it exists, otherwise return empty config."""
config_path = root / "config.yaml"
if config_path.exists():
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
print(f"Config loaded: {config_path}")
return config
example_path = root / "config.example.yaml"
if example_path.exists():
print("WARN: No config.yaml found. Using defaults. Copy config.example.yaml → config.yaml to customize.")
return {}
def flatten_config(config: dict, prefix: str = "") -> dict:
"""Flatten nested config dict for template substitution.
Example: {"user": {"name": "Salva"}} → {"user.name": "Salva"}
"""
flat = {}
for key, value in config.items():
full_key = f"{prefix}{key}" if not prefix else f"{prefix}.{key}"
if isinstance(value, dict):
flat.update(flatten_config(value, full_key))
elif isinstance(value, list):
flat[full_key] = value
flat[f"{full_key}.count"] = len(value)
flat[f"{full_key}.csv"] = ", ".join(str(v) for v in value if not isinstance(v, dict))
else:
flat[full_key] = value
return flat
def inject_config(content: str, flat_config: dict) -> str:
"""Replace {{config.key}} placeholders with config values."""
def replacer(match):
key = match.group(1).strip()
value = flat_config.get(key, match.group(0)) # keep original if not found
if isinstance(value, list):
return ", ".join(str(v) for v in value if not isinstance(v, dict))
if isinstance(value, bool):
return "enabled" if value else "disabled"
return str(value)
return re.sub(r"\{\{(.+?)\}\}", replacer, content)
def check_conditionals(content: str, flat_config: dict) -> str:
"""Process {{#if key}}...{{/if}} and {{#unless key}}...{{/unless}} blocks."""
# Handle {{#if key}}content{{/if}}
def if_replacer(match):
key = match.group(1).strip()
body = match.group(2)
value = flat_config.get(key)
if value and value not in (False, 0, "", "false", "none", "disabled", None, []):
return body
return ""
content = re.sub(r"\{\{#if (.+?)\}\}(.*?)\{\{/if\}\}", if_replacer, content, flags=re.DOTALL)
# Handle {{#unless key}}content{{/unless}}
def unless_replacer(match):
key = match.group(1).strip()
body = match.group(2)
value = flat_config.get(key)
if not value or value in (False, 0, "", "false", "none", "disabled", None, []):
return body
return ""
content = re.sub(r"\{\{#unless (.+?)\}\}(.*?)\{\{/unless\}\}", unless_replacer, content, flags=re.DOTALL)
return content
def parse_persona_md(filepath: Path, flat_config: dict) -> dict:
"""Parse a persona markdown file into structured data."""
content = filepath.read_text(encoding="utf-8")
# Apply config injection
if flat_config:
content = check_conditionals(content, flat_config)
content = inject_config(content, flat_config)
# Extract YAML frontmatter
fm_match = re.match(r"^---\n(.*?)\n---\n(.*)$", content, re.DOTALL)
if not fm_match:
print(f" WARN: No frontmatter in {filepath}")
return {}
frontmatter = yaml.safe_load(fm_match.group(1))
body = fm_match.group(2).strip()
# Extract sections from body
sections = {}
current_section = None
current_content = []
for line in body.split("\n"):
if line.startswith("## "):
if current_section:
sections[current_section] = "\n".join(current_content).strip()
current_section = line[3:].strip().lower().replace(" ", "_").replace("&", "and")
current_content = []
else:
current_content.append(line)
if current_section:
sections[current_section] = "\n".join(current_content).strip()
return {
"metadata": frontmatter,
"sections": sections,
"raw_body": body,
}
def build_persona(persona_dir: Path, output_dir: Path, flat_config: dict, config: dict, escalation_graph: dict = None, skills_index: dict = None):
"""Build all variants for a persona directory."""
md_files = sorted(persona_dir.glob("*.md"))
if not md_files:
return 0
persona_name = persona_dir.name
out_path = output_dir / persona_name
out_path.mkdir(parents=True, exist_ok=True)
# Load _meta.yaml if exists
meta_file = persona_dir / "_meta.yaml"
meta = {}
if meta_file.exists():
meta_content = meta_file.read_text(encoding="utf-8")
if flat_config:
meta_content = inject_config(meta_content, flat_config)
meta = yaml.safe_load(meta_content) or {}
# Apply config overrides for address
addresses = config.get("persona_defaults", {}).get("custom_addresses", {})
if persona_name in addresses:
meta["address_to"] = addresses[persona_name]
count = 0
for md_file in md_files:
if md_file.name.startswith("_"):
continue
variant = md_file.stem
parsed = parse_persona_md(md_file, flat_config)
if not parsed:
continue
# Build output object
output = {**meta, **parsed["metadata"], "variant": variant, "sections": parsed["sections"]}
# Inject config metadata
if config:
output["_config"] = {
"user": config.get("user", {}).get("name", "unknown"),
"tools": {k: v for k, v in config.get("infrastructure", {}).get("tools", {}).items() if v is True},
"frameworks": {k: v for k, v in config.get("frameworks", {}).items() if v is True},
"regional_focus": config.get("regional_focus", {}),
}
# Inject escalation graph for this persona
if escalation_graph and persona_name in escalation_graph:
output["escalates_to"] = escalation_graph[persona_name]
# Inject mapped skills for this persona
if skills_index:
mapped_skills = []
for skill_name, skill_info in skills_index.get("skills", {}).items():
if persona_name in skill_info.get("personas", []):
mapped_skills.append(skill_name)
if mapped_skills:
output["skills"] = sorted(mapped_skills)
# Inject section word counts for quality tracking
output["_stats"] = {
"total_words": sum(len(s.split()) for s in parsed["sections"].values()),
"sections": list(parsed["sections"].keys()),
"section_count": len(parsed["sections"]),
}
# Write YAML
yaml_out = out_path / f"{variant}.yaml"
yaml_out.write_text(
yaml.dump(output, allow_unicode=True, default_flow_style=False, sort_keys=False),
encoding="utf-8",
)
# Write JSON
json_out = out_path / f"{variant}.json"
json_out.write_text(json.dumps(output, ensure_ascii=False, indent=2), encoding="utf-8")
# Write plain system prompt (just the body, no config metadata)
prompt_out = out_path / f"{variant}.prompt.md"
prompt_out.write_text(parsed["raw_body"], encoding="utf-8")
count += 1
print(f" Built: {persona_name}/{variant} -> .yaml .json .prompt.md")
return count
SKILL_PERSONA_MAP = {
# Cybersecurity skills → personas
"pentest": ["neo"], "nmap-recon": ["neo", "vortex"], "security-scanner": ["neo", "phantom"],
"sql-injection-testing": ["neo", "phantom"], "stealth-browser": ["neo", "oracle"],
"security-audit-toolkit": ["neo", "forge"], "pwnclaw-security-scan": ["neo"],
"senior-secops": ["bastion"], "clawsec": ["neo", "vortex"],
"pcap-analyzer": ["vortex", "bastion"], "sys-guard-linux-remediator": ["bastion"],
"ctf-writeup-generator": ["neo"], "dns-networking": ["vortex", "architect"],
"network-scanner": ["neo", "vortex"], "security-skill-scanner": ["neo"],
"pentest-active-directory": ["neo"], "pentest-api-attacker": ["neo", "phantom"],
"pentest-auth-bypass": ["neo", "phantom"], "pentest-c2-operator": ["neo", "sentinel"],
"gov-cybersecurity": ["sentinel", "bastion"],
# Intelligence skills → personas
"osint-investigator": ["oracle"], "seithar-intel": ["sentinel", "frodo"],
"freshrss": ["frodo", "oracle"], "freshrss-reader": ["frodo", "oracle"],
"war-intel-monitor": ["frodo", "marshal"], "news-crawler": ["frodo", "herald"],
"dellight-intelligence-ops": ["frodo", "echo"], "dellight-strategic-intelligence": ["frodo"],
"agent-intelligence-network-scan": ["oracle"], "social-trust-manipulation-detector": ["ghost"],
# Infrastructure skills → personas
"docker-essentials": ["architect"], "session-logs": ["architect"],
# Document processing → personas
"image-ocr": ["oracle", "scribe"], "mistral-ocr": ["oracle", "scribe"],
"pdf-text-extractor": ["scribe", "scholar"], "youtube-transcript": ["herald", "scholar"],
# Web scraping → personas
"deep-scraper": ["oracle"], "crawl-for-ai": ["oracle", "herald"],
}
def build_skills_index(shared_dir: Path) -> dict:
"""Index all shared skills from _shared/skills/ and _shared/paperclip-skills/."""
index = {"skills": {}, "paperclip_skills": {}, "design_brands": [], "ui_ux_styles": 0}
# Index shared-skills
skills_dir = shared_dir / "skills"
if skills_dir.exists():
for skill_dir in sorted(skills_dir.iterdir()):
if not skill_dir.is_dir():
continue
skill_md = skill_dir / "SKILL.md"
if skill_md.exists():
content = skill_md.read_text(encoding="utf-8")
first_line = ""
for line in content.split("\n"):
line = line.strip()
if line and not line.startswith(("---", "#", "name:", "description:")):
first_line = line[:120]
break
index["skills"][skill_dir.name] = {
"personas": SKILL_PERSONA_MAP.get(skill_dir.name, []),
"summary": first_line,
"has_references": (skill_dir / "references").is_dir(),
}
# Index paperclip-skills
pskills_dir = shared_dir / "paperclip-skills"
if pskills_dir.exists():
for skill_dir in sorted(pskills_dir.iterdir()):
if not skill_dir.is_dir():
continue
skill_md = skill_dir / "SKILL.md"
if skill_md.exists():
index["paperclip_skills"][skill_dir.name] = True
# Index design brands
design_dir = shared_dir / "design-md"
if design_dir.exists():
index["design_brands"] = sorted([d.name for d in design_dir.iterdir() if d.is_dir()])
# Count UI/UX data
uiux_dir = shared_dir / "ui-ux-pro-max" / "data"
if uiux_dir.exists():
index["ui_ux_styles"] = sum(1 for f in uiux_dir.glob("*.csv"))
return index
def build_escalation_graph(personas_dir: Path, flat_config: dict) -> dict:
"""Extract cross-persona escalation paths from Boundaries sections."""
graph = {} # {persona: [escalation_targets]}
for persona_dir in sorted(personas_dir.iterdir()):
if not persona_dir.is_dir() or persona_dir.name.startswith((".", "_")):
continue
general = persona_dir / "general.md"
if not general.exists():
continue
parsed = parse_persona_md(general, flat_config)
if not parsed:
continue
boundaries = parsed["sections"].get("boundaries", "")
targets = re.findall(r"Escalate to \*\*(\w+)\*\*", boundaries)
graph[persona_dir.name] = [t.lower() for t in targets]
return graph
def build_trigger_index(personas_dir: Path) -> dict:
"""Build reverse index: trigger keyword → persona codenames for multi-agent routing."""
index = {} # {trigger: [persona_names]}
for persona_dir in sorted(personas_dir.iterdir()):
if not persona_dir.is_dir() or persona_dir.name.startswith((".", "_")):
continue
meta_file = persona_dir / "_meta.yaml"
if not meta_file.exists():
continue
meta = yaml.safe_load(meta_file.read_text(encoding="utf-8")) or {}
triggers = meta.get("activation_triggers", [])
for trigger in triggers:
t = trigger.lower()
if t not in index:
index[t] = []
index[t].append(persona_dir.name)
return index
def validate_persona(persona_name: str, parsed: dict) -> list:
"""Validate persona structure and return warnings."""
warnings = []
required_sections = ["soul", "expertise", "methodology", "boundaries"]
for section in required_sections:
if section not in parsed.get("sections", {}):
warnings.append(f"Missing section: {section}")
elif len(parsed["sections"][section].split()) < 30:
warnings.append(f"Thin section ({len(parsed['sections'][section].split())} words): {section}")
fm = parsed.get("metadata", {})
for field in ["codename", "name", "domain", "address_to", "tone"]:
if field not in fm:
warnings.append(f"Missing frontmatter: {field}")
return warnings
def build_catalog(personas_dir: Path, output_dir: Path, config: dict, flat_config: dict):
"""Generate CATALOG.md with stats, escalation paths, and trigger index."""
addresses = config.get("persona_defaults", {}).get("custom_addresses", {})
# Build escalation graph and trigger index
escalation_graph = build_escalation_graph(personas_dir, flat_config)
trigger_index = build_trigger_index(personas_dir)
catalog_lines = [
"# Persona Catalog\n",
f"_Auto-generated by build.py | User: {config.get('user', {}).get('name', 'default')}_\n",
]
total_words = 0
total_sections = 0
all_warnings = []
for persona_dir in sorted(personas_dir.iterdir()):
if not persona_dir.is_dir() or persona_dir.name.startswith((".", "_")):
continue
meta_file = persona_dir / "_meta.yaml"
if not meta_file.exists():
continue
meta = yaml.safe_load(meta_file.read_text(encoding="utf-8")) or {}
codename = meta.get("codename", persona_dir.name)
address = addresses.get(persona_dir.name, meta.get("address_to", "N/A"))
variants = [f.stem for f in sorted(persona_dir.glob("*.md")) if not f.name.startswith("_")]
# Parse general.md for stats
general = persona_dir / "general.md"
word_count = 0
section_count = 0
if general.exists():
parsed = parse_persona_md(general, flat_config)
if parsed:
for s in parsed["sections"].values():
word_count += len(s.split())
section_count = len(parsed["sections"])
# Validate
warns = validate_persona(codename, parsed)
for w in warns:
all_warnings.append(f" {codename}: {w}")
total_words += word_count
total_sections += section_count
escalates_to = escalation_graph.get(persona_dir.name, [])
catalog_lines.append(f"## {codename}{meta.get('role', 'Unknown')}")
catalog_lines.append(f"- **Domain:** {meta.get('domain', 'N/A')}")
catalog_lines.append(f"- **Hitap:** {address}")
catalog_lines.append(f"- **Variants:** {', '.join(variants)}")
catalog_lines.append(f"- **Depth:** {word_count:,} words, {section_count} sections")
if escalates_to:
catalog_lines.append(f"- **Escalates to:** {', '.join(escalates_to)}")
catalog_lines.append("")
# Add trigger index section
catalog_lines.append("---\n")
catalog_lines.append("## Activation Trigger Index\n")
catalog_lines.append("_Keyword → persona routing for multi-agent systems_\n")
for trigger in sorted(trigger_index.keys()):
personas = ", ".join(trigger_index[trigger])
catalog_lines.append(f"- **{trigger}** → {personas}")
catalog_lines.append("")
# Add stats
catalog_lines.append("---\n")
catalog_lines.append("## Build Statistics\n")
catalog_lines.append(f"- Total prompt content: {total_words:,} words")
catalog_lines.append(f"- Total sections: {total_sections}")
catalog_lines.append(f"- Escalation connections: {sum(len(v) for v in escalation_graph.values())}")
catalog_lines.append(f"- Unique triggers: {len(trigger_index)}")
catalog_lines.append("")
catalog_path = personas_dir / "CATALOG.md"
catalog_path.write_text("\n".join(catalog_lines), encoding="utf-8")
print(f" Catalog: {catalog_path}")
# Write escalation graph and trigger index as JSON for API consumers
index_path = output_dir / "_index"
index_path.mkdir(parents=True, exist_ok=True)
(index_path / "escalation_graph.json").write_text(
json.dumps(escalation_graph, indent=2, ensure_ascii=False), encoding="utf-8"
)
(index_path / "trigger_index.json").write_text(
json.dumps(trigger_index, indent=2, ensure_ascii=False), encoding="utf-8"
)
print(f" Index: {index_path}/escalation_graph.json, trigger_index.json")
# Write skills index if shared dir exists
shared_dir = personas_dir / "_shared"
if shared_dir.exists():
si = build_skills_index(shared_dir)
(index_path / "skills_index.json").write_text(
json.dumps(si, indent=2, ensure_ascii=False), encoding="utf-8"
)
print(f" Skills: {len(si.get('skills', {}))} shared + {len(si.get('paperclip_skills', {}))} paperclip + {len(si.get('design_brands', []))} design brands + {si.get('ui_ux_styles', 0)} UI/UX data files")
# Print validation warnings
if all_warnings:
print(f"\n WARNINGS ({len(all_warnings)}):")
for w in all_warnings:
print(f" {w}")
return total_words
def print_summary(config: dict, total_personas: int, total_variants: int, total_words: int = 0):
"""Print build summary with config status."""
print("\n" + "=" * 50)
print(f"BUILD COMPLETE")
print(f" Personas: {total_personas}")
print(f" Variants: {total_variants}")
print(f" Words: {total_words:,}")
print(f" Output: generated/")
print(f" Index: generated/_index/")
if config:
user = config.get("user", {}).get("name", "?")
tools_on = sum(1 for v in config.get("infrastructure", {}).get("tools", {}).values() if v is True)
frameworks_on = sum(1 for v in config.get("frameworks", {}).values() if v is True)
regions = config.get("regional_focus", {}).get("primary", [])
print(f"\n Config: {user}")
print(f" Tools: {tools_on} enabled")
print(f" Frameworks: {frameworks_on} enabled")
if regions:
print(f" Regions: {', '.join(regions)}")
else:
print("\n Config: none (using defaults)")
print(" Tip: Copy config.example.yaml → config.yaml to customize")
print("=" * 50)
def install_claude(output_dir: Path):
"""Install personas to Claude Code as slash commands (~/.claude/commands/)."""
commands_dir = Path.home() / ".claude" / "commands"
commands_dir.mkdir(parents=True, exist_ok=True)
count = 0
for persona_dir in sorted(output_dir.iterdir()):
if not persona_dir.is_dir() or persona_dir.name.startswith("_"):
continue
for prompt_file in persona_dir.glob("*.prompt.md"):
variant = prompt_file.stem
codename = persona_dir.name
cmd_name = f"persona-{codename}" if variant == "general" else f"persona-{codename}-{variant}"
dest = commands_dir / f"{cmd_name}.md"
content = prompt_file.read_text(encoding="utf-8")
# Wrap as Claude command: $ARGUMENTS placeholder for user query
command_content = f"{content}\n\n---\nUser query: $ARGUMENTS\n"
dest.write_text(command_content, encoding="utf-8")
count += 1
print(f" Claude: {count} commands installed to {commands_dir}")
return count
def install_antigravity(output_dir: Path):
"""Install personas to Antigravity IDE system prompts."""
# Antigravity stores system prompts in ~/.config/antigravity/prompts/ or project .antigravity/
ag_dir = Path.home() / ".config" / "antigravity" / "personas"
ag_dir.mkdir(parents=True, exist_ok=True)
count = 0
for persona_dir in sorted(output_dir.iterdir()):
if not persona_dir.is_dir() or persona_dir.name.startswith("_"):
continue
for prompt_file in persona_dir.glob("*.prompt.md"):
variant = prompt_file.stem
codename = persona_dir.name
dest = ag_dir / codename / f"{variant}.md"
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(prompt_file.read_text(encoding="utf-8"), encoding="utf-8")
count += 1
print(f" Antigravity: {count} personas installed to {ag_dir}")
return count
def install_gemini(output_dir: Path):
"""Install personas as Gemini Gems (JSON format for Google AI Studio)."""
gems_dir = output_dir / "_gems"
gems_dir.mkdir(parents=True, exist_ok=True)
count = 0
for persona_dir in sorted(output_dir.iterdir()):
if not persona_dir.is_dir() or persona_dir.name.startswith("_"):
continue
for json_file in persona_dir.glob("*.json"):
data = json.loads(json_file.read_text(encoding="utf-8"))
variant = data.get("variant", json_file.stem)
codename = data.get("codename", persona_dir.name)
name = data.get("name", codename.title())
# Build Gemini Gem format
gem = {
"name": f"{name}{variant}" if variant != "general" else name,
"description": f"{data.get('role', '')} | {data.get('domain', '')}",
"system_instruction": data.get("sections", {}).get("soul", "") + "\n\n" +
data.get("sections", {}).get("expertise", "") + "\n\n" +
data.get("sections", {}).get("methodology", "") + "\n\n" +
data.get("sections", {}).get("behavior_rules", ""),
"metadata": {
"codename": codename,
"variant": variant,
"domain": data.get("domain", ""),
"address_to": data.get("address_to", ""),
"tone": data.get("tone", ""),
"activation_triggers": data.get("activation_triggers", []),
},
}
dest = gems_dir / f"{codename}-{variant}.json"
dest.write_text(json.dumps(gem, ensure_ascii=False, indent=2), encoding="utf-8")
count += 1
print(f" Gemini: {count} gems generated to {gems_dir}")
return count
def install_paperclip(output_dir: Path, personas_dir: Path):
"""Install personas as Paperclip agents (SOUL.md + hermes-config.yaml + AGENTS.md per agent)."""
pc_dir = output_dir / "_paperclip"
agents_dir = pc_dir / "agents"
skills_dir = pc_dir / "skills"
agents_dir.mkdir(parents=True, exist_ok=True)
skills_dir.mkdir(parents=True, exist_ok=True)
# Build escalation graph for AGENTS.md org chart
flat_config = {}
escalation_graph = build_escalation_graph(personas_dir, flat_config)
# Domain → toolset mapping for hermes-config
domain_toolsets = {
"cybersecurity": ["terminal", "file", "web", "code_execution"],
"intelligence": ["terminal", "file", "web"],
"military": ["terminal", "file", "web"],
"engineering": ["terminal", "file", "web", "code_execution"],
"law-economics": ["file", "web"],
"history": ["file", "web"],
"linguistics": ["file", "web"],
"academia": ["file", "web"],
}
agent_count = 0
skill_count = 0
for persona_dir in sorted(output_dir.iterdir()):
if not persona_dir.is_dir() or persona_dir.name.startswith("_"):
continue
general_json = persona_dir / "general.json"
general_prompt = persona_dir / "general.prompt.md"
if not general_json.exists():
continue
data = json.loads(general_json.read_text(encoding="utf-8"))
codename = data.get("codename", persona_dir.name)
name = data.get("name", codename.title())
role = data.get("role", "Specialist")
domain = data.get("domain", "general")
address_to = data.get("address_to", "")
tone = data.get("tone", "")
escalates_to = escalation_graph.get(persona_dir.name, [])
skills = data.get("skills", [])
agent_dir = agents_dir / codename
agent_dir.mkdir(parents=True, exist_ok=True)
# 1. SOUL.md — persona prompt adapted to Paperclip format
soul_lines = [
f"# {name}{role}\n",
f"## Kimlik",
f"- **Ad:** {name}",
f"- **Kod Adı:** {codename}",
f"- **Hitap:** {address_to}",
f"- **Domain:** {domain}",
f"- **Ton:** {tone}",
"",
]
if escalates_to:
soul_lines.append(f"## İlişkiler")
soul_lines.append(f"- **Escalation:** {', '.join(escalates_to)}")
soul_lines.append("")
if skills:
soul_lines.append(f"## Skills")
for s in skills:
soul_lines.append(f"- {s}")
soul_lines.append("")
# Append the full prompt body
if general_prompt.exists():
soul_lines.append("## Detaylı Tanım\n")
soul_lines.append(general_prompt.read_text(encoding="utf-8"))
(agent_dir / "SOUL.md").write_text("\n".join(soul_lines), encoding="utf-8")
# 2. hermes-config.yaml
toolsets = domain_toolsets.get(domain, ["terminal", "file", "web"])
hermes_config = {
"model": "qwen/qwen3.6-plus:free",
"provider": "openrouter",
"defaults": {"quiet": True, "reasoning_effort": "medium"},
"mcp_servers": {
"web-search": {
"command": "npx",
"args": ["-y", "ddg-mcp-search"],
},
},
"skills": {"external_dirs": ["~/.hermes/skills"]},
"toolsets": toolsets,
}
(agent_dir / "hermes-config.yaml").write_text(
yaml.dump(hermes_config, allow_unicode=True, default_flow_style=False),
encoding="utf-8",
)
# 3. AGENTS.md — workspace overview with org connections
agents_md_lines = [
f"# {name} — Workspace\n",
f"- **Agent:** {name} ({role})",
f"- **Domain:** {domain}",
"",
]
if escalates_to:
agents_md_lines.append("## Bağlantılar\n")
for target in escalates_to:
agents_md_lines.append(f"- → {target}")
agents_md_lines.append("")
(agent_dir / "AGENTS.md").write_text("\n".join(agents_md_lines), encoding="utf-8")
agent_count += 1
# Copy shared skills as Paperclip skills (SKILL.md format already compatible)
shared_skills = personas_dir / "_shared" / "skills"
if shared_skills.exists():
for skill_dir in sorted(shared_skills.iterdir()):
if not skill_dir.is_dir():
continue
skill_md = skill_dir / "SKILL.md"
if skill_md.exists():
dest = skills_dir / skill_dir.name
dest.mkdir(parents=True, exist_ok=True)
(dest / "SKILL.md").write_text(skill_md.read_text(encoding="utf-8"), encoding="utf-8")
refs = skill_dir / "references"
if refs.is_dir():
import shutil
shutil.copytree(refs, dest / "references", dirs_exist_ok=True)
skill_count += 1
# Copy paperclip-specific skills
pc_skills = personas_dir / "_shared" / "paperclip-skills"
if pc_skills.exists():
for skill_dir in sorted(pc_skills.iterdir()):
if not skill_dir.is_dir():
continue
skill_md = skill_dir / "SKILL.md"
if skill_md.exists() and not (skills_dir / skill_dir.name).exists():
dest = skills_dir / skill_dir.name
dest.mkdir(parents=True, exist_ok=True)
(dest / "SKILL.md").write_text(skill_md.read_text(encoding="utf-8"), encoding="utf-8")
refs = skill_dir / "references"
if refs.is_dir():
import shutil
shutil.copytree(refs, dest / "references", dirs_exist_ok=True)
scripts = skill_dir / "scripts"
if scripts.is_dir():
import shutil
shutil.copytree(scripts, dest / "scripts", dirs_exist_ok=True)
skill_count += 1
# Deploy original Paperclip company agents from _shared/paperclip-agents/
pc_agents_src = personas_dir / "_shared" / "paperclip-agents"
pc_agent_count = 0
if pc_agents_src.exists():
for agent_src in sorted(pc_agents_src.iterdir()):
if not agent_src.is_dir():
continue
agent_name = agent_src.name
# Skip if persona-based agent already exists with same name
if (agents_dir / agent_name).exists():
continue
dest = agents_dir / agent_name
dest.mkdir(parents=True, exist_ok=True)
for f in agent_src.iterdir():
if f.is_file():
(dest / f.name).write_text(f.read_text(encoding="utf-8"), encoding="utf-8")
pc_agent_count += 1
total_agents = agent_count + pc_agent_count
print(f" Paperclip: {agent_count} persona agents + {pc_agent_count} company agents + {skill_count} skills to {pc_dir}")
return total_agents
def install_openclaw(output_dir: Path):
"""Install personas to OpenClaw format (IDENTITY.md + individual persona files)."""
oc_dir = output_dir / "_openclaw"
oc_dir.mkdir(parents=True, exist_ok=True)
personas_dir = oc_dir / "personas"
personas_dir.mkdir(parents=True, exist_ok=True)
count = 0
identity_sections = []
for persona_dir in sorted(output_dir.iterdir()):
if not persona_dir.is_dir() or persona_dir.name.startswith("_"):
continue
general_prompt = persona_dir / "general.prompt.md"
if not general_prompt.exists():
continue
content = general_prompt.read_text(encoding="utf-8")
codename = persona_dir.name
# Write individual persona file
(personas_dir / f"{codename}.md").write_text(content, encoding="utf-8")
# Extract first line as title for IDENTITY.md
first_line = content.split("\n")[0].strip("# ").strip()
identity_sections.append(f"### {first_line}\nSee: personas/{codename}.md\n")
count += 1
# Write IDENTITY.md
identity = "# IDENTITY — Persona Definitions\n\n" + "\n".join(identity_sections)
(oc_dir / "IDENTITY.md").write_text(identity, encoding="utf-8")
print(f" OpenClaw: {count} personas + IDENTITY.md to {oc_dir}")
return count
def main():
import argparse
parser = argparse.ArgumentParser(description="Build persona library and optionally install to platforms.")
parser.add_argument("--install", choices=["claude", "antigravity", "gemini", "openclaw", "paperclip", "all"],
help="Install generated personas to a target platform")
args = parser.parse_args()
root = Path(__file__).parent
personas_dir = root / "personas"
if not personas_dir.exists():
print("No personas/ directory found.")
sys.exit(1)
output_dir = root / "generated"
# Load config
config = load_config(root)
flat_config = flatten_config(config) if config else {}
# Find all persona directories
persona_dirs = [
d for d in sorted(personas_dir.iterdir()) if d.is_dir() and not d.name.startswith((".", "_"))
]
if not persona_dirs:
print("No persona directories found.")
sys.exit(1)
output_dir.mkdir(parents=True, exist_ok=True)
print(f"Building {len(persona_dirs)} personas -> {output_dir}\n")
# Pre-build escalation graph and skills index
escalation_graph = build_escalation_graph(personas_dir, flat_config)
shared_dir = personas_dir / "_shared"
skills_index = build_skills_index(shared_dir) if shared_dir.exists() else {}
total_variants = 0
for pdir in persona_dirs:
total_variants += build_persona(pdir, output_dir, flat_config, config, escalation_graph, skills_index)
total_words = build_catalog(personas_dir, output_dir, config, flat_config)
# Platform installation
if args.install:
print(f"\n--- Installing to: {args.install} ---\n")
targets = ["claude", "antigravity", "gemini", "openclaw", "paperclip"] if args.install == "all" else [args.install]
for target in targets:
if target == "claude":
install_claude(output_dir)
elif target == "antigravity":
install_antigravity(output_dir)
elif target == "gemini":
install_gemini(output_dir)
elif target == "openclaw":
install_openclaw(output_dir)
elif target == "paperclip":
install_paperclip(output_dir, personas_dir)
print_summary(config, len(persona_dirs), total_variants, total_words)
if __name__ == "__main__":
main()