From cbb28903fb0cda7358543d0d4a31c29a0d75202f Mon Sep 17 00:00:00 2001 From: salvacybersec Date: Thu, 7 May 2026 16:49:01 +0300 Subject: [PATCH] feat(build): user-agnostic personalized variant routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Personalized variants (default trigger: salva.md) compile to a separate top-level folder so generated// 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 /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//general.{json,yaml,prompt.md} (shared) - generated//.{json,yaml,prompt.md} (shared) - generated/salva-personas//salva.{json,yaml,prompt.md} (private) Source layout: - personas//general.md, .md (committed) - personalized//salva.md (gitignored) - personalized/skills//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. --- .gitignore | 6 ++ CLAUDE.md | 19 ++++ README.md | 13 ++- build.py | 226 +++++++++++++++++++++++++++++++++++++++----- config.example.yaml | 13 +++ personas/CATALOG.md | 48 +++++----- 6 files changed, 272 insertions(+), 53 deletions(-) diff --git a/.gitignore b/.gitignore index 2973c83..54507fa 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,9 @@ __pycache__/ /agents-claude-archive/ /skills-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/ diff --git a/CLAUDE.md b/CLAUDE.md index 5ad93d3..f5dfd36 100644 --- a/CLAUDE.md +++ b/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 - `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//` free of per-user content: + +- `generated//general.*` + `generated//.*` — generic & domain variants +- `generated/-personas//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. ## 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 - Turkish honorific titles ("Hitap") are used for `address_to` fields - `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//salva.md` — per-persona user-personalized variant +3. `personas/_shared/skills//SKILL.md` — new shared skills (persona-mappable, not vendor-synced) diff --git a/README.md b/README.md index e909202..a24ef80 100644 --- a/README.md +++ b/README.md @@ -222,11 +222,14 @@ sources/ ### Variant Types -| Type | Purpose | Example | -|------|---------|---------| -| `general.md` | Base persona — works standalone for any user | `neo/general.md` | -| `.md` | Domain deep-dive narrowing the persona's focus | `neo/redteam.md` | -| `salva.md` | User-personalized — references specific projects, data, tools | `neo/salva.md` | +| Type | Purpose | Example | Output location | +|------|---------|---------|-----------------| +| `general.md` | Base persona — works standalone for any user | `neo/general.md` | `generated//` | +| `.md` | Domain deep-dive narrowing the persona's focus | `neo/redteam.md` | `generated//` | +| `salva.md` (personalized) | User-personalized — references specific projects, data, tools | `neo/salva.md` | `generated/-personas//` | + +**Personalized variant routing (config-driven, user-agnostic):** +The build keeps user-specific output in a separate top-level folder so the main `generated//` directories stay clean. The folder name is derived from `config.user.name` (slugified, `-personas` suffix) — e.g. `user.name: "Salva"` → `generated/salva-personas//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 diff --git a/build.py b/build.py index 2340383..de3f2f3 100755 --- a/build.py +++ b/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// 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 + `/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: ///.md + The shared persona library stays in personas//. 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// — general + non-personalized + 2. output_dir/// — 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( persona_dir: Path, output_dir: Path, @@ -199,8 +314,6 @@ def build_persona( 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" @@ -226,6 +339,14 @@ def build_persona( if not parsed: 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 output = { **meta, @@ -372,6 +493,7 @@ DEFAULT_SKILL_PERSONA_MAP = { "pentest-reporter": ["neo", "phantom", "bastion"], # Marketing / business → personas "marketing-strategist": ["herald", "ledger"], + "linkedin-content-strategy": ["herald", "forge", "frodo", "ghost"], # Document processing → personas "image-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(), } + # Also index user-personalized skills from //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 pskills_dir = shared_dir / "paperclip-skills" if pskills_dir.exists(): @@ -1155,9 +1324,7 @@ def install_claude(output_dir: Path): 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 + for persona_dir in iter_persona_output_dirs(output_dir): # Install slash commands for all variants (general + specializations). 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.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 persona_dir in iter_persona_output_dirs(output_dir): for prompt_file in persona_dir.glob("*.prompt.md"): variant = prompt_file.stem codename = persona_dir.name @@ -1612,9 +1777,7 @@ def install_opencode( # Emit one agent file per variant. General → .md (picker-visible). # Non-general → -.md with hidden:true so it's # task-dispatchable by name without cluttering the picker. - for persona_dir in sorted(output_dir.iterdir()): - if not persona_dir.is_dir() or persona_dir.name.startswith("_"): - continue + for persona_dir in iter_persona_output_dirs(output_dir): for variant_json in sorted(persona_dir.glob("*.json")): try: @@ -1929,9 +2092,7 @@ def install_feynman( emitted_agents: set[str] = set() ident_re = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") - for persona_dir in sorted(output_dir.iterdir()): - if not persona_dir.is_dir() or persona_dir.name.startswith("_"): - continue + for persona_dir in iter_persona_output_dirs(output_dir): for variant_json in sorted(persona_dir.glob("*.json")): try: 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.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 persona_dir in iter_persona_output_dirs(output_dir): 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) @@ -2252,9 +2411,7 @@ def install_paperclip(output_dir: Path, personas_dir: Path, shared_dir: Path | N 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 + for persona_dir in iter_persona_output_dirs(output_dir): general_json = persona_dir / "general.json" 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) count = 0 identity_sections = [] - for persona_dir in sorted(output_dir.iterdir()): - if not persona_dir.is_dir() or persona_dir.name.startswith("_"): - continue + for persona_dir in iter_persona_output_dirs(output_dir): general_prompt = persona_dir / "general.prompt.md" if not general_prompt.exists(): continue @@ -2602,13 +2757,36 @@ def main(): config = load_config(root) 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 = [ d for d in sorted(personas_dir.iterdir()) 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: print("No persona directories found.") sys.exit(1) diff --git a/config.example.yaml b/config.example.yaml index 5c78d99..bd5f512 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -18,6 +18,19 @@ user: casual_language: "en" technical_language: "en" + # Personalized variant routing (optional). Variants matched here are + # written to generated/// instead + # of generated//, 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: servers: [] diff --git a/personas/CATALOG.md b/personas/CATALOG.md index 0ff0938..8f49e4f 100644 --- a/personas/CATALOG.md +++ b/personas/CATALOG.md @@ -5,14 +5,14 @@ _Auto-generated by build.py | User: Salva_ ## arbiter — International Law & War Crimes Specialist - **Domain:** law - **Hitap:** Kadı -- **Variants:** general, salva, sanctions +- **Variants:** general, sanctions - **Depth:** 2,880 words, 6 sections - **Escalates to:** frodo, marshal, tribune, chronos ## architect — DevOps & Systems Engineer - **Domain:** engineering - **Hitap:** Mimar Ağa -- **Variants:** general, salva +- **Variants:** general - **Depth:** 1,526 words, 6 sections - **Escalates to:** forge, vortex, neo @@ -26,14 +26,14 @@ _Auto-generated by build.py | User: Salva_ ## centurion — Military History & War Analysis Specialist - **Domain:** military - **Hitap:** Vakanüvis -- **Variants:** general, ottoman-wars, salva, ukraine-russia +- **Variants:** general, ottoman-wars, ukraine-russia - **Depth:** 2,269 words, 6 sections - **Escalates to:** marshal, warden, chronos, corsair ## chronos — World History & Civilization Analysis Specialist - **Domain:** history - **Hitap:** Tarihçibaşı -- **Variants:** general, salva +- **Variants:** general - **Depth:** 2,581 words, 6 sections - **Escalates to:** centurion, scholar, sage, tribune, scribe @@ -47,84 +47,84 @@ _Auto-generated by build.py | User: Salva_ ## corsair — Special Operations & Irregular Warfare Specialist - **Domain:** military - **Hitap:** Akıncı -- **Variants:** general, proxy-warfare, salva +- **Variants:** general, proxy-warfare - **Depth:** 2,352 words, 6 sections - **Escalates to:** marshal, wraith, centurion, warden ## echo — SIGINT / COMINT / ELINT Specialist - **Domain:** intelligence - **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 - **Escalates to:** cipher, vortex, frodo, wraith, sentinel ## forge — Software Development & AI/ML Engineer - **Domain:** engineering - **Hitap:** Demirci -- **Variants:** agent-dev, frontend-design, general, salva +- **Variants:** agent-dev, frontend-design, general - **Depth:** 1,882 words, 6 sections - **Escalates to:** architect, cipher, sentinel ## frodo — Strategic Intelligence Analyst - **Domain:** intelligence - **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 - **Escalates to:** oracle, ghost, wraith, echo, sentinel, marshal ## gambit — Chess & Strategic Thinking Specialist - **Domain:** strategy - **Hitap:** Vezir -- **Variants:** general, salva +- **Variants:** general - **Depth:** 2,548 words, 6 sections - **Escalates to:** marshal, sage, tribune, corsair ## ghost — PSYOP & Information Warfare Specialist - **Domain:** intelligence - **Hitap:** Propagandist -- **Variants:** cognitive-warfare, general, russian-info-war, salva +- **Variants:** cognitive-warfare, general, russian-info-war - **Depth:** 2,117 words, 6 sections - **Escalates to:** oracle, frodo, herald, wraith ## herald — Media Analysis & Strategic Communication Specialist - **Domain:** media - **Hitap:** Münadi -- **Variants:** general, salva +- **Variants:** general - **Depth:** 2,827 words, 6 sections - **Escalates to:** ghost, polyglot, oracle, frodo ## ledger — Economic Intelligence & FININT Specialist - **Domain:** economics - **Hitap:** Defterdar -- **Variants:** general, salva, sanctions-evasion +- **Variants:** general, sanctions-evasion - **Depth:** 2,847 words, 6 sections - **Escalates to:** arbiter, frodo, tribune, scribe ## marshal — Military Doctrine & Strategy Specialist - **Domain:** military - **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 - **Escalates to:** centurion, warden, corsair, frodo ## medic — Biomedical & CBRN Specialist - **Domain:** science - **Hitap:** Hekim Başı -- **Variants:** cbrn-defense, general, salva +- **Variants:** cbrn-defense, general - **Depth:** 2,309 words, 6 sections - **Escalates to:** warden, frodo, marshal, corsair ## neo — Red Team Lead / Exploit Developer - **Domain:** cybersecurity - **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 - **Escalates to:** bastion, phantom, specter, vortex, sentinel ## oracle — OSINT & Digital Intelligence Specialist - **Domain:** intelligence - **Hitap:** Kaşif -- **Variants:** crypto-osint, general, salva, source-verification +- **Variants:** crypto-osint, general, source-verification - **Depth:** 1,880 words, 6 sections - **Escalates to:** ghost, sentinel, frodo, herald @@ -138,35 +138,35 @@ _Auto-generated by build.py | User: Salva_ ## polyglot — Linguistics & LINGINT Specialist - **Domain:** linguistics - **Hitap:** Tercüman-ı Divan -- **Variants:** arabic, general, russian, salva, swahili +- **Variants:** arabic, general, russian, swahili - **Depth:** 2,308 words, 6 sections - **Escalates to:** frodo, ghost, herald, scholar ## sage — Philosophy, Psychology & Power Theory Specialist - **Domain:** humanities - **Hitap:** Arif -- **Variants:** general, salva +- **Variants:** general - **Depth:** 2,132 words, 6 sections - **Escalates to:** tribune, scholar, chronos, ghost ## scholar — Academic Researcher - **Domain:** academia - **Hitap:** Münevver -- **Variants:** general, salva +- **Variants:** general - **Depth:** 1,588 words, 6 sections - **Escalates to:** frodo, tribune, sage, chronos ## scribe — FOIA Archivist & Declassified Document Analyst - **Domain:** history - **Hitap:** Verakçı -- **Variants:** cia-foia, cold-war-ops, general, salva +- **Variants:** cia-foia, cold-war-ops, general - **Depth:** 2,847 words, 6 sections - **Escalates to:** chronos, wraith, frodo, echo ## sentinel — Cyber Threat Intelligence Analyst - **Domain:** cybersecurity - **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 - **Escalates to:** specter, bastion, frodo, neo, echo @@ -180,7 +180,7 @@ _Auto-generated by build.py | User: Salva_ ## tribune — Political Science & Regime Analysis Specialist - **Domain:** politics - **Hitap:** Müderris -- **Variants:** general, salva +- **Variants:** general - **Depth:** 3,356 words, 6 sections - **Escalates to:** frodo, chronos, arbiter, sage, scholar @@ -194,14 +194,14 @@ _Auto-generated by build.py | User: Salva_ ## warden — Defense Analyst & Weapons Systems Specialist - **Domain:** military - **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 - **Escalates to:** marshal, centurion, corsair, medic ## wraith — HUMINT & Counter-Intelligence Specialist - **Domain:** intelligence - **Hitap:** Mahrem -- **Variants:** case-studies, general, salva, source-validation +- **Variants:** case-studies, general, source-validation - **Depth:** 2,265 words, 6 sections - **Escalates to:** oracle, ghost, echo, frodo, sentinel