diff --git a/skills-codex/.agentops-manifest.json b/skills-codex/.agentops-manifest.json index e66074bd5..b0b0c23ad 100644 --- a/skills-codex/.agentops-manifest.json +++ b/skills-codex/.agentops-manifest.json @@ -811,7 +811,7 @@ { "name": "heal-skill", "source_skill": "skills/heal-skill", - "source_hash": "bae0dc8ce6e916ad059776627ce2f94a42ed1ef15ff9c169f07ecd3cfb2f8c5e", + "source_hash": "901f981bf69ce82bfc7535b38241c672c0465978c6aaaa3f91cc5176c47a1b91", "generated_hash": "0a4b01de799212b1c33f718aba85c1ea0e03a033542a8e81929ddbd52425ab4b" }, { diff --git a/skills-codex/heal-skill/.agentops-generated.json b/skills-codex/heal-skill/.agentops-generated.json index 5b8f4b809..3137a04bc 100644 --- a/skills-codex/heal-skill/.agentops-generated.json +++ b/skills-codex/heal-skill/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "manual-maintained", "source_skill": "skills/heal-skill", "layout": "modular", - "source_hash": "bae0dc8ce6e916ad059776627ce2f94a42ed1ef15ff9c169f07ecd3cfb2f8c5e", + "source_hash": "901f981bf69ce82bfc7535b38241c672c0465978c6aaaa3f91cc5176c47a1b91", "generated_hash": "0a4b01de799212b1c33f718aba85c1ea0e03a033542a8e81929ddbd52425ab4b" } diff --git a/skills/heal-skill/scripts/heal.sh b/skills/heal-skill/scripts/heal.sh index 2e26591e3..718f28be0 100755 --- a/skills/heal-skill/scripts/heal.sh +++ b/skills/heal-skill/scripts/heal.sh @@ -20,9 +20,11 @@ while [[ $# -gt 0 ]]; do esac done -# Find repo root (location of skills/ directory) +# Find repo root (location of skills/ directory). HEAL_REPO_ROOT overrides for +# fixture-driven tests (tests/scripts/heal-dispositions.bats); production derives +# it from the script location. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +REPO_ROOT="${HEAL_REPO_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}" SKILLS_ROOT="$REPO_ROOT/skills" # If no targets, scan all skill dirs (skills/ and skills-codex/) @@ -459,6 +461,25 @@ for skill_check in "$SKILLS_ROOT"/*/SKILL.md; do fi done +# Check 12: Dispositions coverage (global, not per-skill). Every user-invocable +# skills/ must have a row in docs/contracts/skill-dispositions.yaml. This is +# the gate that silently passed when /burndown (ag-3yl8 #600) was added, costing +# a CI round — heal --strict now catches it locally (ag-cw2y item 1). +DISPOSITIONS_FILE="$REPO_ROOT/docs/contracts/skill-dispositions.yaml" +if [[ -f "$DISPOSITIONS_FILE" ]]; then + for skill_check in "$SKILLS_ROOT"/*/SKILL.md; do + [[ -f "$skill_check" ]] || continue + check_dir="$(dirname "$skill_check")" + check_name="$(basename "$check_dir")" + # Skip internal/non-invocable skills (same exemptions as catalog check). + if grep -q 'user-invocable: false' "$skill_check" 2>/dev/null; then continue; fi + if grep -q 'internal: true' "$skill_check" 2>/dev/null; then continue; fi + if ! grep -qE "^[[:space:]]*-[[:space:]]+skill:[[:space:]]+${check_name}[[:space:]]*$" "$DISPOSITIONS_FILE" 2>/dev/null; then + report "MISSING_DISPOSITION" "$check_dir" "user-invocable but has no row in docs/contracts/skill-dispositions.yaml" + fi + done +fi + if [[ $FINDINGS -gt 0 ]]; then echo "" echo "$FINDINGS finding(s) detected." diff --git a/tests/scripts/heal-dispositions.bats b/tests/scripts/heal-dispositions.bats new file mode 100644 index 000000000..84174c05c --- /dev/null +++ b/tests/scripts/heal-dispositions.bats @@ -0,0 +1,53 @@ +#!/usr/bin/env bats +# Regression for ag-cw2y item 1: heal --strict must flag a user-invocable skill +# that has no row in docs/contracts/skill-dispositions.yaml. This is the exact +# gate that silently passed when /burndown (ag-3yl8 #600) was added, costing a +# CI round. The check is fixture-driven via the HEAL_REPO_ROOT override. + +setup() { + HEAL="$BATS_TEST_DIRNAME/../../skills/heal-skill/scripts/heal.sh" + FIX="$(mktemp -d)" + mkdir -p "$FIX/skills/foo" "$FIX/docs/contracts" + cat > "$FIX/skills/foo/SKILL.md" <<'EOF' +--- +name: foo +description: A fixture skill for the dispositions coverage check. +skill_api_version: 1 +--- +# foo +EOF + cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'EOF' +dispositions: + - skill: bar + domain: "BC1 Corpus" + hexagonal_role: domain + disposition: keep + rationale: "unrelated existing row" +EOF +} + +teardown() { rm -rf "$FIX"; } + +@test "heal flags a user-invocable skill missing from skill-dispositions.yaml" { + run env HEAL_REPO_ROOT="$FIX" bash "$HEAL" --check skills/foo + [[ "$output" == *"MISSING_DISPOSITION"* ]] + [[ "$output" == *"foo"* ]] +} + +@test "heal does NOT flag a skill that has a dispositions row" { + cat >> "$FIX/docs/contracts/skill-dispositions.yaml" <<'EOF' + - skill: foo + domain: "BC1 Corpus" + hexagonal_role: domain + disposition: keep + rationale: "now covered" +EOF + run env HEAL_REPO_ROOT="$FIX" bash "$HEAL" --check skills/foo + [[ "$output" != *"MISSING_DISPOSITION"* ]] +} + +@test "heal --strict exits non-zero and names the missing disposition" { + run env HEAL_REPO_ROOT="$FIX" bash "$HEAL" --check --strict skills/foo + [ "$status" -eq 1 ] + [[ "$output" == *"MISSING_DISPOSITION"* ]] +}