Просмотр исходного кода

merge: doc-drift prose/ghost-ref extensions + exec-bit gate, adversarially hardened

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0xDarkMatter 6 дней назад
Родитель
Сommit
841af8a82f

+ 3 - 0
.github/workflows/validate.yml

@@ -32,6 +32,9 @@ jobs:
       - name: Doc-drift gate (docs must match disk)
         run: bash tests/doc-drift.sh
 
+      - name: Executable-bit gate (tracked scripts must be executable)
+        run: bash tests/check-exec-bits.sh
+
       - name: Run skill behavioural test suites
         run: bash tests/run-skill-tests.sh
 

+ 1 - 1
skills/claude-code-ops/SKILL.md

@@ -7,7 +7,7 @@ compatibility: "Claude Code CLI v2.1.x (June 2026 docs)"
 allowed-tools: "Bash Read Grep"
 metadata:
   author: claude-mods
-  related-skills: "mcp-ops, setperms, dsp-launch"
+  related-skills: "mcp-ops, setperms"
 ---
 
 # Claude Code Internals

+ 1 - 1
skills/color-ops/scripts/color-convert.js

@@ -5,7 +5,7 @@ license: MIT
 allowed-tools: "Read Write Bash"
 metadata:
   author: claude-mods
-  related-skills: claude-code-hooks, claude-code-debug, typescript-ops, python-fastapi-ops
+  related-skills: claude-code-ops, typescript-ops, python-fastapi-ops
 ---
 
 # MCP Operations

+ 1 - 1
skills/net-ops/SKILL.md

@@ -5,7 +5,7 @@ license: MIT
 allowed-tools: "Read Write Bash"
 metadata:
   author: claude-mods
-  related-skills: debug-ops, network-tools
+  related-skills: debug-ops
 ---
 
 # Network Operations

+ 1 - 1
skills/spawn/SKILL.md

@@ -6,7 +6,7 @@ compatibility: "Requires internet access for WebSearch/WebFetch to research offi
 allowed-tools: "Read Write Bash WebSearch WebFetch AskUserQuestion"
 metadata:
   author: claude-mods
-  related-skills: claude-code-templates
+  related-skills: ""
 ---
 
 # Spawn - Expert Agent Generator

+ 1 - 1
skills/ytdlp-ops/SKILL.md

@@ -7,7 +7,7 @@ compatibility: "yt-dlp 2025.x+ (releases near-monthly; run the verifier FIRST wh
 allowed-tools: "Read Write Edit Bash Glob Grep"
 metadata:
   author: claude-mods
-  related-skills: ffmpeg-ops, cutcraft, debug-ops
+  related-skills: ffmpeg-ops, debug-ops
 ---
 
 # yt-dlp Operations

+ 69 - 0
tests/check-exec-bits.sh

@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+# Check git index modes, not filesystem permissions (Git Bash can fake those).
+# Root tests/*.sh and scripts/*.sh are excluded because tracked 100644 files
+# there are legitimate today; this gate covers skill-bundled scripts only.
+set -u
+
+usage() {
+    cat <<'EOF'
+Usage: tests/check-exec-bits.sh [--help]
+
+Print tracked skill scripts whose git mode is not 100755, one per line.
+
+EXAMPLES
+  bash tests/check-exec-bits.sh
+  bash tests/check-exec-bits.sh --help
+
+Exit codes: 0 clean, 1 findings, 2 usage error.
+EOF
+}
+
+case "${1-}" in
+    "") ;;
+    -h|--help) usage; exit 0 ;;
+    *) echo "check-exec-bits: unknown argument: $1" >&2; usage >&2; exit 2 ;;
+esac
+[ "$#" -le 1 ] || { echo "check-exec-bits: too many arguments" >&2; usage >&2; exit 2; }
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT" || exit 2
+
+findings=0
+candidates=0
+while read -r mode object stage path; do
+    case "$path" in
+        skills/*/scripts/*)
+            # A "script" is: known script extension, OR extensionless with a
+            # shebang (e.g. introspect/scripts/cc-session). Data/doc files and
+            # .gitkeep placeholders are not scripts.
+            base="${path##*/}"
+            is_script=0
+            case "$base" in
+                *.sh|*.py|*.mjs|*.js) is_script=1 ;;
+                *.*|.*) ;; # other extensions / dotfiles: not gated
+                *) head -c 2 -- "$path" 2>/dev/null | grep -q '^#!' && is_script=1 ;;
+            esac
+            [ "$is_script" -eq 1 ] || continue
+            candidates=$((candidates + 1))
+            if [ "$mode" != "100755" ]; then
+                echo "$path"
+                findings=$((findings + 1))
+            fi
+            ;;
+    esac
+done < <(git ls-files -s -- skills)
+
+# Empty candidate list means the scan itself is broken (not in a repo, path
+# typo, git missing) — a gate that checked nothing must not report clean.
+if [ "$candidates" -eq 0 ]; then
+    echo "check-exec-bits: self-check failed — zero candidate scripts found (broken scan?)" >&2
+    exit 2
+fi
+
+if [ "$findings" -eq 0 ]; then
+    echo "check-exec-bits: clean ($candidates scripts checked)" >&2
+    exit 0
+fi
+
+echo "check-exec-bits: $findings file(s) are not tracked as 100755" >&2
+exit 1

+ 51 - 1
tests/doc-drift.sh

@@ -3,10 +3,11 @@
 #
 # Checks:
 #   1. Component counts on disk vs claims in README.md header, AGENTS.md
-#      overview bullets, and docs/PLAN.md inventory table
+#      overview bullets, docs/PLAN.md inventory table, and selected README prose
 #   2. Every skill directory has a row in a README skill table
 #   3. Every repo-relative markdown link in README.md / AGENTS.md resolves
 #      to an existing file or directory (no ghost references)
+#   4. Skill frontmatter references only skills that exist on disk
 #
 # Exit 0 = clean, exit 1 = drift detected.
 set -u
@@ -44,6 +45,27 @@ else
     [ "$r_rules"  = "$rules_disk"  ] || err "README header: $r_rules rules claimed, $rules_disk on disk"
 fi
 
+# --- Selected README prose count claims -------------------------------------
+# These five known count-bearing patterns are intentionally exhaustive; newly
+# introduced prose patterns must be added explicitly if they should be gated.
+check_readme_prose() { # $1=regex $2=disk-count $3=label
+    local line claim matched=0
+    while IFS= read -r line; do
+        [ -n "$line" ] || continue
+        matched=$((matched + 1))
+        claim="$(echo "$line" | grep -oE '[0-9]+' | head -1)"
+        [ "$claim" = "$2" ] || err "README.md: $claim $3 claimed, $2 on disk"
+    done < <(grep -E "$1" README.md || true)
+    # Zero matches = the count-bearing line was deleted or reworded, which is
+    # drift too — a guard that finds nothing to check must not stay silent.
+    [ "$matched" -ge 1 ] || err "README.md: prose pattern for '$3' matched no lines (deleted/reworded?)"
+}
+check_readme_prose '[0-9]+ specialized skills' "$skills_disk" "specialized skills"
+check_readme_prose '[0-9]+ on-demand skills' "$skills_disk" "on-demand skills"
+check_readme_prose 'Custom skills \([0-9]+\)' "$skills_disk" "custom skills"
+check_readme_prose 'Slash commands \([0-9]+\)' "$commands_disk" "slash commands"
+check_readme_prose 'Expert subagents \([0-9]+\)' "$agents_disk" "expert subagents"
+
 # --- AGENTS.md overview bullets ---------------------------------------------
 check_agents_md() { # $1=regex $2=disk-count $3=label
     local claim
@@ -92,6 +114,34 @@ for doc in README.md AGENTS.md; do
              | sed -E 's/^\]\(//; s/\)$//')
 done
 
+# --- 4. Skill frontmatter ghost references ---------------------------------
+# These are Claude-Code-bundled skills, not skills shipped by this repo.
+# Keep this allowlist narrow: every other unknown reference is a failure.
+external_skills="frontend-design"
+
+for skill_file in skills/*/SKILL.md; do
+    while IFS= read -r refs; do
+        refs="${refs#*:}"
+        refs="${refs#"${refs%%[![:space:]]*}"}"
+        refs="${refs%"${refs##*[![:space:]]}"}"
+        refs="${refs#\"}"; refs="${refs%\"}"
+        IFS=',' read -ra names <<< "$refs"
+        for name in "${names[@]}"; do
+            name="${name#"${name%%[![:space:]]*}"}"
+            name="${name%"${name##*[![:space:]]}"}"
+            [ -z "$name" ] && continue
+            case " $external_skills " in *" $name "*) continue ;; esac
+            [ -d "skills/$name" ] || err "$skill_file: unknown skill reference '$name'"
+        done
+    done < <(awk '
+        NR == 1 && $0 == "---" { frontmatter=1; next }
+        frontmatter && $0 == "---" { exit }
+        frontmatter && /^metadata:/ { metadata=1; next }
+        metadata && /^[^ ]/ { metadata=0 }
+        metadata && /^  (related-skills|depends-on):/ { print }
+    ' "$skill_file")
+done
+
 echo
 if [ "$errors" -eq 0 ]; then
     echo "doc-drift: clean"