From 0086377d67be137b6af70a60e88d40d6053cb82d Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Sat, 27 Jun 2026 08:13:22 +0200 Subject: [PATCH 1/2] fix(agent-rules): accept both scope-index headings in validation generate-agents.sh emits the root scope index under the heading "## Scoped AGENTS.md (MUST read when working in these directories)" (strengthened from the legacy "## Index of scoped AGENTS.md" in commit bbb6899), but validate-structure.sh still grepped for the legacy literal. A freshly generated root longer than 50 lines therefore failed the skill's own structure validation: ERROR: Root is bloated: N lines and no scope index Match both headings via a shared ERE so validation stays in sync with the generator and remains backward-compatible with files produced by older versions and the reference examples (which use the legacy heading). Add a regression test that drives generate-agents.sh -> validate-structure.sh end to end and a workflow to run it, so the two cannot drift apart again. Fixes #55 Signed-off-by: Sebastian Mendel --- .github/workflows/test-scripts.yml | 31 +++++++ .../scripts/tests/test-scope-index-heading.sh | 85 +++++++++++++++++++ .../agent-rules/scripts/validate-structure.sh | 12 ++- 3 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/test-scripts.yml create mode 100755 skills/agent-rules/scripts/tests/test-scope-index-heading.sh diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml new file mode 100644 index 0000000..9ab4852 --- /dev/null +++ b/.github/workflows/test-scripts.yml @@ -0,0 +1,31 @@ +name: Test Scripts + +# Runs the agent-rules script regression tests (e.g. the scope-index heading +# contract between generate-agents.sh and validate-structure.sh, issue #55). +# Local job — no reusable workflow needed; the tests only require git, jq and +# bash, all preinstalled on the runner. + +on: + push: + branches: [main] + paths: + - 'skills/agent-rules/scripts/**' + - 'skills/agent-rules/assets/**' + pull_request: + paths: + - 'skills/agent-rules/scripts/**' + - 'skills/agent-rules/assets/**' + workflow_dispatch: + +permissions: + contents: read + +jobs: + script-tests: + name: Script regression tests + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Run scope-index heading regression test + run: bash skills/agent-rules/scripts/tests/test-scope-index-heading.sh diff --git a/skills/agent-rules/scripts/tests/test-scope-index-heading.sh b/skills/agent-rules/scripts/tests/test-scope-index-heading.sh new file mode 100755 index 0000000..41e8b96 --- /dev/null +++ b/skills/agent-rules/scripts/tests/test-scope-index-heading.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Regression test for the scope-index heading contract between +# generate-agents.sh and validate-structure.sh (issue #55). +# +# generate-agents.sh emits the root scope index under the heading +# "## Scoped AGENTS.md (MUST read when working in these directories)" +# while older output used the legacy heading +# "## Index of scoped AGENTS.md". +# validate-structure.sh must accept BOTH so a freshly generated root does not +# fail the skill's own structure validation, while a bloated root without any +# scope index still fails. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPTS_DIR="$(dirname "$SCRIPT_DIR")" +GENERATE="$SCRIPTS_DIR/generate-agents.sh" +VALIDATE="$SCRIPTS_DIR/validate-structure.sh" + +NEW_HEADING='## Scoped AGENTS.md (MUST read when working in these directories)' +LEGACY_HEADING='## Index of scoped AGENTS.md' + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +fail() { echo "❌ FAIL: $1"; exit 1; } +pass() { echo "✅ PASS: $1"; } + +# Build a minimal repo with a scoped directory so the generated root exceeds +# 50 lines and includes a populated scope index. +build_fixture() { + local dir="$1" + rm -rf "$dir" + mkdir -p "$dir/src" "$dir/.github/workflows" + cat > "$dir/package.json" <<'JSON' +{ "name": "fixture", "version": "1.0.0", "scripts": { "test": "vitest", "build": "tsc" } } +JSON + echo "console.log('hi')" > "$dir/src/index.ts" + echo "name: ci" > "$dir/.github/workflows/ci.yml" + git -C "$dir" init -q + git -C "$dir" -c user.email=t@t.t -c user.name=t add -A + git -C "$dir" -c user.email=t@t.t -c user.name=t commit -qm init +} + +# --- Test 1: generated root (new heading) passes validation ----------------- +FX1="$WORK/new-heading" +build_fixture "$FX1" +bash "$GENERATE" "$FX1" --style=thin >/dev/null 2>&1 || fail "generate-agents.sh errored" + +grep -qF "$NEW_HEADING" "$FX1/AGENTS.md" \ + || fail "generator no longer emits the expected heading: $NEW_HEADING" +lines=$(wc -l < "$FX1/AGENTS.md") +[ "$lines" -gt 50 ] || fail "generated root is only $lines lines; expected >50 to exercise the scope-index path" + +if bash "$VALIDATE" "$FX1" >/dev/null 2>&1; then + pass "generated root ($lines lines, new heading) validates" +else + fail "validate-structure.sh rejected a freshly generated root (#55 regression)" +fi + +# --- Test 2: legacy heading still accepted (backward compatibility) ---------- +FX2="$WORK/legacy-heading" +cp -r "$FX1" "$FX2" +# Rewrite only the heading line, keeping the rest of the generated root intact. +sed -i "s|^${NEW_HEADING}\$|${LEGACY_HEADING}|" "$FX2/AGENTS.md" +grep -qF "$LEGACY_HEADING" "$FX2/AGENTS.md" || fail "could not rewrite heading to legacy form" + +if bash "$VALIDATE" "$FX2" >/dev/null 2>&1; then + pass "root with legacy heading still validates (backward compatible)" +else + fail "validate-structure.sh rejected the legacy scope-index heading" +fi + +# --- Test 3: bloated root without any scope index still fails ---------------- +FX3="$WORK/no-heading" +cp -r "$FX1" "$FX3" +# Drop the scope-index heading so the >50-line root has no index at all. +sed -i "/^${NEW_HEADING}\$/d" "$FX3/AGENTS.md" + +if bash "$VALIDATE" "$FX3" >/dev/null 2>&1; then + fail "validate-structure.sh accepted a bloated root with no scope index" +else + pass "bloated root without a scope index is still rejected" +fi + +echo "All scope-index heading regression tests passed." diff --git a/skills/agent-rules/scripts/validate-structure.sh b/skills/agent-rules/scripts/validate-structure.sh index 6794bdb..31aaf52 100755 --- a/skills/agent-rules/scripts/validate-structure.sh +++ b/skills/agent-rules/scripts/validate-structure.sh @@ -5,6 +5,12 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Scope-index heading regex (ERE). Matches both the current template heading +# "## Scoped AGENTS.md (MUST read ...)" and the legacy "## Index of scoped +# AGENTS.md" so validation stays in sync with generate-agents.sh output and +# remains backward-compatible with files produced by older versions. +SCOPE_INDEX_HEADING_RE='^## (Index of scoped|Scoped) AGENTS\.md' + # Default options PROJECT_DIR="" CHECK_FRESHNESS=false @@ -119,7 +125,7 @@ check_root_is_thin() { if [ "$line_count" -le 50 ]; then success "Root is thin: $line_count lines" return 0 - elif grep -q "## Index of scoped AGENTS.md" "$file"; then + elif grep -qE "$SCOPE_INDEX_HEADING_RE" "$file"; then success "Root has scope index (verbose style acceptable)" return 0 else @@ -191,7 +197,7 @@ check_scoped_sections() { check_scope_links() { local root_file="$1" - if ! grep -q "## Index of scoped AGENTS.md" "$root_file"; then + if ! grep -qE "$SCOPE_INDEX_HEADING_RE" "$root_file"; then # No scope index (thin root without scopes) -- nothing to check. Set an # explicit status so a subsequent record_check does not reuse the # previous check's LAST_STATUS/LAST_DETAIL in --json mode. @@ -201,7 +207,7 @@ check_scope_links() { # Extract links from scope index local links - links=$(sed -n '/## Index of scoped AGENTS.md/,/^##/p' "$root_file" | grep -o '\./[^)]*AGENTS.md' || true) + links=$(sed -nE "/$SCOPE_INDEX_HEADING_RE/,/^##/p" "$root_file" | grep -o '\./[^)]*AGENTS.md' || true) if [ -z "$links" ]; then # Empty scope index with AGENTS-GENERATED markers is valid (placeholder) From e17369c5928cd682078b781855820534b43915df Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Sat, 27 Jun 2026 08:43:38 +0200 Subject: [PATCH 2/2] test(agent-rules): make scope-index test sed portable to BSD sed Replace GNU-only `sed -i` with a temp-file + mv form so the regression test runs on BSD sed (macOS) as well as GNU sed; this repo's CI also runs the generator on macos-latest. Signed-off-by: Sebastian Mendel --- .../scripts/tests/test-scope-index-heading.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/skills/agent-rules/scripts/tests/test-scope-index-heading.sh b/skills/agent-rules/scripts/tests/test-scope-index-heading.sh index 41e8b96..401dfe3 100755 --- a/skills/agent-rules/scripts/tests/test-scope-index-heading.sh +++ b/skills/agent-rules/scripts/tests/test-scope-index-heading.sh @@ -61,7 +61,10 @@ fi FX2="$WORK/legacy-heading" cp -r "$FX1" "$FX2" # Rewrite only the heading line, keeping the rest of the generated root intact. -sed -i "s|^${NEW_HEADING}\$|${LEGACY_HEADING}|" "$FX2/AGENTS.md" +# Use a temp file + mv rather than `sed -i` so the test stays portable across +# GNU sed (Linux) and BSD sed (macOS); this repo's CI also runs on macos-latest. +sed "s|^${NEW_HEADING}\$|${LEGACY_HEADING}|" "$FX2/AGENTS.md" > "$FX2/AGENTS.md.tmp" +mv "$FX2/AGENTS.md.tmp" "$FX2/AGENTS.md" grep -qF "$LEGACY_HEADING" "$FX2/AGENTS.md" || fail "could not rewrite heading to legacy form" if bash "$VALIDATE" "$FX2" >/dev/null 2>&1; then @@ -74,7 +77,9 @@ fi FX3="$WORK/no-heading" cp -r "$FX1" "$FX3" # Drop the scope-index heading so the >50-line root has no index at all. -sed -i "/^${NEW_HEADING}\$/d" "$FX3/AGENTS.md" +# Temp file + mv keeps this portable across GNU and BSD sed (see Test 2). +sed "/^${NEW_HEADING}\$/d" "$FX3/AGENTS.md" > "$FX3/AGENTS.md.tmp" +mv "$FX3/AGENTS.md.tmp" "$FX3/AGENTS.md" if bash "$VALIDATE" "$FX3" >/dev/null 2>&1; then fail "validate-structure.sh accepted a bloated root with no scope index"