Files
agency-agents/scripts/convert.sh
T
Hotragn Pettugani 1f92396556 fix(convert): two color names fall through to grey, and nothing checked (#872)
* fix(convert): two color names fall through to grey, and nothing checked

resolve_opencode_color() maps a name it does not recognise to #6B7280 and
says nothing about it. Four agents ask for a name it has never known:

  engineering/engineering-minimal-change-engineer.md   slate
  specialized/operations-manager.md                    slate
  gis/gis-technical-consultant.md                      navy
  specialized/chief-financial-officer.md               navy

All four render grey in the OpenCode integration, which reads as a
deliberate grey rather than a miss — and some agents do choose grey, so
there was nothing to notice.

`slate` and `navy` are now in the map. Values follow the CSS named color
where one exists (navy -> #000080, matching how teal already works) and
Tailwind's 500 shade otherwise (slate -> #64748B, matching gray).

Adding two names does not stop the next one, so two checks now cover it:

  - lint-agents.sh rejects a color that is neither #RRGGBB nor a name the
    converter knows, and prints the names it knows. The list is read out
    of resolve_opencode_color() rather than copied, so it cannot drift
    from the map that does the work. This runs on changed files in agent
    PRs, which is where a new color arrives.

  - test-convert-outputs.sh fails when an opencode output is #6B7280 and
    the source did not ask for grey. Grey stays a legitimate choice; the
    check is "grey only when the source said so".

CONTRIBUTING now says which color values actually work, since the template
just said `colorname or "#hexcode"`.

Verified: all 279 agents pass the new lint rule, and no opencode output
falls through to grey by accident.

* ci(lint): run the whole roster when the linter itself changes

The lint job scopes itself to the agent files a PR touched, which is right
for an agent PR and wrong for a rule change. A new rule lands without ever
having run against the other 278 agents — it either breaks a division
nobody edited or quietly does nothing, and either way CI is silent.

The colour check in the previous commit is the case in point: it changes
scripts/, so the lint workflow's path filter did not even fire, and the
rule would have merged without CI looking at a single agent.

Adds a lint-all job that runs the full-roster lint when scripts/
lint-agents.sh, convert.sh, or lib.sh is part of the diff, and says why it
is skipping when they are not. Those three paths are in the workflow's
trigger list now so the job can fire at all.
2026-09-20 18:35:13 -05:00

823 lines
25 KiB
Bash
Executable File

#!/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 roster index 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
research 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;
'
}
# Quote a single-line value for a YAML frontmatter scalar. Single-quoted YAML
# strings keep colons, hashes, backslashes, and Unicode literal, while doubling
# an apostrophe is the only escaping rule required here.
yaml_quote() {
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/''/g")"
}
# --- 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: $(yaml_quote "$slug")
description: $(yaml_quote "$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: $(yaml_quote "$slug")
description: $(yaml_quote "$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: $(yaml_quote "$slug")
description: $(yaml_quote "$description")
---
${body}
HEREDOC
}
# Map known color names and normalize to OpenCode-safe #RRGGBB values.
# An unknown name lands on grey, which looks like a choice rather than a miss,
# so scripts/lint-agents.sh reads this list and rejects a color that is not in
# it. Values follow the CSS named color where one exists (teal, navy) and
# Tailwind's 500 shade otherwise (gray, slate).
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" ;;
slate) mapped="#64748B" ;;
navy) mapped="#000080" ;;
*) 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: $(yaml_quote "$name")
description: $(yaml_quote "$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: $(yaml_quote "$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 fence_marker is set, ## lines are code content, not section
# boundaries (issue #849). See lib.sh fence_open_p / fence_closes_p.
local fence_marker="" fence_len=0 fence_indent=0
while IFS= read -r line; do
if [[ -n "$fence_marker" ]]; then
current_section+="$line"$'\n'
if fence_closes_p "$line" "$fence_marker" "$fence_len" "$fence_indent"; then
fence_marker=""
fence_len=0
fence_indent=0
fi
continue
fi
if fence_open_p "$line"; then
fence_marker="${BASH_REMATCH[2]:0:1}"
fence_len=${#BASH_REMATCH[2]}
fence_indent=${#BASH_REMATCH[1]}
current_section+="$line"$'\n'
continue
fi
# 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: $(yaml_quote "$slug")
description: $(yaml_quote "$description")
tools: $(yaml_quote "$tools")
---
${body}
HEREDOC
else
cat > "$outfile" <<HEREDOC
---
name: $(yaml_quote "$slug")
description: $(yaml_quote "$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: $(yaml_quote "$slug")
description: $(yaml_quote "$description")
tools: $(yaml_quote "$tools")
---
${body}
HEREDOC
else
cat > "$outfile" <<HEREDOC
---
name: $(yaml_quote "$slug")
description: $(yaml_quote "$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
#
# The roster of specialized AI agents from The Agency
# (https://github.com/msitarzewski/agency-agents): each one's name, what it is
# for, and where its full instructions live.
#
# Aider keeps a conventions file in context for the whole session, so this is an
# index and not the agents themselves. Inlining every body would make this file
# about 3.8 million characters, which no model will take.
#
# To use an agent:
# 1. Name it in your prompt — "Use the Frontend Developer agent to review
# this component." The description below is usually enough for that.
# 2. For its full instructions, pull the agent file into the session:
# /read-only /path/to/agency-agents/engineering/engineering-frontend-developer.md
#
# Paths below are relative to an agency-agents checkout.
#
# 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 source division
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
source="${file#"$REPO_ROOT"/}"
division="${source%%/*}"
# One index entry per agent, not the agent. A conventions file is read into
# every request; the bodies together are 3.8 million characters and this
# index is about 90,000.
cat >> "$AIDER_TMP" <<HEREDOC
---
## ${name}
> ${description}
Division: ${division}
Full instructions: ${source}
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() {
# Defensive: tool names are plain slugs; refuse anything else so a future
# caller can never steer this rm -rf outside $OUT_DIR via "../" or "/".
[[ "$1" =~ ^[a-z0-9-]+$ ]] || { echo "ERROR: clean_tool_output: refusing non-slug tool name '$1'" >&2; return 1; }
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 kimi 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=$(( ${#parallel_tools[@]} + 1 ))
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 "$@"