feat(build): user-agnostic personalized variant routing
Personalized variants (default trigger: salva.md) compile to a separate
top-level folder so generated/<persona>/ stays clean of per-user content.
Both the trigger variant filename(s) and the output folder name are
config-driven via config.user — the build is truly user-agnostic.
Build changes:
- Module-level _RUNTIME dict cached by configure_runtime(config) at startup
- resolve_personalized_variants(config) — config.user.personalized_variants
- resolve_personalized_output_dirname(config) — slug-derives from
user.name (e.g. "Salva" → "salva-personas") with explicit override key
- resolve_personalized_source_dirname(config) — defaults to literal
"personalized" (gitignorable, user-name-independent)
- iter_persona_output_dirs() — single helper used by all install_*()
functions to transparently iterate both layouts
- build_persona() routes salva variants based on _RUNTIME at write time
- build_skills_index() also scans <repo>/personalized/skills/ so personal
skills (with user-private data) merge into the shared skill index
- main() picks up persona dirs from BOTH personas/ and personalized/
New config keys (all optional):
- user.personalized_variants
- user.personalized_output_folder
- user.personalized_source_folder
Output layout (e.g. for user.name="Salva"):
- generated/<persona>/general.{json,yaml,prompt.md} (shared)
- generated/<persona>/<spec>.{json,yaml,prompt.md} (shared)
- generated/salva-personas/<persona>/salva.{json,yaml,prompt.md} (private)
Source layout:
- personas/<persona>/general.md, <spec>.md (committed)
- personalized/<persona>/salva.md (gitignored)
- personalized/skills/<skill>/SKILL.md (gitignored)
- personalized/_user_context.md (gitignored)
Adds /personalized/ to .gitignore. Documents the new layout in CLAUDE.md
and README.md. Maps linkedin-content-strategy → herald/forge/frodo/ghost
in the default skill persona map.
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -10,3 +10,9 @@ __pycache__/
|
|||||||
/agents-claude-archive/
|
/agents-claude-archive/
|
||||||
/skills-feynman-archive/
|
/skills-feynman-archive/
|
||||||
/agents-feynman-archive/
|
/agents-feynman-archive/
|
||||||
|
|
||||||
|
# User-personalized variant source — gitignored so the shared persona library
|
||||||
|
# stays user-agnostic. Each user keeps their own salva.md (or whatever
|
||||||
|
# variant name they configure) and _user_context.md locally. Override the
|
||||||
|
# folder name via config.user.personalized_source_folder.
|
||||||
|
/personalized/
|
||||||
|
|||||||
19
CLAUDE.md
19
CLAUDE.md
@@ -52,6 +52,13 @@ Optional: `cp config.example.yaml config.yaml` for dynamic variable injection. B
|
|||||||
- `trigger_index.json` — keyword→persona routing for multi-agent auto-switching
|
- `trigger_index.json` — keyword→persona routing for multi-agent auto-switching
|
||||||
- `skills_index.json` — all shared skills mapped to personas with metadata
|
- `skills_index.json` — all shared skills mapped to personas with metadata
|
||||||
|
|
||||||
|
**Output layout — personalized variant routing**: User-personalized variants (default trigger: `salva.md`) are written to a separate top-level folder under `generated/`, keeping `generated/<persona>/` free of per-user content:
|
||||||
|
|
||||||
|
- `generated/<persona>/general.*` + `generated/<persona>/<specialization>.*` — generic & domain variants
|
||||||
|
- `generated/<user-slug>-personas/<persona>/salva.*` — personalized variants (user-slug = slugified `config.user.name`, e.g. `salva-personas`, `alice-smith-personas`)
|
||||||
|
|
||||||
|
The build is **user-agnostic**: both the trigger variant filename(s) and the output folder name are config-driven. See `config.example.yaml` `user.personalized_variants` and `user.personalized_output_folder`. Defaults preserve the existing `salva.md` convention with auto-derived folder name. All install_*() functions iterate via `iter_persona_output_dirs(output_dir)` which yields persona dirs from BOTH locations transparently.
|
||||||
|
|
||||||
**Skill injection**: Build auto-maps skills from `_shared/skills/` to personas via domain mapping. Each persona's JSON/YAML output includes a `skills` array listing applicable shared skills.
|
**Skill injection**: Build auto-maps skills from `_shared/skills/` to personas via domain mapping. Each persona's JSON/YAML output includes a `skills` array listing applicable shared skills.
|
||||||
|
|
||||||
## Install to Platforms
|
## Install to Platforms
|
||||||
@@ -77,3 +84,15 @@ python3 build.py --install all # all platforms at once
|
|||||||
- Section headers use `## ` (H2) — the build parser splits on these
|
- Section headers use `## ` (H2) — the build parser splits on these
|
||||||
- Turkish honorific titles ("Hitap") are used for `address_to` fields
|
- Turkish honorific titles ("Hitap") are used for `address_to` fields
|
||||||
- `config.yaml` must never be committed (contains personal infrastructure details)
|
- `config.yaml` must never be committed (contains personal infrastructure details)
|
||||||
|
|
||||||
|
## Scope Boundaries (don't touch)
|
||||||
|
|
||||||
|
- **`personas/_shared/paperclip-agents/`** — Belongs to a separate Paperclip company project (Odin, Thor, Freya, Loki, Idunn, etc.). Do NOT edit Paperclip agent SOUL/AGENTS/hermes-config files when working on persona changes. Mirrored here for build-pipeline integration only.
|
||||||
|
- **`personas/_shared/paperclip-skills/`** — Same: separate-project artifacts. New skills for our personas go in `personas/_shared/skills/` (not `paperclip-skills/`).
|
||||||
|
- **`personas/_shared/community-skills/`** — Vendor-synced from skills.sh marketplace. Do NOT modify in place — patches will be lost on resync. Add domain-specific guidance as a new skill in `_shared/skills/` instead.
|
||||||
|
- **`personas/_shared/feynman-skills/`, `agents-*-archive/`, `skills-*-archive/`** — Archived/imported from upstream Feynman project. Read-only reference unless explicitly working on Feynman integration.
|
||||||
|
|
||||||
|
For user-personalized changes, the canonical surfaces are:
|
||||||
|
1. `personas/_user_context.md` — shared user context for all `salva.md` variants
|
||||||
|
2. `personas/<codename>/salva.md` — per-persona user-personalized variant
|
||||||
|
3. `personas/_shared/skills/<skill-name>/SKILL.md` — new shared skills (persona-mappable, not vendor-synced)
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -222,11 +222,14 @@ sources/
|
|||||||
|
|
||||||
### Variant Types
|
### Variant Types
|
||||||
|
|
||||||
| Type | Purpose | Example |
|
| Type | Purpose | Example | Output location |
|
||||||
|------|---------|---------|
|
|------|---------|---------|-----------------|
|
||||||
| `general.md` | Base persona — works standalone for any user | `neo/general.md` |
|
| `general.md` | Base persona — works standalone for any user | `neo/general.md` | `generated/<persona>/` |
|
||||||
| `<specialization>.md` | Domain deep-dive narrowing the persona's focus | `neo/redteam.md` |
|
| `<specialization>.md` | Domain deep-dive narrowing the persona's focus | `neo/redteam.md` | `generated/<persona>/` |
|
||||||
| `salva.md` | User-personalized — references specific projects, data, tools | `neo/salva.md` |
|
| `salva.md` (personalized) | User-personalized — references specific projects, data, tools | `neo/salva.md` | `generated/<user-slug>-personas/<persona>/` |
|
||||||
|
|
||||||
|
**Personalized variant routing (config-driven, user-agnostic):**
|
||||||
|
The build keeps user-specific output in a separate top-level folder so the main `generated/<persona>/` directories stay clean. The folder name is derived from `config.user.name` (slugified, `-personas` suffix) — e.g. `user.name: "Salva"` → `generated/salva-personas/<persona>/salva.*`. Both the trigger variant filename(s) and the output folder name are overridable via `config.user.personalized_variants` / `config.user.personalized_output_folder`. Defaults preserve the historical `salva.md` convention.
|
||||||
|
|
||||||
## Prompt Format
|
## Prompt Format
|
||||||
|
|
||||||
|
|||||||
226
build.py
226
build.py
@@ -185,6 +185,121 @@ def parse_persona_md(filepath: Path, flat_config: dict) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# User-personalized variants get routed to a separate top-level folder so
|
||||||
|
# generated/<persona>/ stays clean of per-user content. Both the variant
|
||||||
|
# filename(s) treated as personalized AND the output folder name are
|
||||||
|
# config-driven via config.user — the build is user-agnostic.
|
||||||
|
#
|
||||||
|
# Defaults preserve the historical convention (variant `salva.md`) but the
|
||||||
|
# output folder is derived from `config.user.name` (slugified, "-personas"
|
||||||
|
# suffix). Override either by setting config.user.personalized_variants and/or
|
||||||
|
# config.user.personalized_output_folder.
|
||||||
|
PERSONALIZED_VARIANT_DEFAULT = "salva"
|
||||||
|
PERSONALIZED_OUTPUT_FALLBACK = "personalized-personas"
|
||||||
|
PERSONALIZED_SOURCE_DEFAULT = "personalized"
|
||||||
|
|
||||||
|
# Set by main() from config; helpers and install_* read this at runtime.
|
||||||
|
_RUNTIME: dict = {
|
||||||
|
"personalized_variants": {PERSONALIZED_VARIANT_DEFAULT},
|
||||||
|
"personalized_dirname": PERSONALIZED_OUTPUT_FALLBACK,
|
||||||
|
"personalized_source_dirname": PERSONALIZED_SOURCE_DEFAULT,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_personalized_variants(config: dict) -> set[str]:
|
||||||
|
"""Variant filenames (without .md) treated as user-personalized.
|
||||||
|
|
||||||
|
Reads config.user.personalized_variants. Accepts a string or list.
|
||||||
|
Falls back to {"salva"} for backward-compat with the established
|
||||||
|
`<persona>/salva.md` convention.
|
||||||
|
"""
|
||||||
|
raw = (config.get("user") or {}).get("personalized_variants")
|
||||||
|
if raw is None or raw == "":
|
||||||
|
return {PERSONALIZED_VARIANT_DEFAULT}
|
||||||
|
if isinstance(raw, str):
|
||||||
|
return {raw}
|
||||||
|
items = {str(v).strip() for v in raw if str(v).strip()}
|
||||||
|
return items or {PERSONALIZED_VARIANT_DEFAULT}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_personalized_output_dirname(config: dict) -> str:
|
||||||
|
"""Top-level folder name for personalized variants under output_dir.
|
||||||
|
|
||||||
|
Priority:
|
||||||
|
1. config.user.personalized_output_folder (explicit override)
|
||||||
|
2. slugify(config.user.name) + "-personas" (e.g. "salva" → "salva-personas")
|
||||||
|
3. PERSONALIZED_OUTPUT_FALLBACK ("personalized-personas")
|
||||||
|
"""
|
||||||
|
user = config.get("user") or {}
|
||||||
|
explicit = user.get("personalized_output_folder")
|
||||||
|
if explicit:
|
||||||
|
return str(explicit).strip() or PERSONALIZED_OUTPUT_FALLBACK
|
||||||
|
name = (user.get("name") or "").strip().lower()
|
||||||
|
if name and name not in ("your name", "default"):
|
||||||
|
slug = re.sub(r"[^a-z0-9]+", "-", name).strip("-")
|
||||||
|
if slug:
|
||||||
|
return f"{slug}-personas"
|
||||||
|
return PERSONALIZED_OUTPUT_FALLBACK
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_personalized_source_dirname(config: dict) -> str:
|
||||||
|
"""Source dir (under repo root) holding user-personalized .md variants.
|
||||||
|
|
||||||
|
Layout: <root>/<personalized_source>/<codename>/<variant>.md
|
||||||
|
The shared persona library stays in personas/<codename>/. Personalized
|
||||||
|
.md files live in this separate root so the public/shared repo can
|
||||||
|
gitignore the user-specific source content.
|
||||||
|
|
||||||
|
Default: "personalized" (literal — gitignorable, doesn't depend on user
|
||||||
|
name). Override via config.user.personalized_source_folder.
|
||||||
|
"""
|
||||||
|
user = config.get("user") or {}
|
||||||
|
explicit = user.get("personalized_source_folder")
|
||||||
|
if explicit:
|
||||||
|
return str(explicit).strip() or PERSONALIZED_SOURCE_DEFAULT
|
||||||
|
return PERSONALIZED_SOURCE_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
def configure_runtime(config: dict) -> None:
|
||||||
|
"""Cache personalization settings in module state for helpers/installers.
|
||||||
|
|
||||||
|
Called once from main() after config is loaded. Helpers like
|
||||||
|
iter_persona_output_dirs() and build_persona() read _RUNTIME without
|
||||||
|
needing the config threaded through every signature.
|
||||||
|
"""
|
||||||
|
_RUNTIME["personalized_variants"] = resolve_personalized_variants(config)
|
||||||
|
_RUNTIME["personalized_dirname"] = resolve_personalized_output_dirname(config)
|
||||||
|
_RUNTIME["personalized_source_dirname"] = resolve_personalized_source_dirname(config)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_persona_output_dirs(output_dir: Path):
|
||||||
|
"""Yield every persona output directory, from both layouts.
|
||||||
|
|
||||||
|
Iterates:
|
||||||
|
1. output_dir/<codename>/ — general + non-personalized
|
||||||
|
2. output_dir/<personalized_dirname>/<codename>/ — user-personalized variants
|
||||||
|
|
||||||
|
The personalized_dirname is read from _RUNTIME (set by configure_runtime()).
|
||||||
|
Skips dotfiles, underscore-prefixed special dirs, and the personalized
|
||||||
|
container itself in pass 1.
|
||||||
|
"""
|
||||||
|
personalized_dirname = _RUNTIME["personalized_dirname"]
|
||||||
|
skip_names = {personalized_dirname}
|
||||||
|
for persona_dir in sorted(output_dir.iterdir()):
|
||||||
|
if not persona_dir.is_dir():
|
||||||
|
continue
|
||||||
|
if persona_dir.name.startswith(("_", ".")) or persona_dir.name in skip_names:
|
||||||
|
continue
|
||||||
|
yield persona_dir
|
||||||
|
|
||||||
|
personalized_root = output_dir / personalized_dirname
|
||||||
|
if personalized_root.exists():
|
||||||
|
for persona_dir in sorted(personalized_root.iterdir()):
|
||||||
|
if not persona_dir.is_dir() or persona_dir.name.startswith(("_", ".")):
|
||||||
|
continue
|
||||||
|
yield persona_dir
|
||||||
|
|
||||||
|
|
||||||
def build_persona(
|
def build_persona(
|
||||||
persona_dir: Path,
|
persona_dir: Path,
|
||||||
output_dir: Path,
|
output_dir: Path,
|
||||||
@@ -199,8 +314,6 @@ def build_persona(
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
persona_name = persona_dir.name
|
persona_name = persona_dir.name
|
||||||
out_path = output_dir / persona_name
|
|
||||||
out_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Load _meta.yaml if exists
|
# Load _meta.yaml if exists
|
||||||
meta_file = persona_dir / "_meta.yaml"
|
meta_file = persona_dir / "_meta.yaml"
|
||||||
@@ -226,6 +339,14 @@ def build_persona(
|
|||||||
if not parsed:
|
if not parsed:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Per-variant output path: personalized variants land in a separate
|
||||||
|
# top-level folder (config.user-driven; see resolve_* helpers above).
|
||||||
|
if variant in _RUNTIME["personalized_variants"]:
|
||||||
|
out_path = output_dir / _RUNTIME["personalized_dirname"] / persona_name
|
||||||
|
else:
|
||||||
|
out_path = output_dir / persona_name
|
||||||
|
out_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Build output object
|
# Build output object
|
||||||
output = {
|
output = {
|
||||||
**meta,
|
**meta,
|
||||||
@@ -372,6 +493,7 @@ DEFAULT_SKILL_PERSONA_MAP = {
|
|||||||
"pentest-reporter": ["neo", "phantom", "bastion"],
|
"pentest-reporter": ["neo", "phantom", "bastion"],
|
||||||
# Marketing / business → personas
|
# Marketing / business → personas
|
||||||
"marketing-strategist": ["herald", "ledger"],
|
"marketing-strategist": ["herald", "ledger"],
|
||||||
|
"linkedin-content-strategy": ["herald", "forge", "frodo", "ghost"],
|
||||||
# Document processing → personas
|
# Document processing → personas
|
||||||
"image-ocr": ["oracle", "scribe"],
|
"image-ocr": ["oracle", "scribe"],
|
||||||
"mistral-ocr": ["oracle", "scribe"],
|
"mistral-ocr": ["oracle", "scribe"],
|
||||||
@@ -751,6 +873,53 @@ def build_skills_index(shared_dir: Path, config: dict = None) -> dict:
|
|||||||
"has_references": (skill_dir / "references").is_dir(),
|
"has_references": (skill_dir / "references").is_dir(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Also index user-personalized skills from <repo_root>/<personalized_source>/skills/
|
||||||
|
# if present. Merged into the same "skills" bucket so persona JSON output
|
||||||
|
# includes them just like shared skills. Personalized skills typically
|
||||||
|
# contain user-private data (analytics, account-specific playbooks) and
|
||||||
|
# live in a gitignored folder so the public repo stays user-agnostic.
|
||||||
|
# shared_dir is personas/_shared, so repo_root is shared_dir.parent.parent.
|
||||||
|
repo_root = shared_dir.parent.parent
|
||||||
|
personalized_root = repo_root / _RUNTIME["personalized_source_dirname"]
|
||||||
|
personalized_skills_dir = personalized_root / "skills"
|
||||||
|
if personalized_skills_dir.exists() and personalized_skills_dir.is_dir():
|
||||||
|
for skill_dir in sorted(personalized_skills_dir.iterdir()):
|
||||||
|
if not skill_dir.is_dir():
|
||||||
|
continue
|
||||||
|
skill_md = skill_dir / "SKILL.md"
|
||||||
|
if not skill_md.exists():
|
||||||
|
continue
|
||||||
|
skill_meta = parse_skill_frontmatter(skill_md)
|
||||||
|
inferred_personas = infer_personas_from_skill_metadata(
|
||||||
|
skill_dir.name, skill_meta
|
||||||
|
)
|
||||||
|
configured_personas = skill_map.get(skill_dir.name, [])
|
||||||
|
merged_personas = sorted(
|
||||||
|
set(configured_personas).union(inferred_personas)
|
||||||
|
)
|
||||||
|
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": merged_personas,
|
||||||
|
"summary": first_line,
|
||||||
|
"domain": str(skill_meta.get("domain", "")),
|
||||||
|
"subdomain": str(skill_meta.get("subdomain", "")),
|
||||||
|
"tags": skill_meta.get("tags", []),
|
||||||
|
"source": "personalized",
|
||||||
|
"mapped_by": {
|
||||||
|
"explicit": configured_personas,
|
||||||
|
"inferred": inferred_personas,
|
||||||
|
},
|
||||||
|
"has_references": (skill_dir / "references").is_dir(),
|
||||||
|
}
|
||||||
|
|
||||||
# Index paperclip-skills
|
# Index paperclip-skills
|
||||||
pskills_dir = shared_dir / "paperclip-skills"
|
pskills_dir = shared_dir / "paperclip-skills"
|
||||||
if pskills_dir.exists():
|
if pskills_dir.exists():
|
||||||
@@ -1155,9 +1324,7 @@ def install_claude(output_dir: Path):
|
|||||||
stale.unlink()
|
stale.unlink()
|
||||||
orphans_removed += 1
|
orphans_removed += 1
|
||||||
|
|
||||||
for persona_dir in sorted(output_dir.iterdir()):
|
for persona_dir in iter_persona_output_dirs(output_dir):
|
||||||
if not persona_dir.is_dir() or persona_dir.name.startswith("_"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Install slash commands for all variants (general + specializations).
|
# 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"):
|
||||||
@@ -1401,9 +1568,7 @@ def install_antigravity(output_dir: Path):
|
|||||||
ag_dir = Path.home() / ".config" / "antigravity" / "personas"
|
ag_dir = Path.home() / ".config" / "antigravity" / "personas"
|
||||||
ag_dir.mkdir(parents=True, exist_ok=True)
|
ag_dir.mkdir(parents=True, exist_ok=True)
|
||||||
count = 0
|
count = 0
|
||||||
for persona_dir in sorted(output_dir.iterdir()):
|
for persona_dir in iter_persona_output_dirs(output_dir):
|
||||||
if not persona_dir.is_dir() or persona_dir.name.startswith("_"):
|
|
||||||
continue
|
|
||||||
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
|
||||||
codename = persona_dir.name
|
codename = persona_dir.name
|
||||||
@@ -1612,9 +1777,7 @@ def install_opencode(
|
|||||||
# Emit one agent file per variant. General → <codename>.md (picker-visible).
|
# Emit one agent file per variant. General → <codename>.md (picker-visible).
|
||||||
# Non-general → <codename>-<variant>.md with hidden:true so it's
|
# Non-general → <codename>-<variant>.md with hidden:true so it's
|
||||||
# task-dispatchable by name without cluttering the picker.
|
# task-dispatchable by name without cluttering the picker.
|
||||||
for persona_dir in sorted(output_dir.iterdir()):
|
for persona_dir in iter_persona_output_dirs(output_dir):
|
||||||
if not persona_dir.is_dir() or persona_dir.name.startswith("_"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
for variant_json in sorted(persona_dir.glob("*.json")):
|
for variant_json in sorted(persona_dir.glob("*.json")):
|
||||||
try:
|
try:
|
||||||
@@ -1929,9 +2092,7 @@ def install_feynman(
|
|||||||
emitted_agents: set[str] = set()
|
emitted_agents: set[str] = set()
|
||||||
ident_re = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
ident_re = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
||||||
|
|
||||||
for persona_dir in sorted(output_dir.iterdir()):
|
for persona_dir in iter_persona_output_dirs(output_dir):
|
||||||
if not persona_dir.is_dir() or persona_dir.name.startswith("_"):
|
|
||||||
continue
|
|
||||||
for variant_json in sorted(persona_dir.glob("*.json")):
|
for variant_json in sorted(persona_dir.glob("*.json")):
|
||||||
try:
|
try:
|
||||||
data = json.loads(variant_json.read_text(encoding="utf-8"))
|
data = json.loads(variant_json.read_text(encoding="utf-8"))
|
||||||
@@ -2181,9 +2342,7 @@ def install_gemini(output_dir: Path):
|
|||||||
gems_dir = output_dir / "_gems"
|
gems_dir = output_dir / "_gems"
|
||||||
gems_dir.mkdir(parents=True, exist_ok=True)
|
gems_dir.mkdir(parents=True, exist_ok=True)
|
||||||
count = 0
|
count = 0
|
||||||
for persona_dir in sorted(output_dir.iterdir()):
|
for persona_dir in iter_persona_output_dirs(output_dir):
|
||||||
if not persona_dir.is_dir() or persona_dir.name.startswith("_"):
|
|
||||||
continue
|
|
||||||
for json_file in persona_dir.glob("*.json"):
|
for json_file in persona_dir.glob("*.json"):
|
||||||
data = json.loads(json_file.read_text(encoding="utf-8"))
|
data = json.loads(json_file.read_text(encoding="utf-8"))
|
||||||
variant = data.get("variant", json_file.stem)
|
variant = data.get("variant", json_file.stem)
|
||||||
@@ -2252,9 +2411,7 @@ def install_paperclip(output_dir: Path, personas_dir: Path, shared_dir: Path | N
|
|||||||
agent_count = 0
|
agent_count = 0
|
||||||
skill_count = 0
|
skill_count = 0
|
||||||
|
|
||||||
for persona_dir in sorted(output_dir.iterdir()):
|
for persona_dir in iter_persona_output_dirs(output_dir):
|
||||||
if not persona_dir.is_dir() or persona_dir.name.startswith("_"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
general_json = persona_dir / "general.json"
|
general_json = persona_dir / "general.json"
|
||||||
general_prompt = persona_dir / "general.prompt.md"
|
general_prompt = persona_dir / "general.prompt.md"
|
||||||
@@ -2449,9 +2606,7 @@ def install_openclaw(output_dir: Path):
|
|||||||
personas_dir.mkdir(parents=True, exist_ok=True)
|
personas_dir.mkdir(parents=True, exist_ok=True)
|
||||||
count = 0
|
count = 0
|
||||||
identity_sections = []
|
identity_sections = []
|
||||||
for persona_dir in sorted(output_dir.iterdir()):
|
for persona_dir in iter_persona_output_dirs(output_dir):
|
||||||
if not persona_dir.is_dir() or persona_dir.name.startswith("_"):
|
|
||||||
continue
|
|
||||||
general_prompt = persona_dir / "general.prompt.md"
|
general_prompt = persona_dir / "general.prompt.md"
|
||||||
if not general_prompt.exists():
|
if not general_prompt.exists():
|
||||||
continue
|
continue
|
||||||
@@ -2602,13 +2757,36 @@ def main():
|
|||||||
config = load_config(root)
|
config = load_config(root)
|
||||||
flat_config = flatten_config(config) if config else {}
|
flat_config = flatten_config(config) if config else {}
|
||||||
|
|
||||||
# Find all persona directories
|
# Cache user-personalization runtime: which variant filenames are treated
|
||||||
|
# as personalized, and which top-level folder under generated/ collects
|
||||||
|
# them. Helpers (iter_persona_output_dirs, build_persona) read these.
|
||||||
|
configure_runtime(config)
|
||||||
|
|
||||||
|
# Find all persona directories from BOTH the shared library (personas/)
|
||||||
|
# AND the user-personalized source (e.g. personalized/ — gitignored).
|
||||||
|
# Same codename can appear in both; build_persona() processes whatever
|
||||||
|
# .md files exist in the dir it's given, and routes personalized variants
|
||||||
|
# to the separate output folder via _RUNTIME.
|
||||||
persona_dirs = [
|
persona_dirs = [
|
||||||
d
|
d
|
||||||
for d in sorted(personas_dir.iterdir())
|
for d in sorted(personas_dir.iterdir())
|
||||||
if d.is_dir() and not d.name.startswith((".", "_"))
|
if d.is_dir() and not d.name.startswith((".", "_"))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
personalized_source_dir = root / _RUNTIME["personalized_source_dirname"]
|
||||||
|
if personalized_source_dir.exists() and personalized_source_dir.is_dir():
|
||||||
|
personalized_persona_dirs = [
|
||||||
|
d
|
||||||
|
for d in sorted(personalized_source_dir.iterdir())
|
||||||
|
if d.is_dir() and not d.name.startswith((".", "_"))
|
||||||
|
]
|
||||||
|
persona_dirs.extend(personalized_persona_dirs)
|
||||||
|
if personalized_persona_dirs:
|
||||||
|
print(
|
||||||
|
f"Personalized source: {personalized_source_dir} "
|
||||||
|
f"({len(personalized_persona_dirs)} codenames)"
|
||||||
|
)
|
||||||
|
|
||||||
if not persona_dirs:
|
if not persona_dirs:
|
||||||
print("No persona directories found.")
|
print("No persona directories found.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -18,6 +18,19 @@ user:
|
|||||||
casual_language: "en"
|
casual_language: "en"
|
||||||
technical_language: "en"
|
technical_language: "en"
|
||||||
|
|
||||||
|
# Personalized variant routing (optional). Variants matched here are
|
||||||
|
# written to generated/<personalized_output_folder>/<persona>/ instead
|
||||||
|
# of generated/<persona>/, keeping the main persona folders clean of
|
||||||
|
# per-user content.
|
||||||
|
#
|
||||||
|
# Defaults if both are omitted:
|
||||||
|
# personalized_variants: ["salva"] (existing convention)
|
||||||
|
# personalized_output_folder: slugify(user.name) + "-personas"
|
||||||
|
# e.g. "Alice Smith" → "alice-smith-personas"
|
||||||
|
# (or "personalized-personas" if name unset)
|
||||||
|
personalized_variants: ["salva"]
|
||||||
|
# personalized_output_folder: "my-personas" # explicit override
|
||||||
|
|
||||||
# ─── Infrastructure ─────────────────────────────────────────────
|
# ─── Infrastructure ─────────────────────────────────────────────
|
||||||
infrastructure:
|
infrastructure:
|
||||||
servers: []
|
servers: []
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ _Auto-generated by build.py | User: Salva_
|
|||||||
## arbiter — International Law & War Crimes Specialist
|
## arbiter — International Law & War Crimes Specialist
|
||||||
- **Domain:** law
|
- **Domain:** law
|
||||||
- **Hitap:** Kadı
|
- **Hitap:** Kadı
|
||||||
- **Variants:** general, salva, sanctions
|
- **Variants:** general, sanctions
|
||||||
- **Depth:** 2,880 words, 6 sections
|
- **Depth:** 2,880 words, 6 sections
|
||||||
- **Escalates to:** frodo, marshal, tribune, chronos
|
- **Escalates to:** frodo, marshal, tribune, chronos
|
||||||
|
|
||||||
## architect — DevOps & Systems Engineer
|
## architect — DevOps & Systems Engineer
|
||||||
- **Domain:** engineering
|
- **Domain:** engineering
|
||||||
- **Hitap:** Mimar Ağa
|
- **Hitap:** Mimar Ağa
|
||||||
- **Variants:** general, salva
|
- **Variants:** general
|
||||||
- **Depth:** 1,526 words, 6 sections
|
- **Depth:** 1,526 words, 6 sections
|
||||||
- **Escalates to:** forge, vortex, neo
|
- **Escalates to:** forge, vortex, neo
|
||||||
|
|
||||||
@@ -26,14 +26,14 @@ _Auto-generated by build.py | User: Salva_
|
|||||||
## centurion — Military History & War Analysis Specialist
|
## centurion — Military History & War Analysis Specialist
|
||||||
- **Domain:** military
|
- **Domain:** military
|
||||||
- **Hitap:** Vakanüvis
|
- **Hitap:** Vakanüvis
|
||||||
- **Variants:** general, ottoman-wars, salva, ukraine-russia
|
- **Variants:** general, ottoman-wars, ukraine-russia
|
||||||
- **Depth:** 2,269 words, 6 sections
|
- **Depth:** 2,269 words, 6 sections
|
||||||
- **Escalates to:** marshal, warden, chronos, corsair
|
- **Escalates to:** marshal, warden, chronos, corsair
|
||||||
|
|
||||||
## chronos — World History & Civilization Analysis Specialist
|
## chronos — World History & Civilization Analysis Specialist
|
||||||
- **Domain:** history
|
- **Domain:** history
|
||||||
- **Hitap:** Tarihçibaşı
|
- **Hitap:** Tarihçibaşı
|
||||||
- **Variants:** general, salva
|
- **Variants:** general
|
||||||
- **Depth:** 2,581 words, 6 sections
|
- **Depth:** 2,581 words, 6 sections
|
||||||
- **Escalates to:** centurion, scholar, sage, tribune, scribe
|
- **Escalates to:** centurion, scholar, sage, tribune, scribe
|
||||||
|
|
||||||
@@ -47,84 +47,84 @@ _Auto-generated by build.py | User: Salva_
|
|||||||
## corsair — Special Operations & Irregular Warfare Specialist
|
## corsair — Special Operations & Irregular Warfare Specialist
|
||||||
- **Domain:** military
|
- **Domain:** military
|
||||||
- **Hitap:** Akıncı
|
- **Hitap:** Akıncı
|
||||||
- **Variants:** general, proxy-warfare, salva
|
- **Variants:** general, proxy-warfare
|
||||||
- **Depth:** 2,352 words, 6 sections
|
- **Depth:** 2,352 words, 6 sections
|
||||||
- **Escalates to:** marshal, wraith, centurion, warden
|
- **Escalates to:** marshal, wraith, centurion, warden
|
||||||
|
|
||||||
## echo — SIGINT / COMINT / ELINT Specialist
|
## echo — SIGINT / COMINT / ELINT Specialist
|
||||||
- **Domain:** intelligence
|
- **Domain:** intelligence
|
||||||
- **Hitap:** Kulakçı
|
- **Hitap:** Kulakçı
|
||||||
- **Variants:** electronic-order-of-battle, general, nsa-sigint, salva
|
- **Variants:** electronic-order-of-battle, general, nsa-sigint
|
||||||
- **Depth:** 2,504 words, 6 sections
|
- **Depth:** 2,504 words, 6 sections
|
||||||
- **Escalates to:** cipher, vortex, frodo, wraith, sentinel
|
- **Escalates to:** cipher, vortex, frodo, wraith, sentinel
|
||||||
|
|
||||||
## forge — Software Development & AI/ML Engineer
|
## forge — Software Development & AI/ML Engineer
|
||||||
- **Domain:** engineering
|
- **Domain:** engineering
|
||||||
- **Hitap:** Demirci
|
- **Hitap:** Demirci
|
||||||
- **Variants:** agent-dev, frontend-design, general, salva
|
- **Variants:** agent-dev, frontend-design, general
|
||||||
- **Depth:** 1,882 words, 6 sections
|
- **Depth:** 1,882 words, 6 sections
|
||||||
- **Escalates to:** architect, cipher, sentinel
|
- **Escalates to:** architect, cipher, sentinel
|
||||||
|
|
||||||
## frodo — Strategic Intelligence Analyst
|
## frodo — Strategic Intelligence Analyst
|
||||||
- **Domain:** intelligence
|
- **Domain:** intelligence
|
||||||
- **Hitap:** Müsteşar
|
- **Hitap:** Müsteşar
|
||||||
- **Variants:** africa, china, energy-geopolitics, general, india, iran, middle-east, nato-alliance, nuclear, pakistan, russia, salva, turkey
|
- **Variants:** africa, china, energy-geopolitics, general, india, iran, middle-east, nato-alliance, nuclear, pakistan, russia, turkey
|
||||||
- **Depth:** 1,776 words, 6 sections
|
- **Depth:** 1,776 words, 6 sections
|
||||||
- **Escalates to:** oracle, ghost, wraith, echo, sentinel, marshal
|
- **Escalates to:** oracle, ghost, wraith, echo, sentinel, marshal
|
||||||
|
|
||||||
## gambit — Chess & Strategic Thinking Specialist
|
## gambit — Chess & Strategic Thinking Specialist
|
||||||
- **Domain:** strategy
|
- **Domain:** strategy
|
||||||
- **Hitap:** Vezir
|
- **Hitap:** Vezir
|
||||||
- **Variants:** general, salva
|
- **Variants:** general
|
||||||
- **Depth:** 2,548 words, 6 sections
|
- **Depth:** 2,548 words, 6 sections
|
||||||
- **Escalates to:** marshal, sage, tribune, corsair
|
- **Escalates to:** marshal, sage, tribune, corsair
|
||||||
|
|
||||||
## ghost — PSYOP & Information Warfare Specialist
|
## ghost — PSYOP & Information Warfare Specialist
|
||||||
- **Domain:** intelligence
|
- **Domain:** intelligence
|
||||||
- **Hitap:** Propagandist
|
- **Hitap:** Propagandist
|
||||||
- **Variants:** cognitive-warfare, general, russian-info-war, salva
|
- **Variants:** cognitive-warfare, general, russian-info-war
|
||||||
- **Depth:** 2,117 words, 6 sections
|
- **Depth:** 2,117 words, 6 sections
|
||||||
- **Escalates to:** oracle, frodo, herald, wraith
|
- **Escalates to:** oracle, frodo, herald, wraith
|
||||||
|
|
||||||
## herald — Media Analysis & Strategic Communication Specialist
|
## herald — Media Analysis & Strategic Communication Specialist
|
||||||
- **Domain:** media
|
- **Domain:** media
|
||||||
- **Hitap:** Münadi
|
- **Hitap:** Münadi
|
||||||
- **Variants:** general, salva
|
- **Variants:** general
|
||||||
- **Depth:** 2,827 words, 6 sections
|
- **Depth:** 2,827 words, 6 sections
|
||||||
- **Escalates to:** ghost, polyglot, oracle, frodo
|
- **Escalates to:** ghost, polyglot, oracle, frodo
|
||||||
|
|
||||||
## ledger — Economic Intelligence & FININT Specialist
|
## ledger — Economic Intelligence & FININT Specialist
|
||||||
- **Domain:** economics
|
- **Domain:** economics
|
||||||
- **Hitap:** Defterdar
|
- **Hitap:** Defterdar
|
||||||
- **Variants:** general, salva, sanctions-evasion
|
- **Variants:** general, sanctions-evasion
|
||||||
- **Depth:** 2,847 words, 6 sections
|
- **Depth:** 2,847 words, 6 sections
|
||||||
- **Escalates to:** arbiter, frodo, tribune, scribe
|
- **Escalates to:** arbiter, frodo, tribune, scribe
|
||||||
|
|
||||||
## marshal — Military Doctrine & Strategy Specialist
|
## marshal — Military Doctrine & Strategy Specialist
|
||||||
- **Domain:** military
|
- **Domain:** military
|
||||||
- **Hitap:** Mareşal
|
- **Hitap:** Mareşal
|
||||||
- **Variants:** chinese-doctrine, general, hybrid-warfare, iranian-military, nato-doctrine, russian-doctrine, salva, turkish-doctrine, wargaming
|
- **Variants:** chinese-doctrine, general, hybrid-warfare, iranian-military, nato-doctrine, russian-doctrine, turkish-doctrine, wargaming
|
||||||
- **Depth:** 1,760 words, 6 sections
|
- **Depth:** 1,760 words, 6 sections
|
||||||
- **Escalates to:** centurion, warden, corsair, frodo
|
- **Escalates to:** centurion, warden, corsair, frodo
|
||||||
|
|
||||||
## medic — Biomedical & CBRN Specialist
|
## medic — Biomedical & CBRN Specialist
|
||||||
- **Domain:** science
|
- **Domain:** science
|
||||||
- **Hitap:** Hekim Başı
|
- **Hitap:** Hekim Başı
|
||||||
- **Variants:** cbrn-defense, general, salva
|
- **Variants:** cbrn-defense, general
|
||||||
- **Depth:** 2,309 words, 6 sections
|
- **Depth:** 2,309 words, 6 sections
|
||||||
- **Escalates to:** warden, frodo, marshal, corsair
|
- **Escalates to:** warden, frodo, marshal, corsair
|
||||||
|
|
||||||
## neo — Red Team Lead / Exploit Developer
|
## neo — Red Team Lead / Exploit Developer
|
||||||
- **Domain:** cybersecurity
|
- **Domain:** cybersecurity
|
||||||
- **Hitap:** Sıfırıncı Gün
|
- **Hitap:** Sıfırıncı Gün
|
||||||
- **Variants:** exploit-dev, general, mobile-security, redteam, salva, social-engineering, wireless
|
- **Variants:** exploit-dev, general, mobile-security, redteam, social-engineering, wireless
|
||||||
- **Depth:** 1,090 words, 6 sections
|
- **Depth:** 1,090 words, 6 sections
|
||||||
- **Escalates to:** bastion, phantom, specter, vortex, sentinel
|
- **Escalates to:** bastion, phantom, specter, vortex, sentinel
|
||||||
|
|
||||||
## oracle — OSINT & Digital Intelligence Specialist
|
## oracle — OSINT & Digital Intelligence Specialist
|
||||||
- **Domain:** intelligence
|
- **Domain:** intelligence
|
||||||
- **Hitap:** Kaşif
|
- **Hitap:** Kaşif
|
||||||
- **Variants:** crypto-osint, general, salva, source-verification
|
- **Variants:** crypto-osint, general, source-verification
|
||||||
- **Depth:** 1,880 words, 6 sections
|
- **Depth:** 1,880 words, 6 sections
|
||||||
- **Escalates to:** ghost, sentinel, frodo, herald
|
- **Escalates to:** ghost, sentinel, frodo, herald
|
||||||
|
|
||||||
@@ -138,35 +138,35 @@ _Auto-generated by build.py | User: Salva_
|
|||||||
## polyglot — Linguistics & LINGINT Specialist
|
## polyglot — Linguistics & LINGINT Specialist
|
||||||
- **Domain:** linguistics
|
- **Domain:** linguistics
|
||||||
- **Hitap:** Tercüman-ı Divan
|
- **Hitap:** Tercüman-ı Divan
|
||||||
- **Variants:** arabic, general, russian, salva, swahili
|
- **Variants:** arabic, general, russian, swahili
|
||||||
- **Depth:** 2,308 words, 6 sections
|
- **Depth:** 2,308 words, 6 sections
|
||||||
- **Escalates to:** frodo, ghost, herald, scholar
|
- **Escalates to:** frodo, ghost, herald, scholar
|
||||||
|
|
||||||
## sage — Philosophy, Psychology & Power Theory Specialist
|
## sage — Philosophy, Psychology & Power Theory Specialist
|
||||||
- **Domain:** humanities
|
- **Domain:** humanities
|
||||||
- **Hitap:** Arif
|
- **Hitap:** Arif
|
||||||
- **Variants:** general, salva
|
- **Variants:** general
|
||||||
- **Depth:** 2,132 words, 6 sections
|
- **Depth:** 2,132 words, 6 sections
|
||||||
- **Escalates to:** tribune, scholar, chronos, ghost
|
- **Escalates to:** tribune, scholar, chronos, ghost
|
||||||
|
|
||||||
## scholar — Academic Researcher
|
## scholar — Academic Researcher
|
||||||
- **Domain:** academia
|
- **Domain:** academia
|
||||||
- **Hitap:** Münevver
|
- **Hitap:** Münevver
|
||||||
- **Variants:** general, salva
|
- **Variants:** general
|
||||||
- **Depth:** 1,588 words, 6 sections
|
- **Depth:** 1,588 words, 6 sections
|
||||||
- **Escalates to:** frodo, tribune, sage, chronos
|
- **Escalates to:** frodo, tribune, sage, chronos
|
||||||
|
|
||||||
## scribe — FOIA Archivist & Declassified Document Analyst
|
## scribe — FOIA Archivist & Declassified Document Analyst
|
||||||
- **Domain:** history
|
- **Domain:** history
|
||||||
- **Hitap:** Verakçı
|
- **Hitap:** Verakçı
|
||||||
- **Variants:** cia-foia, cold-war-ops, general, salva
|
- **Variants:** cia-foia, cold-war-ops, general
|
||||||
- **Depth:** 2,847 words, 6 sections
|
- **Depth:** 2,847 words, 6 sections
|
||||||
- **Escalates to:** chronos, wraith, frodo, echo
|
- **Escalates to:** chronos, wraith, frodo, echo
|
||||||
|
|
||||||
## sentinel — Cyber Threat Intelligence Analyst
|
## sentinel — Cyber Threat Intelligence Analyst
|
||||||
- **Domain:** cybersecurity
|
- **Domain:** cybersecurity
|
||||||
- **Hitap:** İzci
|
- **Hitap:** İzci
|
||||||
- **Variants:** apt-profiling, c2-hunting, darknet, general, mitre-attack, salva
|
- **Variants:** apt-profiling, c2-hunting, darknet, general, mitre-attack
|
||||||
- **Depth:** 1,558 words, 6 sections
|
- **Depth:** 1,558 words, 6 sections
|
||||||
- **Escalates to:** specter, bastion, frodo, neo, echo
|
- **Escalates to:** specter, bastion, frodo, neo, echo
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ _Auto-generated by build.py | User: Salva_
|
|||||||
## tribune — Political Science & Regime Analysis Specialist
|
## tribune — Political Science & Regime Analysis Specialist
|
||||||
- **Domain:** politics
|
- **Domain:** politics
|
||||||
- **Hitap:** Müderris
|
- **Hitap:** Müderris
|
||||||
- **Variants:** general, salva
|
- **Variants:** general
|
||||||
- **Depth:** 3,356 words, 6 sections
|
- **Depth:** 3,356 words, 6 sections
|
||||||
- **Escalates to:** frodo, chronos, arbiter, sage, scholar
|
- **Escalates to:** frodo, chronos, arbiter, sage, scholar
|
||||||
|
|
||||||
@@ -194,14 +194,14 @@ _Auto-generated by build.py | User: Salva_
|
|||||||
## warden — Defense Analyst & Weapons Systems Specialist
|
## warden — Defense Analyst & Weapons Systems Specialist
|
||||||
- **Domain:** military
|
- **Domain:** military
|
||||||
- **Hitap:** Topçubaşı
|
- **Hitap:** Topçubaşı
|
||||||
- **Variants:** drone-warfare, electronic-warfare, general, naval-warfare, salva
|
- **Variants:** drone-warfare, electronic-warfare, general, naval-warfare
|
||||||
- **Depth:** 1,823 words, 6 sections
|
- **Depth:** 1,823 words, 6 sections
|
||||||
- **Escalates to:** marshal, centurion, corsair, medic
|
- **Escalates to:** marshal, centurion, corsair, medic
|
||||||
|
|
||||||
## wraith — HUMINT & Counter-Intelligence Specialist
|
## wraith — HUMINT & Counter-Intelligence Specialist
|
||||||
- **Domain:** intelligence
|
- **Domain:** intelligence
|
||||||
- **Hitap:** Mahrem
|
- **Hitap:** Mahrem
|
||||||
- **Variants:** case-studies, general, salva, source-validation
|
- **Variants:** case-studies, general, source-validation
|
||||||
- **Depth:** 2,265 words, 6 sections
|
- **Depth:** 2,265 words, 6 sections
|
||||||
- **Escalates to:** oracle, ghost, echo, frodo, sentinel
|
- **Escalates to:** oracle, ghost, echo, frodo, sentinel
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user