chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
@@ -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())
|
||||
Executable
+179
@@ -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
|
||||
Executable
+139
@@ -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."
|
||||
Executable
+83
@@ -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
|
||||
Executable
+88
@@ -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."
|
||||
Executable
+770
@@ -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 "$@"
|
||||
@@ -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
|
||||
@@ -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 与构建自动化专家" }
|
||||
}
|
||||
@@ -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."
|
||||
Executable
+1351
File diff suppressed because it is too large
Load Diff
Executable
+163
@@ -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"
|
||||
}
|
||||
Executable
+185
@@ -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
|
||||
Reference in New Issue
Block a user