Add tools.json canonical registry + check-tools.sh guard (#606)

Mirrors the divisions.json / check-divisions.sh pattern for the supported tool
set. tools.json (repo root) is the single source of truth for all 13 tools,
consumed by the Agency Agents app and by scripts/convert.sh + scripts/install.sh.
scripts/check-tools.sh (no-jq, bash 3.2) fails the build if tools.json disagrees
with ALL_TOOLS in install.sh or the converter set in convert.sh, or if any entry
is missing id/label/kebab/format/dest.

Every tool carries its real install contract (format, dest, scope, detect,
version) — verified against actual convert.sh/install.sh behavior via a
sandboxed install pass (all dest templates resolve to the real on-disk layout).

`format` is the renderer contract: same name => byte-identical output. The five
formerly-undescribed tools get distinct names — aider-conventions, antigravity-skill
(its non-deterministic date_added means it can't share osaurus's skill-md),
kimi-agent, openclaw-workspace, windsurf-rules — none colliding with the app's
implemented renderers. Removed the `wired` field: it encoded app renderer state
(not catalog truth); consumers derive installability from `format` against their
own implemented-format set. check-tools.sh requires format+dest for every tool,
not just some. Also fixes antigravity detect (.gemini/antigravity-cli ->
.gemini/antigravity/skills, matching the actual code).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Sitarzewski
2026-06-22 01:38:47 -05:00
committed by GitHub
parent 55beae93a7
commit 9262649a48
2 changed files with 99 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
#!/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, and dest
#
# 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 dest; do
echo "$line" | grep -qE "\"$field\":" || fail "tool '$t' in $JSON is missing \"$field\""
done
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."
+18
View File
@@ -0,0 +1,18 @@
{
"_note": "Source of truth for the supported tool set. Keyed by the CLI tool name (kebab). Each entry carries the install contract (id, detect dirs, dest templates, render `format`, scope, version cmd) plus app presentation (label, short, accent, icon, order). aa converts + installs ALL listed tools. `format` is the renderer contract: the same `format` name guarantees byte-identical output, so two tools may share a format only if their rendered files are identical. Consumers (the Agency Agents app, scripts/convert.sh, scripts/install.sh) derive any per-format capability — e.g. whether the app ships a native renderer — from `format` against their own set; the catalog carries NO app-release state. scripts/check-tools.sh (CI) fails the build if this disagrees with ALL_TOOLS in install.sh or the converter set in convert.sh, or if any entry is missing id/label/kebab/format/dest. Add a tool: add an entry here, a convert_<tool> (or reuse a `format`) in convert.sh, and an install_<tool> in install.sh, then run scripts/check-tools.sh.",
"tools": {
"claude-code": {"id":"claudeCode","label":"Claude Code","short":"Claude","kebab":"claude-code","accent":"#D97757","icon":"claudecode","order":1,"scope":{"user":true,"project":true},"detect":{"dirs":[".claude"],"agentsDir":".claude/agents"},"version":{"bin":"claude","args":["--version"]},"format":"identity","slugFrom":"source","dest":{"user":[".claude/agents/{slug}.md"],"project":[".claude/agents/{slug}.md"]}},
"codex": {"id":"codex","label":"Codex","short":"Codex","kebab":"codex","accent":"#10A37F","icon":"codex","order":2,"scope":{"user":true,"project":true},"detect":{"dirs":[".codex"],"agentsDir":".codex/agents"},"version":{"bin":"codex","args":["--version"]},"format":"codex-toml","slugFrom":"name","dest":{"user":[".codex/agents/{slug}.toml"],"project":[".codex/agents/{slug}.toml"]}},
"gemini-cli": {"id":"geminiCli","label":"Gemini CLI","short":"Gemini","kebab":"gemini-cli","accent":"#4285F4","icon":"geminicli","order":3,"scope":{"user":true,"project":true},"detect":{"dirs":[".gemini/agents"],"agentsDir":".gemini/agents"},"version":{"bin":"gemini","args":["--version"]},"format":"gemini-md","slugFrom":"name","dest":{"user":[".gemini/agents/{slug}.md"],"project":[".gemini/agents/{slug}.md"]}},
"copilot": {"id":"copilot","label":"GitHub Copilot","short":"Copilot","kebab":"copilot","accent":"#6E40C9","icon":"githubcopilot","order":4,"scope":{"user":true,"project":true},"detect":{"dirs":[".github",".copilot"],"agentsDir":".github/agents"},"version":{"bin":"gh","args":["copilot","--version"]},"format":"identity","slugFrom":"source","dest":{"user":[".copilot/agents/{slug}.md",".github/agents/{slug}.md"],"project":[".github/agents/{slug}.md"]}},
"qwen": {"id":"qwen","label":"Qwen Code","short":"Qwen","kebab":"qwen","accent":"#615CED","icon":"qwen","order":5,"scope":{"user":true,"project":true},"detect":{"dirs":[".qwen"],"agentsDir":".qwen/agents"},"version":{"bin":"qwen","args":["--version"]},"format":"qwen-md","slugFrom":"name","dest":{"user":[".qwen/agents/{slug}.md"],"project":[".qwen/agents/{slug}.md"]}},
"cursor": {"id":"cursor","label":"Cursor","short":"Cursor","kebab":"cursor","accent":"#1F2430","icon":"cursor","order":6,"scope":{"user":false,"project":true},"detect":{"dirs":[".cursor"],"agentsDir":null},"version":{"bin":"cursor","args":["--version"]},"format":"cursor-mdc","slugFrom":"name","dest":{"user":[],"project":[".cursor/rules/{slug}.mdc"]}},
"opencode": {"id":"opencode","label":"opencode","short":"opencode","kebab":"opencode","accent":"#FF6B35","icon":"opencode","order":7,"scope":{"user":true,"project":true},"detect":{"dirs":[".config/opencode"],"agentsDir":null},"version":{"bin":"opencode","args":["--version"]},"format":"opencode-md","slugFrom":"name","dest":{"user":[".config/opencode/agents/{slug}.md"],"project":[".opencode/agents/{slug}.md"]}},
"osaurus": {"id":"osaurus","label":"Osaurus","short":"Osaurus","kebab":"osaurus","accent":"#10B981","icon":null,"order":8,"scope":{"user":true,"project":false},"detect":{"dirs":[".osaurus"],"agentsDir":".osaurus/skills"},"version":{"bin":"osaurus","args":["--version"]},"format":"skill-md","slugFrom":"name","slugPrefix":"agency-","dest":{"user":[".osaurus/skills/{slug}/SKILL.md"],"project":[]}},
"aider": {"id":"aider","label":"Aider","short":"Aider","kebab":"aider","accent":"#8B5CF6","icon":null,"order":9,"scope":{"user":false,"project":true},"detect":{"dirs":[],"agentsDir":null},"version":{"bin":"aider","args":["--version"]},"format":"aider-conventions","slugFrom":null,"dest":{"user":[],"project":["CONVENTIONS.md"]}},
"antigravity": {"id":"antigravity","label":"Antigravity","short":"antigravity","kebab":"antigravity","accent":"#0EA5E9","icon":"antigravity","order":10,"scope":{"user":true,"project":false},"detect":{"dirs":[".gemini/antigravity/skills"],"agentsDir":".gemini/antigravity/skills"},"version":{"bin":"agy","args":["--version"]},"format":"antigravity-skill","slugFrom":"name","slugPrefix":"agency-","dest":{"user":[".gemini/antigravity/skills/{slug}/SKILL.md"],"project":[]}},
"kimi": {"id":"kimi","label":"Kimi","short":"Kimi","kebab":"kimi","accent":"#0F0F12","icon":"kimi","order":11,"scope":{"user":true,"project":false},"detect":{"dirs":[],"agentsDir":".config/kimi/agents"},"version":{"bin":"kimi","args":["--version"]},"format":"kimi-agent","slugFrom":"name","dest":{"user":[".config/kimi/agents/{slug}/agent.yaml",".config/kimi/agents/{slug}/system.md"],"project":[]}},
"openclaw": {"id":"openclaw","label":"OpenClaw","short":"openclaw","kebab":"openclaw","accent":"#E11D48","icon":null,"order":12,"scope":{"user":true,"project":false},"detect":{"dirs":[".openclaw"],"agentsDir":".openclaw/agency-agents"},"version":{"bin":"openclaw","args":["--version"]},"format":"openclaw-workspace","slugFrom":"name","dest":{"user":[".openclaw/agency-agents/{slug}/SOUL.md",".openclaw/agency-agents/{slug}/AGENTS.md",".openclaw/agency-agents/{slug}/IDENTITY.md"],"project":[]}},
"windsurf": {"id":"windsurf","label":"Windsurf","short":"Windsurf","kebab":"windsurf","accent":"#09B6A2","icon":"windsurf","order":13,"scope":{"user":false,"project":true},"detect":{"dirs":[".codeium"],"agentsDir":null},"version":{"bin":"windsurf","args":["--version"]},"format":"windsurf-rules","slugFrom":null,"dest":{"user":[],"project":[".windsurfrules"]}}
}
}