chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 11:57:08 +08:00
commit 2624ac09ce
327 changed files with 82275 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# Example --agents-file for ./scripts/install.sh --agents-file <this>
# One agent per line, by slug or human name. Blank lines and # comments are ignored.
#
# ./scripts/install.sh --tool claude-code --agents-file scripts/agents-to-install.example
#
frontend-developer
backend-architect
security-architect
# Names work too (case-insensitive):
Penetration Tester
+494
View File
@@ -0,0 +1,494 @@
#!/usr/bin/env python3
"""Build the Hermes lazy-router plugin for The Agency agents.
The generated plugin exposes a small fixed tool surface to Hermes and keeps the
large agent roster in an on-disk JSON data file. That avoids using
skills.external_dirs, which advertises every Agency agent in Hermes' initial
skill catalog.
"""
from __future__ import annotations
import argparse
import json
import re
import shutil
import textwrap
from pathlib import Path
PLUGIN_NAME = "agency-agents-router"
def division_dirs(repo_root: Path) -> list[str]:
# divisions.json (repo root) is the single source of truth for the division
# set. Read it rather than hardcoding a copy here: a hardcoded list silently
# drops new divisions from the Hermes roster (e.g. healthcare) the moment the
# catalog grows. check-divisions.sh guards divisions.json against the tracked
# dirs, so deriving from it keeps this plugin in sync by construction.
data = json.loads((repo_root / "divisions.json").read_text(encoding="utf-8"))
return sorted(data["divisions"].keys())
def slugify(value: str) -> str:
value = value.lower()
value = re.sub(r"[^a-z0-9]+", "-", value)
return value.strip("-")
def parse_agent(path: Path, repo_root: Path) -> dict[str, str] | None:
text = path.read_text(encoding="utf-8")
if not text.startswith("---\n"):
return None
parts = text.split("---\n", 2)
if len(parts) < 3:
return None
frontmatter = parts[1]
body = parts[2].lstrip("\n")
fields: dict[str, str] = {}
for line in frontmatter.splitlines():
if ":" not in line or line.startswith((" ", "\t")):
continue
key, value = line.split(":", 1)
fields[key.strip()] = value.strip().strip('"').strip("'")
name = fields.get("name", "").strip()
if not name:
return None
rel = path.relative_to(repo_root)
division = rel.parts[0]
return {
"slug": slugify(name),
"name": name,
"description": fields.get("description", "").strip(),
"division": division,
"color": fields.get("color", "").strip(),
"emoji": fields.get("emoji", "").strip(),
"vibe": fields.get("vibe", "").strip(),
"source_path": str(rel),
"body": body,
}
def collect_agents(repo_root: Path) -> list[dict[str, str]]:
agents: list[dict[str, str]] = []
for dirname in division_dirs(repo_root):
base = repo_root / dirname
if not base.is_dir():
continue
for path in sorted(base.rglob("*.md")):
parsed = parse_agent(path, repo_root)
if parsed:
agents.append(parsed)
agents.sort(key=lambda item: (item["division"], item["slug"]))
seen: set[str] = set()
duplicates: set[str] = set()
for agent in agents:
slug = agent["slug"]
if slug in seen:
duplicates.add(slug)
seen.add(slug)
if duplicates:
dupes = ", ".join(sorted(duplicates))
raise SystemExit(f"duplicate Hermes agent slugs: {dupes}")
return agents
def plugin_yaml() -> str:
return textwrap.dedent(
f"""
name: {PLUGIN_NAME}
version: 1.0.0
description: Lazy search/load/delegate router for The Agency agent roster.
provides_tools:
- agency_agents_search
- agency_agents_inspect
- agency_agents_load
- agency_agents_delegate
"""
).lstrip()
def init_py() -> str:
return r'''"""Hermes plugin: lazy router for The Agency agents."""
from __future__ import annotations
import json
import math
import re
from pathlib import Path
from typing import Any
_DATA_PATH = Path(__file__).parent / "data" / "agents.json"
_AGENTS: list[dict[str, Any]] | None = None
_WORD_RE = re.compile(r"[a-z0-9][a-z0-9+.#_-]*", re.I)
def _load_agents() -> list[dict[str, Any]]:
global _AGENTS
if _AGENTS is None:
_AGENTS = json.loads(_DATA_PATH.read_text(encoding="utf-8"))
return _AGENTS
def _tokens(text: str) -> set[str]:
return {token.lower() for token in _WORD_RE.findall(text or "")}
def _agent_lookup(identifier: str) -> dict[str, Any] | None:
needle = (identifier or "").strip().lower()
if not needle:
return None
slug = re.sub(r"[^a-z0-9]+", "-", needle).strip("-")
for agent in _load_agents():
if agent["slug"] == slug or agent["name"].lower() == needle:
return agent
return None
def _identifier(args: dict[str, Any]) -> str:
# Accept either "agent" or "slug": agency_agents_search returns results keyed
# by "slug", so callers naturally chain search -> load/inspect/delegate with
# slug=. Both name the same thing (a slug or exact display name).
return str(args.get("agent") or args.get("slug") or "").strip()
def _not_found(identifier: str) -> dict[str, Any]:
return {
"success": False,
"error": "agent not found" if identifier else "agent or slug is required",
"agent": identifier or None,
}
def _score(agent: dict[str, Any], query_tokens: set[str], query_text: str) -> float:
haystack_fields = [
agent.get("name", ""),
agent.get("description", ""),
agent.get("division", ""),
agent.get("vibe", ""),
agent.get("body", "")[:8000],
]
haystack_text = "\n".join(haystack_fields).lower()
haystack_tokens = _tokens(haystack_text)
overlap = query_tokens & haystack_tokens
score = float(len(overlap))
if query_text and query_text in haystack_text:
score += 5.0
name = agent.get("name", "").lower()
description = agent.get("description", "").lower()
for token in query_tokens:
if token in name:
score += 3.0
if token in description:
score += 1.5
if score == 0.0:
return 0.0
# Slightly prefer focused descriptions over huge bodies when scores tie.
return score + (1.0 / math.sqrt(max(len(haystack_tokens), 1)))
def _summary(agent: dict[str, Any], score: float | None = None) -> dict[str, Any]:
item = {
"slug": agent["slug"],
"name": agent["name"],
"division": agent["division"],
"description": agent.get("description", ""),
"vibe": agent.get("vibe", ""),
"source_path": agent.get("source_path", ""),
}
if score is not None:
item["score"] = round(score, 3)
return item
def _specialist_prompt(agent: dict[str, Any], task: str = "") -> str:
task_block = f"\n\n## User task\n{task.strip()}\n" if task and task.strip() else ""
return (
f"Use the following Agency specialist context for this turn. "
f"Adopt the specialist's relevant standards and checklists, but obey the "
f"user's current request and higher-priority system/developer instructions.\n\n"
f"# {agent['name']} ({agent['slug']})\n\n"
f"Division: {agent.get('division', '')}\n"
f"Description: {agent.get('description', '')}\n"
f"Source: {agent.get('source_path', '')}\n"
f"{task_block}\n\n"
f"## Specialist instructions\n{agent.get('body', '')}"
)
def _json(payload: dict[str, Any]) -> str:
return json.dumps(payload, ensure_ascii=False, indent=2)
SEARCH_DESCRIPTION = (
"Search The Agency's on-disk specialist agent roster without loading all "
"agents into the prompt. Use this when the user asks for an Agency/Data "
"Swami specialist, role, discipline, or wants help choosing the right agent."
)
SEARCH_SCHEMA = {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Natural-language search query."},
"division": {"type": "string", "description": "Optional division filter, e.g. engineering, marketing, testing."},
"limit": {"type": "integer", "description": "Maximum results, default 8, max 25."},
},
"required": ["query"],
}
READ_DESCRIPTION = (
"Read one Agency specialist by slug or name. Returns metadata by default "
"and includes the full specialist instructions only when include_body is true."
)
READ_SCHEMA = {
"type": "object",
"properties": {
"agent": {"type": "string", "description": "Agent slug or exact display name."},
"slug": {"type": "string", "description": "Alias for agent. Pass the slug from agency_agents_search results."},
"include_body": {"type": "boolean", "description": "Include full specialist instructions."},
},
"required": [],
}
PROMPT_DESCRIPTION = (
"Load a selected Agency specialist as a prompt block for the current task. "
"Use after agency_agents_search when you need one specialist's full context."
)
PROMPT_SCHEMA = {
"type": "object",
"properties": {
"agent": {"type": "string", "description": "Agent slug or exact display name."},
"slug": {"type": "string", "description": "Alias for agent. Pass the slug from agency_agents_search results."},
"task": {"type": "string", "description": "The user's task to pair with the specialist context."},
},
"required": [],
}
DELEGATE_DESCRIPTION = (
"Delegate a task to one selected Agency specialist through Hermes' "
"delegate_task tool when available. Falls back to returning the composed "
"specialist prompt if delegation is unavailable."
)
DELEGATE_SCHEMA = {
"type": "object",
"properties": {
"agent": {"type": "string", "description": "Agent slug or exact display name."},
"slug": {"type": "string", "description": "Alias for agent. Pass the slug from agency_agents_search results."},
"task": {"type": "string", "description": "Concrete task for the specialist."},
"toolsets": {
"type": "array",
"items": {"type": "string"},
"description": "Optional Hermes toolsets for the delegated worker, e.g. ['terminal','file'].",
},
},
"required": ["task"],
}
def register(ctx):
def search(args: dict[str, Any], **kwargs) -> str:
del kwargs
query = str(args.get("query", "")).strip()
if not query:
return _json({"success": False, "error": "query is required"})
division = str(args.get("division", "")).strip().lower()
try:
limit = min(max(int(args.get("limit", 8)), 1), 25)
except Exception:
limit = 8
q_tokens = _tokens(query)
q_text = query.lower()
matches: list[tuple[float, dict[str, Any]]] = []
for agent in _load_agents():
if division and agent.get("division", "").lower() != division:
continue
score = _score(agent, q_tokens, q_text)
if score > 0:
matches.append((score, agent))
matches.sort(key=lambda item: (-item[0], item[1]["division"], item[1]["slug"]))
return _json({
"success": True,
"query": query,
"count": len(matches),
"results": [_summary(agent, score) for score, agent in matches[:limit]],
})
def read(args: dict[str, Any], **kwargs) -> str:
del kwargs
identifier = _identifier(args)
agent = _agent_lookup(identifier)
if not agent:
return _json(_not_found(identifier))
payload = {"success": True, "agent": _summary(agent)}
if bool(args.get("include_body", False)):
payload["body"] = agent.get("body", "")
return _json(payload)
def prompt(args: dict[str, Any], **kwargs) -> str:
del kwargs
identifier = _identifier(args)
agent = _agent_lookup(identifier)
if not agent:
return _json(_not_found(identifier))
return _json({
"success": True,
"agent": _summary(agent),
"prompt": _specialist_prompt(agent, str(args.get("task", ""))),
})
def delegate(args: dict[str, Any], **kwargs) -> str:
del kwargs
identifier = _identifier(args)
agent = _agent_lookup(identifier)
task = str(args.get("task", "")).strip()
if not agent:
return _json(_not_found(identifier))
if not task:
return _json({"success": False, "error": "task is required"})
composed = _specialist_prompt(agent, task)
delegate_args: dict[str, Any] = {
"goal": task,
"context": composed,
}
toolsets = args.get("toolsets")
if isinstance(toolsets, list) and toolsets:
delegate_args["toolsets"] = [str(item) for item in toolsets]
try:
result = ctx.dispatch_tool("delegate_task", delegate_args)
return _json({"success": True, "agent": _summary(agent), "delegated": True, "result": result})
except Exception as exc: # pragma: no cover - depends on Hermes runtime
return _json({
"success": True,
"agent": _summary(agent),
"delegated": False,
"warning": f"delegate_task unavailable: {exc}",
"prompt": composed,
})
ctx.register_tool(
name="agency_agents_search",
toolset="agency_agents",
schema=SEARCH_SCHEMA,
handler=search,
description=SEARCH_DESCRIPTION,
)
ctx.register_tool(
name="agency_agents_inspect",
toolset="agency_agents",
schema=READ_SCHEMA,
handler=read,
description=READ_DESCRIPTION,
)
ctx.register_tool(
name="agency_agents_load",
toolset="agency_agents",
schema=PROMPT_SCHEMA,
handler=prompt,
description=PROMPT_DESCRIPTION,
)
ctx.register_tool(
name="agency_agents_delegate",
toolset="agency_agents",
schema=DELEGATE_SCHEMA,
handler=delegate,
description=DELEGATE_DESCRIPTION,
)
'''
def readme(agent_count: int) -> str:
return textwrap.dedent(
f"""
# Hermes Agency Agents Router Plugin
Generated by `scripts/convert.sh --tool hermes`.
This integration installs one Hermes plugin named `{PLUGIN_NAME}` instead
of adding 232+ generated skills to `skills.external_dirs`. Hermes sees a
small fixed tool surface at startup, while the complete Agency roster is
stored on disk in `data/agents.json` and searched/loaded lazily.
Generated agent count: {agent_count}
## Tools exposed to Hermes
- `agency_agents_search` — find matching specialists by query/division.
- `agency_agents_inspect` — inspect one specialist's metadata or full body.
- `agency_agents_load` — compose one specialist prompt for the current task.
- `agency_agents_delegate` — delegate through Hermes `delegate_task` when available.
## Specialist usage instruction for Hermes
When a Hermes project needs Agency specialists, explicitly ask Hermes to use
the `{PLUGIN_NAME}` plugin/router and load only the specialists needed for
the current phase. Do not ask Hermes to install or preload the full Agency
roster as skills.
Recommended project instruction:
```text
Use the agency-agents-router plugin. Search the Agency roster for the right
specialists, then load or delegate only the specific agents needed for each
part of the project. For multi-discipline projects, use multiple selected
specialists across the project, but keep routing lazy: do not preload the
full Agency roster and do not add agency-agents to skills.external_dirs.
```
Example:
```text
For this Data Swami build, use the agency-agents-router plugin to pick
relevant Agency specialists. Search first, then delegate to selected agents
such as frontend, backend, UX, QA, data engineering, and product strategy as
needed. Load/delegate each specialist on demand rather than loading all
Agency agents at startup.
```
## Install
```bash
./scripts/convert.sh --tool hermes
./scripts/install.sh --tool hermes
```
The installer copies the generated plugin to:
```text
${{HERMES_HOME:-~/.hermes}}/plugins/{PLUGIN_NAME}
```
It then enables `{PLUGIN_NAME}` under `plugins.enabled` in the Hermes
config. It does **not** write to `skills.external_dirs`.
"""
).lstrip()
def build(repo_root: Path, out_dir: Path) -> int:
agents = collect_agents(repo_root)
plugin_dir = out_dir / PLUGIN_NAME
if plugin_dir.exists():
shutil.rmtree(plugin_dir)
(plugin_dir / "data").mkdir(parents=True, exist_ok=True)
(plugin_dir / "plugin.yaml").write_text(plugin_yaml(), encoding="utf-8")
(plugin_dir / "__init__.py").write_text(init_py(), encoding="utf-8")
(plugin_dir / "data" / "agents.json").write_text(
json.dumps(agents, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
(out_dir / "README.md").write_text(readme(len(agents)), encoding="utf-8")
return len(agents)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--out", type=Path, default=None, help="Output directory, default integrations/hermes")
args = parser.parse_args()
repo_root = args.repo_root.resolve()
out_dir = (args.out or (repo_root / "integrations" / "hermes")).resolve()
out_dir.mkdir(parents=True, exist_ok=True)
count = build(repo_root, out_dir)
print(count)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env bash
#
# check-agent-originality.sh — Flag agent files that substantially duplicate
# an existing agent (or another agent in the same change set).
#
# Why: a new agent should be genuinely new. Find-replace "re-skins" of an
# existing agent (e.g. swapping a country/platform name) are easy to miss in
# review because they're mergeable and well-formed — but they bloat the
# library with duplicates. This compares each candidate against the whole
# existing roster using entity-neutralized 8-word shingle overlap, so a
# swapped proper noun can't hide the copy.
#
# Usage:
# ./scripts/check-agent-originality.sh [file ...]
# With files: checks those agent .md files (used by CI on changed files).
# With no args: checks every agent in the repo against every other (audit).
#
# Exit status:
# 0 all candidates below the FAIL threshold
# 1 at least one candidate at/above FAIL threshold (likely duplicate)
#
# Tunables (env):
# ORIGINALITY_FAIL default 40 — at/above this %, treated as a duplicate (exit 1)
# ORIGINALITY_WARN default 20 — at/above this %, surfaced as a warning (no fail)
#
# Calibration: across the existing agent library the worst same-pair
# similarity is ~1.5% (median 0%). Anything in the double digits is a strong
# anomaly; the defaults leave a wide safety margin against false positives.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
command -v python3 >/dev/null 2>&1 || {
echo "ERROR: python3 is required for the originality check." >&2
exit 2
}
ORIGINALITY_FAIL="${ORIGINALITY_FAIL:-40}" \
ORIGINALITY_WARN="${ORIGINALITY_WARN:-20}" \
REPO_ROOT="$REPO_ROOT" \
python3 - "$@" <<'PYEOF'
import os, re, sys, glob, json
REPO_ROOT = os.environ["REPO_ROOT"]
FAIL = float(os.environ["ORIGINALITY_FAIL"])
WARN = float(os.environ["ORIGINALITY_WARN"])
# Division set — divisions.json (repo root) is the single source of truth, and
# scripts/check-divisions.sh (CI) enforces it against the directories on disk.
# Read it directly rather than hardcoding the list here so this check can never
# drift out of sync with the catalog the way a copied literal silently would.
with open(os.path.join(REPO_ROOT, "divisions.json")) as _fh:
AGENT_DIRS = sorted(json.load(_fh)["divisions"].keys())
# Proper nouns we neutralize so a find-replace re-skin (swap the country/platform
# and little else) still scores as a near-duplicate. Extend as new markets appear.
ENTITY = re.compile(
r'\b(vietnam|vietnamese|china|chinese|douyin|tiktok|korea|korean|japan|japanese|'
r'india|indian|indonesia|indonesian|thailand|thai|philippines|filipino|brazil|'
r'brazilian|mexico|mexican|wechat|weixin|weibo|xiaohongshu|rednote|kuaishou|'
r'bilibili|zhihu|baidu|shopee|lazada|zalo|tokopedia|taobao|tmall|pinduoduo|'
r'instagram|facebook|youtube|reels|shorts|linkedin|twitter|threads|snapchat)\b')
def strip_frontmatter(t):
if t.startswith('---'):
parts = t.split('---', 2)
if len(parts) >= 3:
return parts[2]
return t
def tokens(text):
text = ENTITY.sub(' ', strip_frontmatter(text).lower())
text = re.sub(r'[^a-z0-9 ]', ' ', text)
return text.split()
def shingles(words, k=8):
return set(' '.join(words[i:i+k]) for i in range(max(0, len(words) - k + 1)))
def jaccard(a, b):
return len(a & b) / len(a | b) if a and b else 0.0
def is_agent(path):
try:
with open(path) as fh:
return fh.readline().strip() == '---'
except OSError:
return False
def rel(p):
try:
return os.path.relpath(p, REPO_ROOT)
except ValueError:
return p
# --- Build the existing-library corpus -------------------------------------
corpus = {}
for d in AGENT_DIRS:
for f in glob.glob(os.path.join(REPO_ROOT, d, '**', '*.md'), recursive=True):
if is_agent(f):
corpus[os.path.abspath(f)] = shingles(tokens(open(f).read()))
# --- Determine candidates ---------------------------------------------------
args = sys.argv[1:]
if args:
candidates = []
for a in args:
p = a if os.path.isabs(a) else os.path.join(os.getcwd(), a)
p = os.path.abspath(p)
if not os.path.isfile(p):
print(f" skip (not found): {a}")
continue
if not is_agent(p):
print(f" skip (no frontmatter, not an agent): {rel(p)}")
continue
candidates.append(p)
else:
candidates = list(corpus.keys()) # audit mode: everything vs everything
if not candidates:
print("No agent files to check.")
sys.exit(0)
cand_sh = {p: corpus.get(p) or shingles(tokens(open(p).read())) for p in candidates}
cand_set = set(candidates)
worst = 0.0
fails, warns = [], []
for p in candidates:
sh = cand_sh[p]
best_name, best_score = "", 0.0
# vs existing library (exclude the candidate itself by path)
for cf, csh in corpus.items():
if cf == p:
continue
s = jaccard(sh, csh)
if s > best_score:
best_name, best_score = rel(cf), s
# vs other candidates in this same change set
for op in candidates:
if op == p:
continue
s = jaccard(sh, cand_sh[op])
if s > best_score:
best_name, best_score = rel(op) + " (same change set)", s
pct = best_score * 100
worst = max(worst, pct)
tag = "OK "
if pct >= FAIL:
tag = "FAIL "; fails.append((rel(p), best_name, pct))
elif pct >= WARN:
tag = "WARN "; warns.append((rel(p), best_name, pct))
print(f" [{tag}] {pct:5.1f}% {rel(p)}")
if best_name:
print(f" closest: {best_name}")
print()
print(f"Thresholds: WARN >= {WARN:.0f}%, FAIL >= {FAIL:.0f}% "
f"(existing-library baseline max ~1.5%)")
if fails:
print()
print(f"FAILED: {len(fails)} agent(s) substantially duplicate existing content:")
for name, match, pct in fails:
print(f" - {name} ~{pct:.0f}% like {match}")
print()
print("A new agent should be genuinely new. If this is intended market/platform")
print("localization, make the body distinct (different platforms, tactics, examples)")
print("rather than a find-replace of an existing agent.")
sys.exit(1)
if warns:
print(f"\n{len(warns)} warning(s) — review for overlap, but not blocking.")
print("\nPASSED")
sys.exit(0)
PYEOF
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
#
# check-divisions.sh — enforce a single source of truth for the division set.
#
# divisions.json (repo root) is canonical. This script fails if any of the
# following disagree with it:
# 1. The actual top-level agent directories on disk
# 2. AGENT_DIRS in scripts/convert.sh
# 3. AGENT_DIRS in scripts/lint-agents.sh
# 4. The path filters in .github/workflows/lint-agents.yml
# 5. Every divisions.json entry has label, icon, and color
#
# Add a division: create its directory, add an entry to divisions.json, then
# this script tells you every other place that must be updated. No deps beyond
# bash 3.2 + coreutils (no jq) so it runs the same on macOS and CI.
#
# Usage: ./scripts/check-divisions.sh
set -euo pipefail
cd "$(dirname "$0")/.."
JSON="divisions.json"
# Top-level directories that are NOT divisions. Everything else at the repo
# root that is a directory is treated as a division (so a new division dir is
# caught even if nobody remembered to register it).
# integrations/ is convert.sh's OUTPUT tree (per-tool conversions written back
# into the repo), not a source-agent category. strategy/ holds playbooks and
# runbooks (no agent frontmatter), not agents. Neither is a division — they must
# never be scanned as source-agent categories.
NON_DIVISION_DIRS=(examples scripts integrations strategy)
errors=0
fail() { echo "ERROR $*"; errors=$((errors + 1)); }
# --- sorted, newline-delimited helpers -------------------------------------
# Canonical set: object-valued keys inside the "divisions" object. Scoping to
# lines after the `"divisions": {` opener excludes both the wrapper key itself
# and the string-valued "_note" key.
canonical() {
awk '/"divisions"[[:space:]]*:[[:space:]]*\{/{f=1; next} f' "$JSON" \
| grep -oE '"[a-z0-9-]+"[[:space:]]*:[[:space:]]*\{' \
| sed -E 's/"([a-z0-9-]+)".*/\1/' | sort -u
}
# Actual division directories: top-level dirs that contain at least one
# git-TRACKED file, minus the excludes and anything dot-prefixed. Using
# `git ls-files` (not a filesystem glob) keeps this in lockstep with what CI's
# clean checkout sees, so a local gitignored scratch dir (e.g. notes/) can't
# produce a false failure.
actual_dirs() {
local base
git ls-files | awk -F/ 'NF>1{print $1}' | sort -u | while IFS= read -r base; do
[[ "$base" == .* ]] && continue
case " ${NON_DIVISION_DIRS[*]} " in *" $base "*) continue ;; esac
echo "$base"
done
}
# Contents of a bash AGENT_DIRS=( ... ) array in the given file, one per line.
agent_dirs_array() {
awk '/AGENT_DIRS=\(/{f=1; next} f && /^\)/{exit} f{print}' "$1" \
| tr ' \t' '\n\n' | grep -E '^[a-z0-9-]+$' | sort -u
}
# Compare canonical vs a candidate set; report both directions.
compare() {
local label="$1" candidate="$2" canon
canon="$(canonical)"
local missing extra
missing="$(comm -23 <(echo "$canon") <(echo "$candidate"))"
extra="$(comm -13 <(echo "$canon") <(echo "$candidate"))"
if [[ -n "$missing" ]]; then
fail "$label is missing division(s) present in $JSON: $(echo "$missing" | tr '\n' ' ')"
fi
if [[ -n "$extra" ]]; then
fail "$label has division(s) not in $JSON: $(echo "$extra" | tr '\n' ' ')"
fi
}
# --- checks ----------------------------------------------------------------
[[ -f "$JSON" ]] || { echo "ERROR $JSON not found at repo root"; exit 1; }
compare "the agent directories on disk" "$(actual_dirs)"
compare "scripts/convert.sh AGENT_DIRS" "$(agent_dirs_array scripts/convert.sh)"
compare "scripts/lint-agents.sh AGENT_DIRS" "$(agent_dirs_array scripts/lint-agents.sh)"
# Workflow path filters: every canonical division must appear as `<div>/` in
# the lint workflow, or new divisions silently skip CI.
WF=".github/workflows/lint-agents.yml"
if [[ -f "$WF" ]]; then
while IFS= read -r div; do
grep -qE "\b${div}/" "$WF" || fail "$WF has no path filter for division '$div'"
done < <(canonical)
else
fail "$WF not found"
fi
# Every entry must have label, icon, and color.
while IFS= read -r div; do
block="$(awk -v d="\"$div\"" '$0 ~ d"[[:space:]]*:[[:space:]]*\\{" {print; found=1; next} found && /\}/ {print; exit} found {print}' "$JSON")"
for field in label icon color; do
echo "$block" | grep -qE "\"$field\"[[:space:]]*:" \
|| fail "division '$div' in $JSON is missing \"$field\""
done
done < <(canonical)
# Every division must contain at least one agent file: a .md whose first line is
# '---' frontmatter. This is the content-derived backstop that keeps a docs or
# playbook directory (e.g. strategy/, all of whose files are frontmatter-less)
# from being registered as an empty agent division.
has_agent_file() {
local f first
while IFS= read -r f; do
first="$(head -1 "$f" | tr -d '\r')"
[[ "$first" == "---" ]] && return 0
done < <(find "$1" -name '*.md' -type f 2>/dev/null)
return 1
}
while IFS= read -r div; do
if [[ ! -d "$div" ]]; then
fail "division '$div' has no directory on disk"
elif ! has_agent_file "$div"; then
fail "division '$div' has no agent files (.md with '---' frontmatter) — not a real division"
fi
done < <(canonical)
# --- result ----------------------------------------------------------------
count="$(canonical | wc -l | tr -d ' ')"
if [[ $errors -gt 0 ]]; then
echo ""
echo "FAILED: $errors divisions consistency error(s). $JSON is the source of truth."
exit 1
fi
echo "PASSED: $count divisions consistent across $JSON, directories, scripts, and CI."
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
#
# check-runbooks.sh — enforce that strategy/runbooks.json stays in sync with the
# real agent roster.
#
# strategy/runbooks.json is the machine-readable roster for the NEXUS scenario
# runbooks: the Agency Agents app reads it to turn a runbook into a one-click
# team deploy, mapping each roster slug to a catalog agent. If a slug there
# doesn't resolve to a real agent file, the app can't deploy that team — so this
# check fails the build when:
# 1. runbooks.json is not valid JSON, or an entry is missing a required field
# 2. any roster `agents[]` slug does not match an agent .md filename stem
# 3. any `doc` path does not exist
# 4. a runbook `slug` is duplicated
#
# Slugs are the agent .md filename stem (the corpus id), e.g.
# engineering/engineering-frontend-developer.md -> "engineering-frontend-developer".
# Uses python3 (already required by check-agent-originality.sh) for JSON; no jq,
# so it runs the same on macOS and CI. Mirrors scripts/check-divisions.sh.
#
# Usage: ./scripts/check-runbooks.sh
set -euo pipefail
cd "$(dirname "$0")/.."
command -v python3 >/dev/null 2>&1 || {
echo "ERROR: python3 is required for the runbooks check." >&2
exit 2
}
python3 - <<'PYEOF'
import json, os, subprocess, sys
JSON = "strategy/runbooks.json"
errors = []
if not os.path.isfile(JSON):
print(f"ERROR {JSON} not found"); sys.exit(1)
try:
data = json.load(open(JSON))
except json.JSONDecodeError as e:
print(f"ERROR {JSON} is not valid JSON: {e}"); sys.exit(1)
# Real slugs = filename stems of tracked agent .md files under division dirs.
NON_DIVISION = {"integrations", "examples", "strategy", "scripts", ".github"}
tracked = subprocess.check_output(["git", "ls-files", "*/*.md"]).decode().splitlines()
real = {os.path.basename(p)[:-3] for p in tracked if p.split("/")[0] not in NON_DIVISION}
runbooks = data.get("runbooks")
if not isinstance(runbooks, list) or not runbooks:
print(f"ERROR {JSON} has no 'runbooks' array"); sys.exit(1)
seen_slugs = set()
total_refs = 0
for rb in runbooks:
rid = rb.get("slug", "<no slug>")
for field in ("slug", "title", "mode", "doc", "roster"):
if field not in rb:
errors.append(f"runbook '{rid}' is missing required field \"{field}\"")
if rb.get("slug") in seen_slugs:
errors.append(f"duplicate runbook slug '{rb.get('slug')}'")
seen_slugs.add(rb.get("slug"))
doc = rb.get("doc")
if doc and not os.path.isfile(doc):
errors.append(f"runbook '{rid}': doc path does not exist: {doc}")
for g in rb.get("roster", []):
for slug in g.get("agents", []):
total_refs += 1
if slug not in real:
errors.append(f"runbook '{rid}' / group '{g.get('group','?')}': "
f"slug '{slug}' does not match any agent .md filename stem")
if errors:
print(f"FAILED: {len(errors)} runbook consistency error(s). "
f"strategy/runbooks.json must reference real agent slugs.\n")
for e in errors:
print(f" ERROR {e}")
sys.exit(1)
print(f"PASSED: {len(runbooks)} runbooks, {total_refs} agent slug references — "
f"all resolve to real agent files.")
PYEOF
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
#
# check-tools.sh — enforce a single source of truth for the supported tool set.
#
# tools.json (repo root) is canonical. This script fails if any of the following
# disagree with it:
# 1. ALL_TOOLS in scripts/install.sh (exact set — every installable tool)
# 2. valid_tools in scripts/convert.sh (every converter tool must exist in tools.json)
# 3. Every tools.json entry has id, label, kebab, format, installKind, dest
# (installKind is one of: per-agent | roster | plugin)
#
# Add a tool: add an entry to tools.json, a convert_<tool> (or reuse a `format`)
# in convert.sh, and an install_<tool> in install.sh, then run this script — it
# tells you every place that must agree. No deps beyond bash 3.2 + coreutils
# (no jq) so it runs the same on macOS and CI. Mirrors scripts/check-divisions.sh.
#
# Usage: ./scripts/check-tools.sh
set -euo pipefail
cd "$(dirname "$0")/.."
JSON="tools.json"
errors=0
fail() { echo "ERROR $*"; errors=$((errors + 1)); }
# --- helpers ---------------------------------------------------------------
# Canonical tool keys (kebab) from tools.json: the keys at 4-space indent inside
# the "tools" object. One tool per line keeps the nested "scope"/"detect"/…
# objects off the line start, so only tool keys match.
canonical() {
awk '/"tools"[[:space:]]*:[[:space:]]*\{/{f=1; next} f' "$JSON" \
| grep -oE '^ "[a-z0-9-]+"' \
| sed -E 's/.*"([a-z0-9-]+)".*/\1/' | sort -u
}
# Entries of a single-line bash array NAME=( ... ) (quoted or bare), one per line.
bash_array() {
grep -oE "$2=\([^)]*\)" "$1" | head -1 | sed -E "s/^$2=\(//; s/\)\$//" \
| tr -d '"' | tr ' \t' '\n\n' | grep -E '^[a-z0-9-]+$' | sort -u
}
# --- checks ----------------------------------------------------------------
[[ -f "$JSON" ]] || { echo "ERROR $JSON not found at repo root"; exit 1; }
canon="$(canonical)"
# 1. tools.json keys == ALL_TOOLS in install.sh (exact, both directions).
all_tools="$(bash_array scripts/install.sh ALL_TOOLS)"
missing="$(comm -23 <(echo "$canon") <(echo "$all_tools"))"
extra="$(comm -13 <(echo "$canon") <(echo "$all_tools"))"
[[ -n "$missing" ]] && fail "scripts/install.sh ALL_TOOLS is missing tool(s) in $JSON: $(echo $missing)"
[[ -n "$extra" ]] && fail "scripts/install.sh ALL_TOOLS has tool(s) not in $JSON: $(echo $extra)"
# 2. Every converter in convert.sh must exist in tools.json (subset; identity
# tools like claude-code/copilot are install-only, so it's a subset not equal).
conv="$(bash_array scripts/convert.sh valid_tools | grep -v '^all$' || true)"
notin="$(comm -13 <(echo "$canon") <(echo "$conv"))"
[[ -n "$notin" ]] && fail "scripts/convert.sh converts tool(s) absent from $JSON: $(echo $notin)"
# 3. Required fields per entry (each tool is one line). aa converts+installs
# every listed tool, so every entry must carry format + dest — there is no
# "half-described" tool. (Renderer coverage is a consumer's concern, derived
# from `format`; the catalog itself carries no such flag.)
while IFS= read -r t; do
[[ -n "$t" ]] || continue
line="$(grep -E "^ \"$t\"[[:space:]]*:" "$JSON")"
for field in id label kebab format installKind dest; do
echo "$line" | grep -qE "\"$field\":" || fail "tool '$t' in $JSON is missing \"$field\""
done
# installKind is the install MECHANISM (upstream truth), not app state: it must
# be one of the known kinds so every consumer can branch on it deterministically.
if echo "$line" | grep -qE '"installKind":'; then
echo "$line" | grep -qE '"installKind":[[:space:]]*"(per-agent|roster|plugin)"' \
|| fail "tool '$t' in $JSON has an invalid installKind (must be per-agent|roster|plugin)"
fi
done < <(echo "$canon")
# --- result ----------------------------------------------------------------
count="$(echo "$canon" | grep -c .)"
if [[ $errors -gt 0 ]]; then
echo ""
echo "FAILED: $errors tool consistency error(s). $JSON is the source of truth."
exit 1
fi
echo "PASSED: $count tools consistent across $JSON, install.sh, and convert.sh."
+770
View File
@@ -0,0 +1,770 @@
#!/usr/bin/env bash
#
# convert.sh — Convert agency agent .md files into tool-specific formats.
#
# Reads all agent files from the standard category directories and outputs
# converted files to integrations/<tool>/. Run this to regenerate all
# integration files after adding or modifying agents.
#
# Usage:
# ./scripts/convert.sh [--tool <name>] [--out <dir>] [--parallel] [--jobs N] [--help]
#
# Tools:
# antigravity — Antigravity skill files (~/.gemini/config/skills/)
# gemini-cli — Gemini CLI subagent files (~/.gemini/agents/*.md)
# opencode — OpenCode agent files (.opencode/agents/*.md)
# cursor — Cursor rule files (.cursor/rules/*.mdc)
# aider — Single CONVENTIONS.md for Aider
# windsurf — Single .windsurfrules for Windsurf
# openclaw — OpenClaw workspaces (integrations/openclaw/<agent>/SOUL.md)
# qwen — Qwen Code SubAgent files (~/.qwen/agents/*.md)
# zcode — ZCode agent files (.zcode/agents/*.md · ~/.config/zcode/agents/*.md)
# kimi — Kimi Code CLI agent files (~/.config/kimi/agents/)
# codex — Codex custom agent TOML files (~/.codex/agents/*.toml)
# osaurus — Osaurus skill files (~/.osaurus/skills/<name>/SKILL.md)
# hermes — Hermes lazy-router plugin (one plugin + on-disk agent index)
# vibe — Mistral Vibe agent TOML + prompt files (~/.vibe/agents/*.toml + ~/.vibe/prompts/*.md)
# all — All tools (default)
#
# Output is written to integrations/<tool>/ relative to the repo root.
# This script never touches user config dirs — see install.sh for that.
#
# --parallel When tool is 'all', run independent tools in parallel (output order may vary).
# --jobs N Max parallel jobs when using --parallel (default: nproc or 4).
set -euo pipefail
# --- Colour helpers ---
if [[ -t 1 && -z "${NO_COLOR:-}" && "${TERM:-}" != "dumb" ]]; then
GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'; RED=$'\033[0;31m'; BOLD=$'\033[1m'; RESET=$'\033[0m'
else
GREEN=''; YELLOW=''; RED=''; BOLD=''; RESET=''
fi
info() { printf "${GREEN}[OK]${RESET} %s\n" "$*"; }
warn() { printf "${YELLOW}[!!]${RESET} %s\n" "$*"; }
error() { printf "${RED}[ERR]${RESET} %s\n" "$*" >&2; }
header() { echo -e "\n${BOLD}$*${RESET}"; }
# Progress bar: [=======> ] 3/8 (tqdm-style)
progress_bar() {
local current="$1" total="$2" width="${3:-20}" i filled empty
(( total > 0 )) || return
filled=$(( width * current / total ))
empty=$(( width - filled ))
printf "\r ["
for (( i=0; i<filled; i++ )); do printf "="; done
if (( filled < width )); then printf ">"; (( empty-- )); fi
for (( i=0; i<empty; i++ )); do printf " "; done
printf "] %s/%s" "$current" "$total"
[[ -t 1 ]] || printf "\n"
}
# --- Paths ---
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OUT_DIR="$REPO_ROOT/integrations"
TODAY="$(date +%Y-%m-%d)"
# Shared helpers (get_field, get_body, slugify, ...)
# shellcheck source=lib.sh
. "$SCRIPT_DIR/lib.sh"
AGENT_DIRS=(
academic design engineering finance game-development gis healthcare marketing paid-media product project-management
sales security spatial-computing specialized support testing
)
# --- Usage ---
usage() {
sed -n '3,27p' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
# Default parallel job count (nproc on Linux; sysctl on macOS when nproc missing)
parallel_jobs_default() {
local n
n=$(nproc 2>/dev/null) && [[ -n "$n" ]] && echo "$n" && return
n=$(sysctl -n hw.ncpu 2>/dev/null) && [[ -n "$n" ]] && echo "$n" && return
echo 4
}
# --- Frontmatter helpers: get_field / get_body / slugify now live in lib.sh ---
# Escape a value for a TOML basic string, including control characters that
# cannot appear raw in TOML source.
toml_escape_string() {
printf '%s' "$1" | perl -0pe '
s/\\/\\\\/g;
s/"/\\"/g;
s/\n/\\n/g;
s/\r/\\r/g;
s/\t/\\t/g;
s/\f/\\f/g;
s/\x08/\\b/g;
s/([\x00-\x07\x0B\x0E-\x1F\x7F])/sprintf("\\u%04X", ord($1))/ge;
'
}
# --- Per-tool converters ---
convert_antigravity() {
local file="$1"
local name description slug outdir outfile body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
slug="agency-$(slugify "$name")"
body="$(get_body "$file")"
outdir="$OUT_DIR/antigravity/$slug"
outfile="$outdir/SKILL.md"
mkdir -p "$outdir"
# Antigravity Agent-Skills SKILL.md — name + description frontmatter and the
# persona as the body, installed into ~/.gemini/config/skills/ (global) or
# <project>/.agents/skills/ (project). Standard fields only, so it stays a
# valid Agent-Skills skill for any host (and deterministic — no date stamp).
cat > "$outfile" <<HEREDOC
---
name: ${slug}
description: ${description}
---
${body}
HEREDOC
}
convert_osaurus() {
local file="$1"
local name description slug outdir outfile body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
slug="agency-$(slugify "$name")"
body="$(get_body "$file")"
# Stage one dir per skill (install.sh copies into ~/.osaurus/skills/<name>/).
outdir="$OUT_DIR/osaurus/$slug"
outfile="$outdir/SKILL.md"
mkdir -p "$outdir"
# Osaurus skill format: the Anthropic "Agent Skills" SKILL.md — a directory
# named for the skill containing a SKILL.md with name + description frontmatter
# and the persona as the instruction body. Installs into ~/.osaurus/skills/.
# Kept to the standard fields so it stays compatible with any Agent-Skills host.
cat > "$outfile" <<HEREDOC
---
name: ${slug}
description: ${description}
---
${body}
HEREDOC
}
convert_codex() {
local file="$1"
local name description slug outfile body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
outfile="$OUT_DIR/codex/agents/${slug}.toml"
mkdir -p "$(dirname "$outfile")"
# Codex custom agent format: one TOML file per agent with minimal required
# fields only. Use a TOML basic string so control characters in the source
# body are encoded safely instead of producing invalid TOML.
cat > "$outfile" <<HEREDOC
name = "$(toml_escape_string "$name")"
description = "$(toml_escape_string "$description")"
developer_instructions = "$(toml_escape_string "$body")"
HEREDOC
}
convert_gemini_cli() {
local file="$1"
local name description slug outdir outfile body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
# Gemini CLI subagent format: .md file in ~/.gemini/agents/
outdir="$OUT_DIR/gemini-cli/agents"
outfile="$outdir/${slug}.md"
mkdir -p "$outdir"
cat > "$outfile" <<HEREDOC
---
name: ${slug}
description: ${description}
---
${body}
HEREDOC
}
# Map known color names and normalize to OpenCode-safe #RRGGBB values.
resolve_opencode_color() {
local c="$1"
local mapped
c="$(printf '%s' "$c" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]')"
case "$c" in
cyan) mapped="#00FFFF" ;;
blue) mapped="#3498DB" ;;
green) mapped="#2ECC71" ;;
red) mapped="#E74C3C" ;;
purple) mapped="#9B59B6" ;;
orange) mapped="#F39C12" ;;
teal) mapped="#008080" ;;
indigo) mapped="#6366F1" ;;
pink) mapped="#E84393" ;;
gold) mapped="#EAB308" ;;
amber) mapped="#F59E0B" ;;
neon-green) mapped="#10B981" ;;
neon-cyan) mapped="#06B6D4" ;;
metallic-blue) mapped="#3B82F6" ;;
yellow) mapped="#EAB308" ;;
violet) mapped="#8B5CF6" ;;
rose) mapped="#F43F5E" ;;
lime) mapped="#84CC16" ;;
gray) mapped="#6B7280" ;;
fuchsia) mapped="#D946EF" ;;
*) mapped="$c" ;;
esac
if [[ "$mapped" =~ ^#[0-9a-fA-F]{6}$ ]]; then
printf '#%s\n' "$(printf '%s' "${mapped#\#}" | tr '[:lower:]' '[:upper:]')"
return
fi
if [[ "$mapped" =~ ^[0-9a-fA-F]{6}$ ]]; then
printf '#%s\n' "$(printf '%s' "$mapped" | tr '[:lower:]' '[:upper:]')"
return
fi
printf '#6B7280\n'
}
convert_opencode() {
local file="$1"
local name description color slug outfile body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
color="$(resolve_opencode_color "$(get_field "color" "$file")")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
outfile="$OUT_DIR/opencode/agents/${slug}.md"
mkdir -p "$OUT_DIR/opencode/agents"
# OpenCode agent format: .md with YAML frontmatter in .opencode/agents/.
# Named colors are resolved to hex via resolve_opencode_color().
cat > "$outfile" <<HEREDOC
---
name: ${name}
description: ${description}
mode: subagent
color: '${color}'
---
${body}
HEREDOC
}
convert_cursor() {
local file="$1"
local name description slug outfile body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
outfile="$OUT_DIR/cursor/rules/${slug}.mdc"
mkdir -p "$OUT_DIR/cursor/rules"
# Cursor .mdc format: description + globs + alwaysApply frontmatter
cat > "$outfile" <<HEREDOC
---
description: ${description}
globs: ""
alwaysApply: false
---
${body}
HEREDOC
}
convert_openclaw() {
local file="$1"
local name description slug outdir body
local soul_content="" agents_content=""
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
outdir="$OUT_DIR/openclaw/$slug"
mkdir -p "$outdir"
# Split body sections into SOUL.md (persona) vs AGENTS.md (operations)
# by matching ## header keywords. Unmatched sections go to AGENTS.md.
#
# SOUL keywords: identity, learning & memory, communication, style,
# critical rules, rules you must follow
# AGENTS keywords: everything else (mission, deliverables, workflow, etc.)
local current_target="agents" # default bucket
local current_section=""
while IFS= read -r line; do
# Detect ## headers (with or without emoji prefixes)
if [[ "$line" =~ ^##[[:space:]] ]]; then
# Flush previous section
if [[ -n "$current_section" ]]; then
if [[ "$current_target" == "soul" ]]; then
soul_content+="$current_section"
else
agents_content+="$current_section"
fi
fi
current_section=""
# Classify this header by keyword (case-insensitive)
local header_lower
header_lower="$(echo "$line" | tr '[:upper:]' '[:lower:]')"
if [[ "$header_lower" =~ identity ]] ||
[[ "$header_lower" =~ learning.*memory ]] ||
[[ "$header_lower" =~ communication ]] ||
[[ "$header_lower" =~ style ]] ||
[[ "$header_lower" =~ critical.rule ]] ||
[[ "$header_lower" =~ rules.you.must.follow ]]; then
current_target="soul"
else
current_target="agents"
fi
fi
current_section+="$line"$'\n'
done <<< "$body"
# Flush final section
if [[ -n "$current_section" ]]; then
if [[ "$current_target" == "soul" ]]; then
soul_content+="$current_section"
else
agents_content+="$current_section"
fi
fi
# Write SOUL.md — persona, tone, boundaries
cat > "$outdir/SOUL.md" <<HEREDOC
${soul_content}
HEREDOC
# Write AGENTS.md — mission, deliverables, workflow
cat > "$outdir/AGENTS.md" <<HEREDOC
${agents_content}
HEREDOC
# Write IDENTITY.md — emoji + name + vibe from frontmatter, fallback to description
local emoji vibe
emoji="$(get_field "emoji" "$file")"
vibe="$(get_field "vibe" "$file")"
if [[ -n "$emoji" && -n "$vibe" ]]; then
cat > "$outdir/IDENTITY.md" <<HEREDOC
# ${emoji} ${name}
${vibe}
HEREDOC
else
cat > "$outdir/IDENTITY.md" <<HEREDOC
# ${name}
${description}
HEREDOC
fi
}
convert_qwen() {
local file="$1"
local name description tools slug outfile body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
tools="$(get_field "tools" "$file")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
outfile="$OUT_DIR/qwen/agents/${slug}.md"
mkdir -p "$(dirname "$outfile")"
# Qwen Code SubAgent format: .md with YAML frontmatter in ~/.qwen/agents/
# name and description required; tools optional (only if present in source)
if [[ -n "$tools" ]]; then
cat > "$outfile" <<HEREDOC
---
name: ${slug}
description: ${description}
tools: ${tools}
---
${body}
HEREDOC
else
cat > "$outfile" <<HEREDOC
---
name: ${slug}
description: ${description}
---
${body}
HEREDOC
fi
}
convert_zcode() {
local file="$1"
local name description tools slug outfile body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
tools="$(get_field "tools" "$file")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
outfile="$OUT_DIR/zcode/agents/${slug}.md"
mkdir -p "$(dirname "$outfile")"
# ZCode agent format (Z.ai GLM harness): .md with YAML frontmatter in
# .zcode/agents/ (project) or ~/.config/zcode/agents/ (global). name and
# description required; tools optional (only if present in source). Byte-
# identical to the qwen-md shape, which the Agency Agents app renders natively.
if [[ -n "$tools" ]]; then
cat > "$outfile" <<HEREDOC
---
name: ${slug}
description: ${description}
tools: ${tools}
---
${body}
HEREDOC
else
cat > "$outfile" <<HEREDOC
---
name: ${slug}
description: ${description}
---
${body}
HEREDOC
fi
}
convert_kimi() {
local file="$1"
local name description slug outdir agent_file body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
outdir="$OUT_DIR/kimi/$slug"
agent_file="$outdir/agent.yaml"
mkdir -p "$outdir"
# Kimi Code CLI agent format: YAML with separate system prompt file
# Uses extend: default to inherit Kimi's default toolset
cat > "$agent_file" <<HEREDOC
version: 1
agent:
name: ${slug}
extend: default
system_prompt_path: ./system.md
HEREDOC
# Write system prompt to separate file
cat > "$outdir/system.md" <<HEREDOC
# ${name}
${description}
${body}
HEREDOC
}
convert_vibe() {
local file="$1"
local name description slug outdir agent_file prompt_file body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
# Mistral Vibe uses two files per agent:
# 1. A TOML configuration file in ~/.vibe/agents/<slug>.toml
# 2. A markdown prompt file in ~/.vibe/prompts/<slug>.md
outdir="$OUT_DIR/vibe"
agent_file="$outdir/agents/${slug}.toml"
prompt_file="$outdir/prompts/${slug}.md"
mkdir -p "$outdir/agents" "$outdir/prompts"
# Write the TOML agent configuration
cat > "$agent_file" <<HEREDOC
agent_type = "agent"
system_prompt_id = "${slug}"
HEREDOC
# Write the markdown prompt file
cat > "$prompt_file" <<HEREDOC
# ${name}
${description}
${body}
HEREDOC
}
# Aider and Windsurf are single-file formats — accumulate into temp files
# then write at the end.
AIDER_TMP="$(mktemp)"
WINDSURF_TMP="$(mktemp)"
trap 'rm -f "$AIDER_TMP" "$WINDSURF_TMP"' EXIT
# Write Aider/Windsurf headers once
cat > "$AIDER_TMP" <<'HEREDOC'
# The Agency — AI Agent Conventions
#
# This file provides Aider with the full roster of specialized AI agents from
# The Agency (https://github.com/msitarzewski/agency-agents).
#
# To activate an agent, reference it by name in your Aider session prompt, e.g.:
# "Use the Frontend Developer agent to review this component."
#
# Generated by scripts/convert.sh — do not edit manually.
HEREDOC
cat > "$WINDSURF_TMP" <<'HEREDOC'
# The Agency — AI Agent Rules for Windsurf
#
# Full roster of specialized AI agents from The Agency.
# To activate an agent, reference it by name in your Windsurf conversation.
#
# Generated by scripts/convert.sh — do not edit manually.
HEREDOC
accumulate_aider() {
local file="$1"
local name description body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
body="$(get_body "$file")"
cat >> "$AIDER_TMP" <<HEREDOC
---
## ${name}
> ${description}
${body}
HEREDOC
}
accumulate_windsurf() {
local file="$1"
local name description body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
body="$(get_body "$file")"
cat >> "$WINDSURF_TMP" <<HEREDOC
================================================================================
## ${name}
${description}
================================================================================
${body}
HEREDOC
}
# --- Main loop ---
# Remove a tool's previously-generated output before regenerating, so renamed or
# deleted agents don't leave orphan files behind (convert.sh overwrites in place
# but never pruned stale output). Preserves the committed README.md — the only
# tracked file under integrations/<tool>/ for conversion targets.
clean_tool_output() {
local dir="$OUT_DIR/$1"
[[ -d "$dir" ]] || return 0
find "$dir" -mindepth 1 -maxdepth 1 ! -name 'README.md' -exec rm -rf {} +
}
run_conversions() {
local tool="$1"
local count=0
if [[ "$tool" == "hermes" ]]; then
clean_tool_output "$tool"
python3 "$SCRIPT_DIR/build-hermes-plugin.py" --repo-root "$REPO_ROOT" --out "$OUT_DIR/hermes"
return
fi
clean_tool_output "$tool"
for dir in "${AGENT_DIRS[@]}"; do
local dirpath="$REPO_ROOT/$dir"
[[ -d "$dirpath" ]] || continue
while IFS= read -r -d '' file; do
# Skip files without frontmatter (non-agent docs like QUICKSTART.md)
local first_line
first_line="$(head -1 "$file")"
[[ "$first_line" == "---" ]] || continue
local name
name="$(get_field "name" "$file")"
[[ -n "$name" ]] || continue
case "$tool" in
antigravity) convert_antigravity "$file" ;;
codex) convert_codex "$file" ;;
gemini-cli) convert_gemini_cli "$file" ;;
opencode) convert_opencode "$file" ;;
cursor) convert_cursor "$file" ;;
openclaw) convert_openclaw "$file" ;;
qwen) convert_qwen "$file" ;;
zcode) convert_zcode "$file" ;;
kimi) convert_kimi "$file" ;;
osaurus) convert_osaurus "$file" ;;
vibe) convert_vibe "$file" ;;
aider) accumulate_aider "$file" ;;
windsurf) accumulate_windsurf "$file" ;;
esac
(( count++ )) || true
done < <(find "$dirpath" -name "*.md" -type f -print0 | sort -z)
done
echo "$count"
}
# --- Entry point ---
main() {
local tool="all"
local use_parallel=false
local parallel_jobs
parallel_jobs="$(parallel_jobs_default)"
while [[ $# -gt 0 ]]; do
case "$1" in
--tool) tool="${2:?'--tool requires a value'}"; shift 2 ;;
--out) OUT_DIR="${2:?'--out requires a value'}"; shift 2 ;;
--parallel) use_parallel=true; shift ;;
--jobs) parallel_jobs="${2:?'--jobs requires a value'}"; shift 2 ;;
--help|-h) usage ;;
*) error "Unknown option: $1"; usage ;;
esac
done
local valid_tools=("antigravity" "gemini-cli" "opencode" "cursor" "aider" "windsurf" "openclaw" "qwen" "zcode" "kimi" "codex" "osaurus" "hermes" "vibe" "all")
local valid=false
for t in "${valid_tools[@]}"; do [[ "$t" == "$tool" ]] && valid=true && break; done
if ! $valid; then
error "Unknown tool '$tool'. Valid: ${valid_tools[*]}"
exit 1
fi
header "The Agency -- Converting agents to tool-specific formats"
echo " Repo: $REPO_ROOT"
echo " Output: $OUT_DIR"
echo " Tool: $tool"
echo " Date: $TODAY"
if $use_parallel && [[ "$tool" == "all" ]]; then
info "Parallel mode: output buffered so each tool's output stays together."
fi
local tools_to_run=()
if [[ "$tool" == "all" ]]; then
tools_to_run=("antigravity" "gemini-cli" "opencode" "cursor" "aider" "windsurf" "openclaw" "qwen" "zcode" "kimi" "codex" "osaurus" "hermes" "vibe")
else
tools_to_run=("$tool")
fi
local total=0
local n_tools=${#tools_to_run[@]}
if $use_parallel && [[ "$tool" == "all" ]]; then
# Tools that write to separate dirs can run in parallel; buffer output so each tool's output stays together
local parallel_tools=(antigravity gemini-cli opencode cursor openclaw qwen zcode codex osaurus hermes vibe)
local parallel_out_dir
parallel_out_dir="$(mktemp -d)"
info "Converting: ${#parallel_tools[@]}/${n_tools} tools in parallel (output buffered per tool)..."
export AGENCY_CONVERT_OUT_DIR="$parallel_out_dir"
export AGENCY_CONVERT_SCRIPT="$SCRIPT_DIR/convert.sh"
export AGENCY_CONVERT_OUT="$OUT_DIR"
printf '%s\n' "${parallel_tools[@]}" | xargs -P "$parallel_jobs" -I {} sh -c '"$AGENCY_CONVERT_SCRIPT" --tool "{}" --out "$AGENCY_CONVERT_OUT" > "$AGENCY_CONVERT_OUT_DIR/{}" 2>&1'
for t in "${parallel_tools[@]}"; do
[[ -f "$parallel_out_dir/$t" ]] && cat "$parallel_out_dir/$t"
done
rm -rf "$parallel_out_dir"
local idx=8
for t in aider windsurf; do
progress_bar "$idx" "$n_tools"
printf "\n"
header "Converting: $t ($idx/$n_tools)"
local count
count="$(run_conversions "$t")"
total=$(( total + count ))
info "Converted $count agents for $t"
(( idx++ )) || true
done
else
local i=0
for t in "${tools_to_run[@]}"; do
(( i++ )) || true
progress_bar "$i" "$n_tools"
printf "\n"
header "Converting: $t ($i/$n_tools)"
local count
count="$(run_conversions "$t")"
total=$(( total + count ))
info "Converted $count agents for $t"
done
fi
# Write single-file outputs after accumulation
if [[ "$tool" == "all" || "$tool" == "aider" ]]; then
mkdir -p "$OUT_DIR/aider"
cp "$AIDER_TMP" "$OUT_DIR/aider/CONVENTIONS.md"
info "Wrote integrations/aider/CONVENTIONS.md"
fi
if [[ "$tool" == "all" || "$tool" == "windsurf" ]]; then
mkdir -p "$OUT_DIR/windsurf"
cp "$WINDSURF_TMP" "$OUT_DIR/windsurf/.windsurfrules"
info "Wrote integrations/windsurf/.windsurfrules"
fi
echo ""
if $use_parallel && [[ "$tool" == "all" ]]; then
info "Done. $n_tools tools (parallel; total conversions not aggregated)."
else
info "Done. Total conversions: $total"
fi
}
main "$@"
+63
View File
@@ -0,0 +1,63 @@
# 🇨🇳 Chinese (zh-CN) Localization
Localize agent `name` and `description` fields in YAML frontmatter to Simplified Chinese. This makes agent names readable in Copilot Chat's agent picker for Chinese-speaking users.
## Files
| File | Description |
|------|-------------|
| `agent-names-zh.json` | Mapping of English agent names → Chinese translations (130+ entries) |
| `localize-agents-zh.ps1` | PowerShell script that reads the JSON and updates installed agent files |
## Usage
After installing agents with `install.sh --tool copilot`:
```powershell
# Localize agent names to Chinese
powershell -ExecutionPolicy Bypass -File scripts/i18n/localize-agents-zh.ps1
```
By default, the script processes:
- `%USERPROFILE%\.github\agents\`
- `%USERPROFILE%\.copilot\agents\`
Pass custom paths if needed:
```powershell
powershell -File scripts/i18n/localize-agents-zh.ps1 -TargetDirs @("C:\custom\path\agents")
```
## How It Works
1. Reads `agent-names-zh.json` (UTF-8 encoded) for the translation map
2. For each `.md` file in the target directories:
- Extracts the `name:` field from YAML frontmatter
- Looks up the Chinese translation
- Replaces `name:` and `description:` fields
- Writes back as UTF-8
## Result
Before:
```yaml
---
name: Security Engineer
description: Threat modeling, secure code review, security architecture
---
```
After:
```yaml
---
name: 安全工程师
description: 威胁建模、安全代码审查与应用安全架构专家
---
```
## Notes
- Only modifies **installed copies** (in `~/.github/agents/`), not source files
- Re-run after each `install.sh` update (which overwrites with English originals)
- JSON file is the single source of truth for translations — add new agents there
- Script is pure ASCII (avoids PowerShell encoding issues); all Chinese text lives in the JSON
+154
View File
@@ -0,0 +1,154 @@
{
"Frontend Developer": { "name": "前端开发工程师", "description": "专注现代 Web 技术、React/Vue/Angular 框架、UI 实现与性能优化的前端专家" },
"Backend Architect": { "name": "后端架构师", "description": "负责 API 设计、数据库架构与可扩展性的后端系统专家" },
"Mobile App Builder": { "name": "移动端开发工程师", "description": "iOS/Android、React Native、Flutter 跨平台移动应用构建者" },
"AI Engineer": { "name": "AI 工程师", "description": "机器学习模型部署、AI 集成与数据管道专家" },
"DevOps Automator": { "name": "DevOps 自动化工程师", "description": "CI/CD、基础设施自动化与云运营专家" },
"Rapid Prototyper": { "name": "快速原型工程师", "description": "快速 POC 开发、MVP 与迭代验证专家" },
"Senior Developer": { "name": "高级开发工程师", "description": "Laravel/Livewire、复杂模式与架构决策专家" },
"Security Engineer": { "name": "安全工程师", "description": "威胁建模、安全代码审查与应用安全架构专家" },
"Autonomous Optimization Architect": { "name": "自主优化架构师", "description": "LLM 路由、成本优化与影子测试专家" },
"Embedded Firmware Engineer": { "name": "嵌入式固件工程师", "description": "裸金属、RTOS、ESP32/STM32/Nordic 固件开发专家" },
"Incident Response Commander":{ "name": "故障响应指挥官", "description": "事件管理、故障复盘与值班应急专家" },
"Solidity Smart Contract Engineer": { "name": "Solidity 智能合约工程师", "description": "EVM 合约、Gas 优化与 DeFi 协议专家" },
"Technical Writer": { "name": "技术文档工程师", "description": "开发者文档、API 参考手册与教程撰写专家" },
"Threat Detection Engineer": { "name": "威胁检测工程师", "description": "SIEM 规则、威胁狩猎与 ATT&CK 映射专家" },
"WeChat Mini Program Developer": { "name": "微信小程序开发工程师", "description": "微信生态、小程序与支付集成开发专家" },
"Code Reviewer": { "name": "代码审查工程师", "description": "建设性代码审查、安全与可维护性评估专家" },
"Database Optimizer": { "name": "数据库优化工程师", "description": "Schema 设计、查询优化与索引策略专家(PostgreSQL/MySQL" },
"Git Workflow Master": { "name": "Git 工作流专家", "description": "分支策略、规范提交与高级 Git 操作专家" },
"Software Architect": { "name": "软件架构师", "description": "系统设计、DDD、架构模式与权衡分析专家" },
"SRE": { "name": "站点可靠性工程师", "description": "SLO、错误预算、可观测性与混沌工程专家" },
"AI Data Remediation Engineer": { "name": "AI 数据修复工程师", "description": "自愈数据管道、离线 SLM 与语义聚类专家" },
"Data Engineer": { "name": "数据工程师", "description": "数据管道、湖仓架构与 ETL/ELT 专家" },
"Feishu Integration Developer": { "name": "飞书集成开发工程师", "description": "飞书/Lark 开放平台、机器人与工作流集成专家" },
"UI Designer": { "name": "UI 设计师", "description": "视觉设计、组件库与设计系统专家" },
"UX Researcher": { "name": "用户体验研究员", "description": "用户测试、行为分析与可用性研究专家" },
"UX Architect": { "name": "用户体验架构师", "description": "技术架构、CSS 系统与前端实现指导专家" },
"Brand Guardian": { "name": "品牌守护者", "description": "品牌认知、一致性与品牌定位专家" },
"Visual Storyteller": { "name": "视觉叙事师", "description": "视觉叙事、多媒体内容与品牌故事专家" },
"Whimsy Injector": { "name": "创意注入师", "description": "品牌个性、微互动与趣味体验设计专家" },
"Image Prompt Engineer": { "name": "图像提示词工程师", "description": "AI 图像生成提示词、摄影风格指令专家" },
"Inclusive Visuals Specialist": { "name": "包容性视觉专家", "description": "多元化呈现、偏见消除与真实 AI 图像生成专家" },
"Growth Hacker": { "name": "增长黑客", "description": "快速用户获取、病毒循环与实验驱动增长专家" },
"Content Creator": { "name": "内容创作者", "description": "多平台内容策略、编辑日历与文案专家" },
"Twitter Engager": { "name": "Twitter 运营专家", "description": "实时互动、思想领导力与推特策略专家" },
"TikTok Strategist": { "name": "TikTok 策略专家", "description": "病毒内容、算法优化与 TikTok 增长专家" },
"Instagram Curator": { "name": "Instagram 运营专家", "description": "视觉叙事、社区运营与 Instagram 策略专家" },
"Reddit Community Builder": { "name": "Reddit 社区运营", "description": "真实互动、价值内容与 Reddit 营销专家" },
"App Store Optimizer": { "name": "应用商店优化专家", "description": "ASO、转化率优化与应用曝光专家" },
"Social Media Strategist": { "name": "社交媒体策略师", "description": "跨平台策略、营销活动与社媒整体规划专家" },
"Xiaohongshu Specialist": { "name": "小红书运营专家", "description": "生活方式内容、趋势策略与小红书增长专家" },
"WeChat Official Account Manager": { "name": "微信公众号运营专家", "description": "粉丝互动、内容营销与微信公众号策略专家" },
"Zhihu Strategist": { "name": "知乎运营专家", "description": "思想领导力、知识驱动互动与知乎权威建立专家" },
"Baidu SEO Specialist": { "name": "百度 SEO 专家", "description": "百度优化、中国 SEO 与 ICP 合规专家" },
"Bilibili Content Strategist": { "name": "Bilibili 内容策略师", "description": "B站算法、弹幕文化与 UP 主成长专家" },
"Carousel Growth Engine": { "name": "轮播图增长引擎", "description": "TikTok/Instagram 轮播图创作与自动发布专家" },
"LinkedIn Content Creator": { "name": "领英内容创作者", "description": "个人品牌、思想领导力与领英专业内容专家" },
"China E-Commerce Operator": { "name": "中国电商运营专家", "description": "淘宝/天猫/拼多多与直播电商运营专家" },
"Kuaishou Strategist": { "name": "快手运营策略师", "description": "快手平台、老铁生态与下沉市场增长专家" },
"SEO Specialist": { "name": "SEO 专家", "description": "技术 SEO、内容策略与外链建设专家" },
"Book Co-Author": { "name": "图书联合作者", "description": "思想领导力书籍、代笔写作与出版策略专家" },
"Cross-Border E-Commerce Specialist": { "name": "跨境电商专家", "description": "亚马逊/Shopee/Lazada 与跨境履约全链路专家" },
"Douyin Strategist": { "name": "抖音运营策略师", "description": "抖音平台、短视频营销与算法增长专家" },
"Livestream Commerce Coach": { "name": "直播带货教练", "description": "主播培训、直播间优化与转化提升专家" },
"Podcast Strategist": { "name": "播客策略师", "description": "播客内容策略与平台运营专家" },
"Private Domain Operator": { "name": "私域运营专家", "description": "企业微信、私域流量与社群运营专家" },
"Short-Video Editing Coach": { "name": "短视频剪辑教练", "description": "后期制作、剪辑流程与平台规格优化专家" },
"Weibo Strategist": { "name": "微博运营策略师", "description": "微博热搜、话题营销与粉丝互动专家" },
"AI Citation Strategist": { "name": "AI 引用策略师", "description": "AEO/GEO、AI 推荐可见度与引用审计专家" },
"Outbound Strategist": { "name": "外呼销售策略师", "description": "基于信号的精准找客、多渠道序列与 ICP 定位专家" },
"Discovery Coach": { "name": "销售发现教练", "description": "SPIN、Gap Selling 与 Sandler 问题设计专家" },
"Deal Strategist": { "name": "商机策略师", "description": "MEDDPICC 资格认定、竞争定位与赢单策略专家" },
"Sales Engineer": { "name": "售前工程师", "description": "技术演示、POC 范围确定与竞争技术定位专家" },
"Proposal Strategist": { "name": "提案策略师", "description": "RFP 响应、赢单主题与叙事结构专家" },
"Pipeline Analyst": { "name": "销售漏斗分析师", "description": "预测、漏斗健康度、商机速度与 RevOps 专家" },
"Account Strategist": { "name": "客户策略师", "description": "拓客留存、QBR 与利益相关者地图专家" },
"Sales Coach": { "name": "销售教练", "description": "销售代表成长、通话辅导与管道审查促进专家" },
"PPC Campaign Strategist": { "name": "竞价广告策略师", "description": "Google/Microsoft/Amazon 广告、账户结构与出价专家" },
"Search Query Analyst": { "name": "搜索词分析师", "description": "搜索词分析、否定关键词与意图映射专家" },
"Paid Media Auditor": { "name": "付费媒体审计师", "description": "200+ 维度账户审计与竞争对手分析专家" },
"Tracking & Measurement Specialist": { "name": "追踪与埋点专家", "description": "GTM、GA4、转化追踪与 CAPI 实施专家" },
"Ad Creative Strategist": { "name": "广告创意策略师", "description": "RSA 文案、Meta 创意与 PMax 素材专家" },
"Programmatic & Display Buyer": { "name": "程序化广告购买专家", "description": "GDN、DSP、合作媒体与 ABM 展示广告专家" },
"Paid Social Strategist": { "name": "付费社交策略师", "description": "Meta/LinkedIn/TikTok 跨平台付费社交专家" },
"Sprint Prioritizer": { "name": "Sprint 优先级规划师", "description": "敏捷规划、功能优先级与 Sprint 管理专家" },
"Trend Researcher": { "name": "市场趋势研究员", "description": "市场情报、竞品分析与机会识别专家" },
"Feedback Synthesizer": { "name": "用户反馈综合分析师", "description": "用户反馈分析、洞察提取与产品优先级专家" },
"Behavioral Nudge Engine": { "name": "行为助推引擎", "description": "行为心理学、助推设计与用户激励专家" },
"Product Manager": { "name": "产品经理", "description": "全生命周期产品管理:发现、PRD、路线图、GTM" },
"Studio Producer": { "name": "工作室制作人", "description": "高层编排、投资组合管理与多项目监督专家" },
"Project Shepherd": { "name": "项目协调专家", "description": "跨职能协调、时间轴管理与端到端项目统筹专家" },
"Studio Operations": { "name": "工作室运营专家", "description": "日常效率优化、流程改进与生产支持专家" },
"Experiment Tracker": { "name": "实验追踪专家", "description": "A/B 测试、假设验证与数据驱动决策专家" },
"Senior Project Manager": { "name": "高级项目经理", "description": "现实范围评估与规格转任务分解专家" },
"Jira Workflow Steward": { "name": "Jira 工作流管理员", "description": "Git 工作流、分支策略与 Jira 关联交付规范专家" },
"Evidence Collector": { "name": "测试证据采集员", "description": "截图 QA、视觉验证与 Bug 文档专家" },
"Reality Checker": { "name": "生产就绪验证员", "description": "基于证据的认证、质量门与发布认证专家" },
"Test Results Analyzer": { "name": "测试结果分析师", "description": "测试评估、质量指标分析与覆盖率报告专家" },
"Performance Benchmarker": { "name": "性能基准测试专家", "description": "性能测试、压力测试与速度优化专家" },
"API Tester": { "name": "API 测试工程师", "description": "API 验证、集成测试与端点核查专家" },
"Tool Evaluator": { "name": "工具评估专家", "description": "技术评估与工具选型专家" },
"Workflow Optimizer": { "name": "工作流优化专家", "description": "流程分析、工作流改进与自动化机会挖掘专家" },
"Accessibility Auditor": { "name": "无障碍审计师", "description": "WCAG 审计、辅助技术测试与包容性设计专家" },
"Support Responder": { "name": "客户支持专员", "description": "客户服务、问题解决与支持运营专家" },
"Analytics Reporter": { "name": "数据分析报告员", "description": "数据分析、仪表板与业务洞察专家" },
"Finance Tracker": { "name": "财务追踪专员", "description": "财务规划、预算管理与业务绩效分析专家" },
"Infrastructure Maintainer": { "name": "基础设施维护工程师", "description": "系统可靠性、性能优化与基础设施运营专家" },
"Legal Compliance Checker": { "name": "法律合规检查员", "description": "合规审查、监管要求与风险管理专家" },
"Executive Summary Generator": { "name": "高管摘要生成师", "description": "C 级沟通、战略摘要与决策支持专家" },
"XR Interface Architect": { "name": "XR 界面架构师", "description": "空间交互设计与沉浸式 UX 专家(AR/VR/XR" },
"macOS Spatial/Metal Engineer": { "name": "macOS 空间/Metal 工程师", "description": "Swift、Metal 与高性能 3D macOS 空间计算专家" },
"XR Immersive Developer": { "name": "WebXR 沉浸式开发者", "description": "WebXR、浏览器端 AR/VR 沉浸式体验开发专家" },
"XR Cockpit Interaction Specialist": { "name": "XR 座舱交互专家", "description": "座舱控制系统与沉浸式控制界面专家" },
"visionOS Spatial Engineer": { "name": "visionOS 空间工程师", "description": "Apple Vision Pro 应用与空间计算体验开发专家" },
"Terminal Integration Specialist": { "name": "终端集成专家", "description": "终端集成、命令行工具与开发者工作流专家" },
"Agents Orchestrator": { "name": "多智能体编排师", "description": "多 Agent 协调、工作流管理与复杂项目统筹专家" },
"LSP/Index Engineer": { "name": "语言服务器/索引工程师", "description": "LSP 实现、代码智能与语义索引专家" },
"Sales Data Extraction Agent": { "name": "销售数据提取 Agent", "description": "Excel 监控与销售指标提取(MTD/YTD)专家" },
"Data Consolidation Agent": { "name": "数据整合 Agent", "description": "销售数据聚合与仪表板报告专家" },
"Report Distribution Agent": { "name": "报告分发 Agent", "description": "自动化报告交付与按区域定时发送专家" },
"Agentic Identity & Trust Architect": { "name": "智能体身份与信任架构师", "description": "Agent 身份、认证与信任验证专家" },
"Identity Graph Operator": { "name": "身份图谱运营专家", "description": "多 Agent 系统实体去重与身份一致性专家" },
"Accounts Payable Agent": { "name": "应付账款 Agent", "description": "支付处理、供应商管理与自主支付专家" },
"Blockchain Security Auditor": { "name": "区块链安全审计师", "description": "智能合约审计与漏洞分析专家" },
"Compliance Auditor": { "name": "合规审计师", "description": "SOC2/ISO27001/HIPAA/PCI-DSS 合规认证指导专家" },
"Cultural Intelligence Strategist": { "name": "文化智能策略师", "description": "全球 UX、多元呈现与文化排斥规避专家" },
"Developer Advocate": { "name": "开发者布道师", "description": "社区建设、开发者体验与技术内容创作专家" },
"Model QA Specialist": { "name": "模型 QA 专家", "description": "ML 审计、特征分析与可解释性专家" },
"ZK Steward": { "name": "知识卡片管理员", "description": "知识管理、Zettelkasten 与笔记系统专家" },
"MCP Builder": { "name": "MCP 构建专家", "description": "Model Context Protocol 服务器与 AI Agent 工具链专家" },
"Document Generator": { "name": "文档生成专家", "description": "PDF/PPTX/DOCX/XLSX 代码生成与专业文档创建专家" },
"Automation Governance Architect": { "name": "自动化治理架构师", "description": "自动化治理、n8n 与工作流审计专家" },
"Corporate Training Designer": { "name": "企业培训设计师", "description": "企业培训、课程开发与学习系统设计专家" },
"Government Digital Presales Consultant": { "name": "政务数字化售前顾问", "description": "ToG 项目售前与数字政府转型方案专家" },
"Healthcare Marketing Compliance": { "name": "医疗营销合规专家", "description": "中国医疗广告法规合规专家" },
"Recruitment Specialist": { "name": "招聘专家", "description": "人才获取、招聘运营与雇主品牌专家" },
"Study Abroad Advisor": { "name": "留学顾问", "description": "国际教育、申请规划与留学目的地专家(美/英/加/澳)" },
"Supply Chain Strategist": { "name": "供应链策略师", "description": "供应链管理、采购策略与优化专家" },
"Workflow Architect": { "name": "工作流架构师", "description": "工作流发现、流程映射与规格说明专家" },
"Salesforce Architect": { "name": "Salesforce 架构师", "description": "多云 Salesforce 设计、Governor Limits 与集成专家" },
"French Consulting Market Navigator": { "name": "法国咨询市场导航师", "description": "ESN/SI 生态与法国 IT 自由职业专家" },
"Korean Business Navigator": { "name": "韩国商务导航师", "description": "韩国商业文化、品议流程与人际关系机制专家" },
"Academic Anthropologist": { "name": "学术人类学家", "description": "文化研究、田野调查与人类学视角分析专家" },
"Anthropologist": { "name": "学术人类学家", "description": "文化研究、田野调查与人类学视角分析专家" },
"Academic Geographer": { "name": "学术地理学家", "description": "空间分析、地理信息与地缘研究专家" },
"Geographer": { "name": "学术地理学家", "description": "空间分析、地理信息与地缘研究专家" },
"Academic Historian": { "name": "学术历史学家", "description": "历史分析、史料解读与历史叙事专家" },
"Historian": { "name": "学术历史学家", "description": "历史分析、史料解读与历史叙事专家" },
"Academic Narratologist": { "name": "学术叙事学家", "description": "叙事结构、故事理论与文本分析专家" },
"Narratologist": { "name": "学术叙事学家", "description": "叙事结构、故事理论与文本分析专家" },
"Academic Psychologist": { "name": "学术心理学家", "description": "心理学研究、行为分析与认知科学专家" },
"Psychologist": { "name": "学术心理学家", "description": "心理学研究、行为分析与认知科学专家" },
"Healthcare Marketing Compliance Specialist": { "name": "医疗营销合规专家", "description": "中国医疗广告法规合规专家" },
"SRE (Site Reliability Engineer)": { "name": "站点可靠性工程师", "description": "SLO、错误预算、可观测性与混沌工程专家" },
"Game Designer": { "name": "游戏设计师", "description": "系统设计、GDD 写作、经济平衡与玩法循环专家" },
"Level Designer": { "name": "关卡设计师", "description": "布局理论、节奏、遭遇设计与环境叙事专家" },
"Technical Artist": { "name": "技术美术", "description": "Shader、VFX、LOD 管线与美术到引擎优化专家" },
"Game Audio Engineer": { "name": "游戏音频工程师", "description": "FMOD/Wwise、自适应音乐与空间音频专家" },
"Narrative Designer": { "name": "叙事设计师", "description": "故事系统、分支对话与世界观架构专家" },
"Unity Architect": { "name": "Unity 架构师", "description": "ScriptableObjects、数据驱动模块化与 DOTS/ECS 专家" },
"Unity Shader Graph Artist": { "name": "Unity Shader 艺术家", "description": "Shader Graph、HLSL、URP/HDRP 与渲染特性专家" },
"Unity Multiplayer Engineer": { "name": "Unity 多人网络工程师", "description": "Netcode for GameObjects、Unity Relay/Lobby 与服务器权威专家" },
"Unity Editor Tool Developer": { "name": "Unity 编辑器工具开发者", "description": "EditorWindow、AssetPostprocessor 与构建自动化专家" }
}
+38
View File
@@ -0,0 +1,38 @@
param(
[string[]]$TargetDirs = @(
"$env:USERPROFILE\.github\agents",
"$env:USERPROFILE\.copilot\agents"
)
)
$mapFile = Join-Path $PSScriptRoot "agent-names-zh.json"
$map = Get-Content $mapFile -Raw -Encoding UTF8 | ConvertFrom-Json
$totalUpdated = 0
foreach ($dir in $TargetDirs) {
if (-not (Test-Path $dir)) { Write-Warning "Skip (not found): $dir"; continue }
$files = Get-ChildItem "$dir\*.md" -ErrorAction SilentlyContinue
$updated = 0
foreach ($f in $files) {
$raw = [System.IO.File]::ReadAllText($f.FullName, [System.Text.Encoding]::UTF8)
if (-not $raw.StartsWith("---")) { continue }
$endIdx = $raw.IndexOf("---", 3)
if ($endIdx -lt 0) { continue }
$yaml = $raw.Substring(3, $endIdx - 3)
if (-not ($yaml -match "(?m)^name:\s*(.+)$")) { continue }
$currentName = $Matches[1].Trim()
$entry = $map.$currentName
if (-not $entry) { continue }
$newYaml = $yaml -replace "(?m)^name:\s*.+$", "name: $($entry.name)"
if ($newYaml -match "(?m)^description:") {
$newYaml = $newYaml -replace "(?m)^description:\s*.+$", "description: $($entry.description)"
}
$newContent = "---" + $newYaml + "---" + $raw.Substring($endIdx + 3)
[System.IO.File]::WriteAllText($f.FullName, $newContent, [System.Text.Encoding]::UTF8)
$updated++
}
Write-Host "OK: $updated agents localized -> $dir"
$totalUpdated += $updated
}
Write-Host "Total: $totalUpdated agent files updated."
Write-Host "Reload VS Code window (Ctrl+Shift+P -> Reload Window) to apply."
+1351
View File
File diff suppressed because it is too large Load Diff
Executable
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env bash
#
# lib.sh — shared pure-bash helpers for scripts/convert.sh and scripts/install.sh.
#
# No external dependencies. Bash 3.2+ compatible (macOS ships 3.2).
# Sourced, not executed. Groups:
# 1. Frontmatter / slug helpers (agent data model)
# 2. set -e-safe primitives
# 3. Terminal capability + ANSI (color, unicode, sizing)
# 4. TUI primitives (raw input, alt-screen, flicker-free draw)
#
# Everything here is namespaced loosely and guarded so it is safe to source
# from a script already running under `set -euo pipefail`.
# ---------------------------------------------------------------------------
# 1. Frontmatter / slug helpers
# ---------------------------------------------------------------------------
# get_field <field> <file> — value of a YAML frontmatter field (first match).
get_field() {
local field="$1" file="$2"
awk -v f="$field" '
/^---$/ { fm++; next }
fm == 1 && $0 ~ "^" f ": " { sub("^" f ": ", ""); print; exit }
' "$file"
}
# get_body <file> — file contents with the leading frontmatter block stripped.
get_body() {
awk 'BEGIN{fm=0} /^---$/{fm++; next} fm>=2{print}' "$1"
}
# slugify <string> — "Frontend Developer" -> "frontend-developer"
slugify() {
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' \
| sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//'
}
# agent_slug <file> — slug derived from the file's `name:` frontmatter.
# Single source of truth so convert + install always agree.
agent_slug() {
local name; name="$(get_field name "$1")"
[[ -n "$name" ]] && slugify "$name"
}
# is_agent_file <file> — true if the file starts with a YAML frontmatter fence.
is_agent_file() {
[[ -f "$1" ]] && [[ "$(head -1 "$1")" == "---" ]]
}
# ---------------------------------------------------------------------------
# 2. set -e-safe primitives (absorbs #505 — no more `(( x++ )) || true`)
# ---------------------------------------------------------------------------
# incr <varname> — increment a numeric variable in place, safely under set -e.
incr() { printf -v "$1" '%d' "$(( ${!1:-0} + 1 ))"; }
# ---------------------------------------------------------------------------
# 3. Terminal capability + ANSI
# ---------------------------------------------------------------------------
supports_color() { [[ -t 1 && -z "${NO_COLOR:-}" && "${TERM:-}" != "dumb" ]]; }
supports_unicode() { [[ "${LANG:-}${LC_ALL:-}${LC_CTYPE:-}" == *[Uu][Tt][Ff]* ]]; }
term_cols() { local c; c="$(tput cols 2>/dev/null)"; [[ "$c" =~ ^[0-9]+$ ]] && echo "$c" || echo 80; }
term_rows() { local r; r="$(tput lines 2>/dev/null)"; [[ "$r" =~ ^[0-9]+$ ]] && echo "$r" || echo 24; }
# init_ansi — populate C_* color vars + box-drawing chars (UTF-8 or ASCII).
init_ansi() {
if supports_color; then
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'; C_REV=$'\033[7m'
C_RED=$'\033[0;31m'; C_GREEN=$'\033[0;32m'; C_YELLOW=$'\033[1;33m'
C_BLUE=$'\033[0;34m'; C_CYAN=$'\033[0;36m'; C_MAGENTA=$'\033[0;35m'
else
C_RESET=''; C_BOLD=''; C_DIM=''; C_REV=''
C_RED=''; C_GREEN=''; C_YELLOW=''; C_BLUE=''; C_CYAN=''; C_MAGENTA=''
fi
if supports_unicode; then
BX_TL='╭'; BX_TR='╮'; BX_BL='╰'; BX_BR='╯'; BX_H='─'; BX_V='│'
GLYPH_ON='✓'; GLYPH_DET='●'; GLYPH_OFF='○'; GLYPH_CUR='▸'
else
BX_TL='+'; BX_TR='+'; BX_BL='+'; BX_BR='+'; BX_H='-'; BX_V='|'
GLYPH_ON='x'; GLYPH_DET='*'; GLYPH_OFF=' '; GLYPH_CUR='>'
fi
}
# repeat <char> <n> — print <char> n times.
repeat() { local i; for (( i=0; i<$2; i++ )); do printf '%s' "$1"; done; }
# strip_ansi <string> — remove ANSI escape sequences (for width math).
strip_ansi() { printf '%s' "$1" | sed $'s/\033\\[[0-9;]*m//g'; }
# vis_len <string> — visible length (ANSI-stripped). Note: assumes 1 col/char.
vis_len() { local s; s="$(strip_ansi "$1")"; printf '%s' "${#s}"; }
# ---------------------------------------------------------------------------
# 4. TUI primitives (used by install.sh's interactive wizard)
# ---------------------------------------------------------------------------
_TUI_ACTIVE=0
_TUI_STTY_SAVE=""
# tui_begin — enter alt screen, hide cursor, raw mode; install restore trap.
tui_begin() {
# Test hook: drive the wizard from piped keystrokes (skips the TTY gate and
# the alt-screen/stty takeover). Used by the install-script test harness.
[[ -n "${AGENCY_TUI_FORCE:-}" ]] && { _TUI_ACTIVE=1; return 0; }
[[ -t 0 && -t 1 ]] || return 1
_TUI_STTY_SAVE="$(stty -g 2>/dev/null)" || return 1
stty -echo -icanon time 0 min 1 2>/dev/null || return 1
printf '\033[?1049h\033[?25l' # alt screen + hide cursor
_TUI_ACTIVE=1
trap 'tui_end' EXIT INT TERM
}
# tui_end — restore terminal (idempotent; safe from trap).
tui_end() {
[[ "$_TUI_ACTIVE" == "1" ]] || return 0
printf '\033[?25h\033[?1049l' # show cursor + leave alt screen
[[ -n "$_TUI_STTY_SAVE" ]] && stty "$_TUI_STTY_SAVE" 2>/dev/null
_TUI_ACTIVE=0
trap - EXIT INT TERM
}
# read_key — read one keypress, echo a normalized token:
# UP DOWN LEFT RIGHT ENTER SPACE ESC BACKSPACE TAB or the literal character.
#
# Reads escape sequences byte-by-byte with INTEGER timeouts (bash 3.2 has no
# fractional -t). A real arrow sends ESC [ A (or ESC O A in application-cursor
# mode) as one buffered burst, so the follow-up reads return instantly; only a
# lone Esc waits out the 1s timeout. Handles both CSI ('[') and SS3 ('O').
read_key() {
local k k2 k3
IFS= read -rsn1 k 2>/dev/null || { printf 'EOF'; return; }
case "$k" in
$'\033')
if ! IFS= read -rsn1 -t 1 k2 2>/dev/null; then printf 'ESC'; return; fi
if [[ "$k2" == '[' || "$k2" == 'O' ]]; then
IFS= read -rsn1 -t 1 k3 2>/dev/null
case "$k3" in
A) printf 'UP' ;; B) printf 'DOWN' ;;
C) printf 'RIGHT' ;; D) printf 'LEFT' ;;
*) printf 'ESC' ;;
esac
else
printf 'ESC'
fi ;;
$'\n'|$'\r'|'') printf 'ENTER' ;; # Enter is CR in raw mode (sometimes empty)
' ') printf 'SPACE' ;;
$'\t') printf 'TAB' ;;
$'\177'|$'\010') printf 'BACKSPACE' ;;
*) printf '%s' "$k" ;;
esac
}
# draw_frame <buffer> — home cursor and paint a pre-composed frame.
# Flicker-free: erase-to-end-of-line (\033[K) on every line so a shorter new
# line never leaves the previous frame's tail behind, then erase-to-end-of-
# screen (\033[0J) to drop any leftover lines below the frame.
draw_frame() {
local buf="${1//$'\n'/$'\033[K'$'\n'}"
printf '\033[H%s\033[K\033[0J' "$buf"
}
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
#
# Validates agent markdown files:
# 1. YAML frontmatter must exist with name, description, color (ERROR)
# 2. Recommended sections checked but only warned (WARN)
# 3. File must have meaningful content
#
# Usage: ./scripts/lint-agents.sh [file ...]
# If no files given, scans all agent directories.
set -euo pipefail
# Keep in sync with AGENT_DIRS in scripts/convert.sh
AGENT_DIRS=(
academic
design
engineering
finance
game-development
gis
healthcare
marketing
paid-media
product
project-management
sales
security
spatial-computing
specialized
support
testing
)
REQUIRED_FRONTMATTER=("name" "description" "color")
RECOMMENDED_SECTIONS=("Identity" "Core Mission" "Critical Rules")
errors=0
warnings=0
classify_header_target() {
local header_lower="$1"
if [[ "$header_lower" =~ identity ]] ||
[[ "$header_lower" =~ learning.*memory ]] ||
[[ "$header_lower" =~ communication ]] ||
[[ "$header_lower" =~ style ]] ||
[[ "$header_lower" =~ critical.rule ]] ||
[[ "$header_lower" =~ rules.you.must.follow ]]; then
printf 'soul'
else
printf 'agents'
fi
}
lint_file() {
local file="$1"
if [[ ! -f "$file" ]]; then
echo "ERROR $file: not a file or does not exist"
errors=$((errors + 1))
return
fi
# 0. Reject CRLF line endings (repo standard is LF — see .gitattributes).
# A trailing \r otherwise makes the frontmatter check below fail with a
# confusing "missing frontmatter ---" even when the file clearly starts ---.
if LC_ALL=C grep -q $'\r' "$file"; then
echo "ERROR $file: CRLF line endings detected — convert to LF (e.g. 'perl -i -pe \"s/\\r\$//\" $file'); repo uses LF per .gitattributes"
errors=$((errors + 1))
return
fi
# 1. Check frontmatter delimiters
local first_line
first_line=$(head -1 "$file")
if [[ "$first_line" != "---" ]]; then
echo "ERROR $file: missing frontmatter opening ---"
errors=$((errors + 1))
return
fi
# Extract frontmatter (between first and second ---)
local frontmatter
frontmatter=$(awk 'NR==1{next} /^---$/{exit} {print}' "$file")
if [[ -z "$frontmatter" ]]; then
echo "ERROR $file: empty or malformed frontmatter"
errors=$((errors + 1))
return
fi
# 2. Check required frontmatter fields
for field in "${REQUIRED_FRONTMATTER[@]}"; do
if ! grep -qE -- "^${field}:" <<<"$frontmatter"; then
echo "ERROR $file: missing frontmatter field '${field}'"
errors=$((errors + 1))
fi
done
# 3. Check recommended sections (warn only)
local body
body=$(awk 'BEGIN{n=0} /^---$/{n++; next} n>=2{print}' "$file")
# Feed grep from a herestring, not a pipe: `grep -q` exits at the first match
# without draining its input, which kills a piping `echo` with SIGPIPE. Under
# `set -o pipefail` that 141 becomes the pipeline's status and is indistinguishable
# from "no match", so a large body raced its way to a spurious WARN.
for section in "${RECOMMENDED_SECTIONS[@]}"; do
if ! grep -qi -- "$section" <<<"$body"; then
echo "WARN $file: missing recommended section '${section}'"
warnings=$((warnings + 1))
fi
done
# 4. Check file has meaningful content (awk strips wc's leading whitespace on macOS/BSD)
local word_count
word_count=$(echo "$body" | wc -w | awk '{print $1}')
if [[ "${word_count:-0}" -lt 50 ]]; then
echo "WARN $file: body seems very short (< 50 words)"
warnings=$((warnings + 1))
fi
local soul_headers=0
local agents_headers=0
while IFS= read -r line; do
if [[ "$line" =~ ^##[[:space:]] ]]; then
local header_lower
header_lower=$(printf '%s' "$line" | tr '[:upper:]' '[:lower:]')
local target
target=$(classify_header_target "$header_lower")
if [[ "$target" == "soul" ]]; then
soul_headers=$((soul_headers + 1))
else
agents_headers=$((agents_headers + 1))
fi
fi
done <<< "$body"
if [[ $soul_headers -eq 0 ]]; then
echo "WARN $file: no section headers map to SOUL.md in convert.sh"
warnings=$((warnings + 1))
fi
if [[ $agents_headers -eq 0 ]]; then
echo "WARN $file: no section headers map to AGENTS.md in convert.sh"
warnings=$((warnings + 1))
fi
}
# Collect files to lint
files=()
if [[ $# -gt 0 ]]; then
files=("$@")
else
for dir in "${AGENT_DIRS[@]}"; do
if [[ -d "$dir" ]]; then
while IFS= read -r f; do
files+=("$f")
done < <(find "$dir" -name "*.md" -type f | sort)
fi
done
fi
if [[ ${#files[@]} -eq 0 ]]; then
echo "No agent files found."
exit 1
fi
echo "Linting ${#files[@]} agent files..."
echo ""
for file in "${files[@]}"; do
lint_file "$file"
done
echo ""
echo "Results: ${errors} error(s), ${warnings} warning(s) in ${#files[@]} files."
if [[ $errors -gt 0 ]]; then
echo "FAILED: fix the errors above before merging."
exit 1
else
echo "PASSED"
exit 0
fi