From fda3beeaba89c60aaaf75712b4dcbbc48600fa57 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 28 Sep 2026 00:32:16 -0400 Subject: [PATCH] fix(convert): refuse duplicate agent slugs before writing --- .github/workflows/check-tools.yml | 3 ++ scripts/convert.sh | 31 ++++++++++++++++++++ scripts/test-convert-slug-collisions.sh | 38 +++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 scripts/test-convert-slug-collisions.sh diff --git a/.github/workflows/check-tools.yml b/.github/workflows/check-tools.yml index ec13b3ec..dd89b0fc 100644 --- a/.github/workflows/check-tools.yml +++ b/.github/workflows/check-tools.yml @@ -25,6 +25,9 @@ jobs: - name: Validate converted YAML frontmatter run: bash scripts/test-convert-frontmatter.sh + - name: Reject colliding agent slugs before conversion + run: bash scripts/test-convert-slug-collisions.sh + - name: Validate converted outputs (round-trip, strict parse, counts, drift) # Drift is advisory on pull requests (agent PRs always move their own manifest line; # maintainers regenerate at landing) and enforced on pushes to main. diff --git a/scripts/convert.sh b/scripts/convert.sh index d810d4c8..a8f17990 100755 --- a/scripts/convert.sh +++ b/scripts/convert.sh @@ -695,6 +695,35 @@ clean_tool_output() { find "$dir" -mindepth 1 -maxdepth 1 ! -name 'README.md' -exec rm -rf {} + } +# Every per-agent integration writes to a path derived from the normalized +# name. Refuse collisions before cleaning any existing output: otherwise the +# later source file silently replaces the earlier agent in the generated tree. +check_agent_slug_collisions() { + local dir dirpath file slug relative i + local seen_slugs=() seen_files=() + local collisions=0 + for dir in "${AGENT_DIRS[@]}"; do + dirpath="$REPO_ROOT/$dir" + [[ -d "$dirpath" ]] || continue + while IFS= read -r -d '' file; do + is_agent_file "$file" || continue + slug="$(agent_slug "$file")" + [[ -n "$slug" ]] || continue + relative="${file#"$REPO_ROOT"/}" + for i in "${!seen_slugs[@]}"; do + if [[ "${seen_slugs[i]}" == "$slug" ]]; then + error "duplicate agent slug '$slug': ${seen_files[i]} and $relative" + collisions=$((collisions + 1)) + break + fi + done + seen_slugs+=("$slug") + seen_files+=("$relative") + done < <(find "$dirpath" -name "*.md" -type f -print0) + done + (( collisions == 0 )) +} + run_conversions() { local tool="$1" local count=0 @@ -772,6 +801,8 @@ main() { exit 1 fi + check_agent_slug_collisions || exit 1 + header "The Agency -- Converting agents to tool-specific formats" echo " Repo: $REPO_ROOT" echo " Output: $OUT_DIR" diff --git a/scripts/test-convert-slug-collisions.sh b/scripts/test-convert-slug-collisions.sh new file mode 100644 index 00000000..9e6dd041 --- /dev/null +++ b/scripts/test-convert-slug-collisions.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Duplicate normalized names must not silently overwrite one agent's output. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +scratch="$(mktemp -d)" +trap 'rm -rf "$scratch"' EXIT + +mkdir -p "$scratch/repo/scripts" "$scratch/repo/engineering" "$scratch/output/gemini-cli" +cp "$SCRIPT_DIR/convert.sh" "$SCRIPT_DIR/lib.sh" "$scratch/repo/scripts/" + +cat > "$scratch/repo/engineering/first.md" <<'EOF' +--- +name: Dev Ops +description: First agent +color: blue +--- +# First agent +EOF +cat > "$scratch/repo/engineering/second.md" <<'EOF' +--- +name: Dev/Ops +description: Second agent +color: red +--- +# Second agent +EOF + +printf 'keep existing output\n' > "$scratch/output/gemini-cli/sentinel" +if bash "$scratch/repo/scripts/convert.sh" --tool gemini-cli --out "$scratch/output" > "$scratch/log" 2>&1; then + echo "FAIL: converter accepted two agents with the same output slug" + exit 1 +fi +grep -q "duplicate agent slug 'dev-ops'" "$scratch/log" +grep -q 'engineering/first.md' "$scratch/log" +grep -q 'engineering/second.md' "$scratch/log" +[[ "$(cat "$scratch/output/gemini-cli/sentinel")" == 'keep existing output' ]] +echo "PASS: duplicate slug refused before existing output changed"