From 33f68d61b7d059742577f99d6880e58407e8cdc0 Mon Sep 17 00:00:00 2001 From: hotragn Date: Wed, 30 Sep 2026 04:51:02 -0400 Subject: [PATCH] fix(install): accept the file-stem ids runbooks.json uses for --agent strategy/runbooks.json names agents by file stem, the corpus id the desktop app maps to catalog entries ("engineering-frontend-developer"). The installer only accepted the slug built from `name:` ("frontend-developer"), and for 206 of the 279 agents those differ. Of the 36 agents the four runbooks list, 35 could not be selected by the ids the runbooks give them: $ install.sh --tool claude-code --agent engineering-frontend-developer --dry-run [ERR] Unknown agent 'engineering-frontend-developer'. Use --list agents ... So there was no way to install a runbook's team from the CLI without translating every id by hand. resolve_agent now takes a slug, a display name (as before, via slugify), or a file stem, and returns the install slug. Slugs are tried first. No stem equals a different agent's slug today, and stems are unique, so nothing that resolved before resolves differently now. Resolution also stops rescanning the roster per request. agent_slug_exists ran get_field over all 279 files for each requested agent; roster_index builds the slug/stem table once in the parent shell. A 36-line agents file went from ~10,000 get_field calls to 279. test-agent-selection.sh gains a stem case and feeds each runbook's roster to --agents-file, checking that it resolves to exactly its own agent count. It fails on main at the first runbook. The usage text and the README selection section say which ids are accepted, with an example of installing a runbook's team. --- README.md | 9 ++++++ scripts/install.sh | 56 +++++++++++++++++++++++++-------- scripts/test-agent-selection.sh | 31 +++++++++++++++++- 3 files changed, 82 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a6eb377f..41d13ce1 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,15 @@ Browse the agents below and copy/adapt the ones you need! ./scripts/install.sh --tool opencode --division engineering --dry-run ``` +`--agent` and `--agents-file` take an agent's slug (as `--list agents` prints it), its display name, or its file name without `.md` — the id the [runbook rosters](strategy/runbooks.json) use — so a runbook's team installs as listed: + +```bash +python3 -c 'import json, sys +for r in json.load(open("strategy/runbooks.json"))["runbooks"]: + if r["slug"] == sys.argv[1]: [print(a) for g in r["roster"] for a in g["agents"]]' startup-mvp > team.txt +./scripts/install.sh --tool claude-code --agents-file team.txt +``` + > **OpenCode note:** OpenCode's runtime currently registers only ~119 agents and silently drops the rest ([upstream bug](https://github.com/anomalyco/opencode/issues/27988)). Installing a subset with `--division` keeps you under that limit. The installer warns you when a selection would exceed it. See the [Multi-Tool Integrations](#-multi-tool-integrations) section below for full details. diff --git a/scripts/install.sh b/scripts/install.sh index f8ccb7fa..5e65ca4c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -33,8 +33,9 @@ # Selection (compose freely; empty = everything): # --tool Only these tools # --division Only these teams/divisions (comma-separated) -# --agent Only these specific agents -# --agents-file Agents listed in a file (one slug/name per line, # comments ok) +# --agent Only these specific agents (install slug, display name, +# or file stem such as engineering-frontend-developer) +# --agents-file Agents listed in a file (one id per line, # comments ok) # # Mode: # --link Symlink instead of copy (updates propagate) @@ -165,6 +166,7 @@ AGENTS_FILE="" # --agents-file DRY_RUN=false # --dry-run SELECTION_ACTIVE=false # true once any agent-level filter is applied _ALLOWED_SLUGS="" # newline-delimited cache of allowed slugs +_ROSTER_INDEX="" # "\t" per agent; see roster_index # division_files — agent file paths (frontmatter only) in a division. division_files() { @@ -178,16 +180,43 @@ division_files() { # division_count — number of agents in a division. division_count() { division_files "$1" | grep -c . ; } -# agent_slug_exists — verify a requested agent against the source roster. -# Selection filters should fail before installation when they name nothing that -# can be installed; otherwise dry-run counts and completion messages lie. -agent_slug_exists() { - local target="$1" div f +# roster_index — fill _ROSTER_INDEX with one "\t" line +# per agent, once. Call it in the parent shell before resolve_agent: a $(...) +# caller would build its own copy and throw it away. +# +# Resolving each requested agent used to rescan the roster, running get_field +# on all 279 files per request, so a 36-agent runbook roster cost ~10,000 +# get_field calls before anything installed. +roster_index() { + [[ -n "$_ROSTER_INDEX" ]] && return 0 + local div f for div in "${ALL_DIVISIONS[@]}"; do while IFS= read -r f; do - [[ "$(agent_slug "$f")" == "$target" ]] && return 0 + _ROSTER_INDEX+="$(agent_slug "$f")"$'\t'"$(basename "$f" .md)"$'\n' done < <(division_files "$div") done +} + +# resolve_agent — print the install slug for a requested agent, +# 1 if nothing matches. Selection filters should fail before installation when +# they name nothing that can be installed; otherwise dry-run counts and +# completion messages lie. +# +# Two spellings name an agent. The install slug comes from `name:` and is what +# --list agents prints. The file stem is the corpus id strategy/runbooks.json +# uses ("engineering-frontend-developer"), and for 206 of 279 agents it is not +# the slug, so 35 of the 36 agents the runbooks list could not be selected by +# the ids the runbooks give. Slugs are tried first; no stem equals another +# agent's slug today, and slug-first keeps it unambiguous if one ever does. +resolve_agent() { + local target="$1" slug stem + [[ -n "$target" ]] || return 1 + while IFS=$'\t' read -r slug stem; do + [[ -n "$slug" && "$slug" == "$target" ]] && { printf '%s' "$slug"; return 0; } + done <<< "$_ROSTER_INDEX" + while IFS=$'\t' read -r slug stem; do + [[ -n "$slug" && "$stem" == "$target" ]] && { printf '%s' "$slug"; return 0; } + done <<< "$_ROSTER_INDEX" return 1 } @@ -199,7 +228,8 @@ build_selection() { return fi SELECTION_ACTIVE=true - local slugs="" div f s line requested + local slugs="" div f s line requested resolved + roster_index for div in ${FILTER_DIVISIONS[@]+"${FILTER_DIVISIONS[@]}"}; do while IFS= read -r f; do s="$(agent_slug "$f")"; [[ -n "$s" ]] && slugs+="$s"$'\n' @@ -207,11 +237,11 @@ build_selection() { done for s in ${FILTER_AGENTS[@]+"${FILTER_AGENTS[@]}"}; do requested="$(slugify "$s")" - if ! agent_slug_exists "$requested"; then + if ! resolved="$(resolve_agent "$requested")"; then err "Unknown agent '$s'. Use --list agents to see the available roster." exit 1 fi - slugs+="$requested"$'\n' + slugs+="$resolved"$'\n' done if [[ -n "$AGENTS_FILE" ]]; then [[ -f "$AGENTS_FILE" ]] || { err "agents-file not found: $AGENTS_FILE"; exit 1; } @@ -220,11 +250,11 @@ build_selection() { line="$(printf '%s' "$line" | xargs 2>/dev/null)" # trim [[ -z "$line" ]] && continue requested="$(slugify "$line")" - if ! agent_slug_exists "$requested"; then + if ! resolved="$(resolve_agent "$requested")"; then err "Unknown agent '$line' in agents-file '$AGENTS_FILE'." exit 1 fi - slugs+="$requested"$'\n' + slugs+="$resolved"$'\n' done < "$AGENTS_FILE" fi _ALLOWED_SLUGS="$(printf '%s' "$slugs" | sort -u | sed '/^$/d')" diff --git a/scripts/test-agent-selection.sh b/scripts/test-agent-selection.sh index 5ce0561c..d7166c1e 100755 --- a/scripts/test-agent-selection.sh +++ b/scripts/test-agent-selection.sh @@ -41,4 +41,33 @@ output="$($INSTALLER --tool claude-code --agent 'Developer Tooling Engineer' --d exit 1 } -echo "PASS: install.sh rejects unknown agent selections and accepts display names" +# The file stem is the id strategy/runbooks.json uses, and for most agents it is +# not the install slug (engineering-frontend-developer vs frontend-developer). +output="$("$INSTALLER" --tool claude-code --agent engineering-frontend-developer --dry-run 2>&1)" +[[ "$output" == *"Agents: 1"* ]] || { + printf 'File-stem selection did not resolve to one agent:\n%s\n' "$output" >&2 + exit 1 +} + +# Every runbook roster, fed to --agents-file as the runbooks list it, resolves +# to exactly its own agents. On main 35 of the 36 ids were "Unknown agent". +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +while IFS=$'\t' read -r runbook count ids; do + printf '%s\n' $ids > "$AGENTS_FILE" + set +e + output="$("$INSTALLER" --tool claude-code --agents-file "$AGENTS_FILE" --dry-run 2>&1)" + status=$? + set -e + [[ "$status" -eq 0 && "$output" == *"Agents: $count"* ]] || { + printf 'Runbook %s roster (%s agents) did not resolve:\n%s\n' "$runbook" "$count" "$output" >&2 + exit 1 + } +done < <(python3 - "$REPO_ROOT/strategy/runbooks.json" <<'PY' +import json, sys +for rb in json.load(open(sys.argv[1], encoding="utf-8"))["runbooks"]: + ids = sorted({a for group in rb["roster"] for a in group["agents"]}) + print(f'{rb["slug"]}\t{len(ids)}\t{" ".join(ids)}') +PY +) + +echo "PASS: install.sh rejects unknown agent selections and accepts display names, file stems, and runbook rosters"