mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
8cdcb5a903
> **Residues closed on the caller's merge instruction** (`499d916a6`): the round-2 findings were the same failure shape — round-1 repairs patched cited lines instead of sweeping the class — so this commit sweeps each file whole: every remaining SATURATED-row-append site in skill-eval now routes to RUNBOOK retirement, the human-only-skills *description* is runtime-conditional, premortem's "(MEASURED)" label is gone, SKILL-API's context table carries all 25 rows and the enforcement table gains `disable-model-invocation`, and the fixture-identity claim is stated precisely (probe id, honesty note, and control arm are the only differing fields — as the acceptance permits). Post-sweep: validators, full Go suite, 68/68 gates, goldens + headroom bats green, projections current, gemini in sync. Merging per Bo's instruction. ## What Train 1 of the accepted [instrument-panel roadmap](docs/plans/2026-08-26-instrument-panel-roadmap.md) (intent landed at `986a4feaf`): the measurement substrate, the skill-context diet, and the retrieval-eval contract. Three worktree-isolated lanes, each independently validated by a fresh context, plus one integration commit. 103 files, +5,510/−76. **L1 — measurement substrate** (`instrument/measurement-substrate`) - Gate `skill.probe-headroom` (advisory, Fast|Full): answers the question `skill.probe-coverage` cannot — not "does a probe result exist" but "could one have existed at all". The rule, ported to Go (`cli/internal/probeheadroom` + `cli/cmd/probe-headroom` behind a thin check script — the witness-crosscheck pattern, **no new `ao` root command**): control arm ≥ 0.75 with ≥ 2 usable reps at ≥ 2 effort levels ⇒ SATURATED (void row, not an honest null); UNMEASURED outranks it; treatment-silent ⇒ FLOOR; else SEPARATED. RED first: both committed fixture pairs read `INERT` to everything else in the repo; the failing separation test predates the implementation, and a bats negative-control swaps fixture bytes and asserts the gate flips. - **First reading on real data: 7 of 11 historical probe groups are SATURATED** — including both `validate-not-proven` runs. Those INERT rows were never honest nulls; they were void. The 0/12 ledger number now argues itself. - Declared denominator for probe-coverage: `scripts/.skill-probe-denominator-exclusions`, fail-closed parser (entry without an argument, stale slug, or duplicate ⇒ exit 2). One entry (`goals`, a pure alias-of `fitness`). Net effect deliberately zero (0/12 → 0/12: alias left, `one-way-door` entered) — the gain is a declared number, not a better-looking one. - Re-landed from the recovered clean-room commit (`9872483bd`), re-validated against *current* main: `skill-eval` (defers saturation to the gate id; its shell scripts dropped, not shipped — ratchet intent), `route`, `one-way-door`, premortem reversibility check, council `caller_challenge` (schema + validator, per the agent-core boundary that the panel may challenge, never overrule). **L4 — context diet** (`instrument/context-diet`) - `disable-model-invocation: true` on 4 human-only skills (key verified verbatim against Anthropic's docs). The plan guessed 35 candidates; the graph said otherwise — 23 carry `user-invocable: true`, and 19 of those are excluded on cited evidence (rpi consumes anti-ceremony/implement/plan/validate; workflow scripts reach others; `goals` is a live migration tombstone). The exclusion evidence is retained in the lane report. - One router skill (`human-only-skills`) — the single always-loaded description that replaces four; it hints, never fires. - `.out-of-scope/` formalized with this week's three refusals (checked-in knowledge corpus; ee self-improvement loops; whole-skill A/B as the measurement unit), each citing its evidence. - Deterministic proof, no model eval: before/after bytes of always-loaded description load reported in the lane summary. **L5 — retrieval-eval contract** (`instrument/retrieval-contract`, lane verdict PASS 10/10) - `AGENTS.md` federated row now names **ee (eidetic-engine)** as a concrete caller-selected memory system — consume, never build; symlink intact. - `schemas/pack-quality-expectations.v1.schema.json` + 4 routing goldens + `scripts/check-routing-probe-goldens.sh` graded against `ao skills find`, wired as an **advisory** nightly job. Zero goldens is a failing state — no new zero-denominator green. - **The instrument caught a real miss on day one — and its own prescription fixed it.** Golden `rq-04` expects `validate` for "judge whether this finished change is actually proven before I merge it"; at authoring, `ao skills find` ranked the *forbidden* `premortem` first and `validate` nowhere in six natural phrasings. The pointer-wording-first repair (validate's description gained the caller's own words: finished, proven, verdict, merge) now ranks it #1 at 0.333; grader 6/6, and the golden pins the repair — a description regression reopens it. ## Integration `regen-all.sh` once over the merged lanes (catalog 52 → 56, four new codex twins, mesh, router, manifests); `skills/route/SKILL.md` catalog/router links became prose repo-root references (the projected twin cannot resolve `../catalog.json` — this was both the portable-conformance failure and the sole broken doc link); `codex-portable-conformance.bats` pin 52 → 56. ## Evidence - `cd cli && go build ./... && go vet ./... && go test ./...` exit 0 · `ao gate check --full` **68/68** · four skill validators PASS · probe-headroom / routing-goldens / probe-coverage bats PASS · `regen-all.sh --check` all current. - Per-lane fresh validators re-ran every suite on detached content; L5 PASS; L1/L4 NOT_PROVEN solely on the projection-regen clause reserved for integration (their remaining acceptance observed green), settled above. Cross-family (Codex) review of the integrated diff recorded in the session report. - Two disclosed scope stretches accepted at integration: a one-line `.gitignore` entry mirroring the witness-crosscheck precedent, and the probe LEDGER.md fact-correction L1's own change made necessary (noted for Train 2's L2, which owns that file next). ## Cross-family review (Codex, fresh context) Round 1: **FAIL** — two blockers (the RED fixtures didn't isolate the control arm; the goldens grader was red where the plan's acceptance says green) and eight majors (contract contradictions in the re-landed skills, a converter-substitution false claim in the codex router twin, two overreaching `.out-of-scope` entries, stale SKILL-API counts). All repaired in one bounded round (`db68935a3`): fixtures now byte-identical outside the control arm, the routing miss actually fixed rather than tolerated, every cited contradiction reconciled at the source and re-projected. Post-repair: full Go suite exit 0, `gate check --full` 68/68, all validators and probe/goldens bats green, projections current, gemini byte-identity restored. Focused re-check verdict recorded in the session report. ## Follow-ups (Train 2, already planned) Seeded-defect probes for the judgment spine (every ledger row citing a passing headroom pre-screen) and the gate-hardening pair (`Gate-Loosen-Reason` tightening ratchet; mechanical grounding-validation over evidence docs). Plus, surfaced by this train: a latent `valid_keys`/schema divergence in `validate-skill-schema.sh` (two keys the schema defines are absent from the script's allowlist — pre-existing).
327 lines
9.2 KiB
Bash
Executable File
327 lines
9.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# validate-skill-schema.sh — Validate SKILL.md YAML frontmatter against JSON schema
|
|
#
|
|
# Finds all skills/*/SKILL.md files, extracts YAML frontmatter, and validates
|
|
# each against schemas/skill-frontmatter.v1.schema.json.
|
|
#
|
|
# Validation tiers (best available wins):
|
|
# 1. yq + python3 jsonschema — full JSON Schema Draft-07 validation
|
|
# 2. yq + jq — structural checks (required fields, types, enum values)
|
|
# 3. grep — basic required-field presence check
|
|
#
|
|
# Usage: scripts/validate-skill-schema.sh [--verbose]
|
|
# Exit: 0 = all pass, 1 = failures found
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
SCHEMA="$REPO_ROOT/schemas/skill-frontmatter.v1.schema.json"
|
|
SKILLS_DIR="$REPO_ROOT/skills"
|
|
VERBOSE=0
|
|
|
|
if [[ "${1:-}" == "--verbose" ]]; then
|
|
VERBOSE=1
|
|
fi
|
|
|
|
# --- Colors (disabled in CI / non-tty) ---
|
|
if [[ -t 1 ]]; then
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; NC='\033[0m'
|
|
else
|
|
RED=''; GREEN=''; YELLOW=''; NC=''
|
|
fi
|
|
|
|
pass=0
|
|
fail=0
|
|
warn=0
|
|
total=0
|
|
failures=""
|
|
|
|
# --- Extract YAML frontmatter from a SKILL.md file ---
|
|
# Outputs everything between the first pair of --- delimiters (exclusive).
|
|
extract_frontmatter() {
|
|
local file="$1"
|
|
awk 'BEGIN{found=0} /^---$/{if(found){exit}else{found=1;next}} found{print}' "$file"
|
|
}
|
|
|
|
# --- Detect validation tier ---
|
|
HAS_YQ=0
|
|
HAS_PYTHON_JSONSCHEMA=0
|
|
HAS_JQ=0
|
|
YQ_VARIANT=""
|
|
|
|
if command -v yq &>/dev/null; then
|
|
HAS_YQ=1
|
|
# Detect yq variant: "go" (Mike Farah, supports `-o json`) vs "python" (kislyuk,
|
|
# jq wrapper that emits JSON by default from YAML). Both are packaged under the
|
|
# same binary name in different distros.
|
|
if yq --help 2>&1 | grep -q 'jq filter'; then
|
|
YQ_VARIANT="python"
|
|
else
|
|
YQ_VARIANT="go"
|
|
fi
|
|
fi
|
|
|
|
if command -v jq &>/dev/null; then
|
|
HAS_JQ=1
|
|
fi
|
|
|
|
# yq_to_json: run a jq filter over YAML on stdin and emit JSON, regardless of
|
|
# which yq variant is installed.
|
|
yq_to_json() {
|
|
local filter="${1:-.}"
|
|
case "$YQ_VARIANT" in
|
|
go) yq -o json "$filter" ;;
|
|
python) yq "$filter" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
if command -v python3 &>/dev/null; then
|
|
if python3 -c "import jsonschema, json, yaml" &>/dev/null; then
|
|
HAS_PYTHON_JSONSCHEMA=1
|
|
fi
|
|
fi
|
|
|
|
if [[ $HAS_YQ -eq 1 && $HAS_PYTHON_JSONSCHEMA -eq 1 ]]; then
|
|
TIER="full"
|
|
echo "=== SKILL.md Schema Validation (full: yq + python3 jsonschema) ==="
|
|
elif [[ $HAS_YQ -eq 1 && $HAS_JQ -eq 1 ]]; then
|
|
TIER="structural"
|
|
echo "=== SKILL.md Schema Validation (structural: yq + jq) ==="
|
|
else
|
|
TIER="basic"
|
|
echo "=== SKILL.md Schema Validation (basic: grep) ==="
|
|
fi
|
|
|
|
# --- Full validation via python3 jsonschema ---
|
|
validate_full() {
|
|
local skill_name="$1"
|
|
local frontmatter="$2"
|
|
|
|
local json_data
|
|
json_data=$(echo "$frontmatter" | yq_to_json '.' 2>&1) || {
|
|
echo -e " ${RED}FAIL${NC} $skill_name: invalid YAML frontmatter"
|
|
[[ $VERBOSE -eq 1 ]] && echo " yq error: $json_data"
|
|
return 1
|
|
}
|
|
|
|
local result
|
|
result=$(python3 -c "
|
|
import json, sys
|
|
from jsonschema import validate, ValidationError
|
|
|
|
schema = json.load(open('$SCHEMA'))
|
|
data = json.loads(sys.stdin.read())
|
|
try:
|
|
validate(instance=data, schema=schema)
|
|
print('OK')
|
|
except ValidationError as e:
|
|
print(f'FAIL: {e.message}')
|
|
" <<< "$json_data" 2>&1) || true
|
|
|
|
if [[ "$result" == "OK" ]]; then
|
|
[[ $VERBOSE -eq 1 ]] && echo -e " ${GREEN}PASS${NC} $skill_name"
|
|
return 0
|
|
else
|
|
echo -e " ${RED}FAIL${NC} $skill_name: ${result#FAIL: }"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# --- Structural validation via yq + jq ---
|
|
validate_structural() {
|
|
local skill_name="$1"
|
|
local frontmatter="$2"
|
|
local errors=""
|
|
|
|
local json_data
|
|
json_data=$(echo "$frontmatter" | yq_to_json '.' 2>&1) || {
|
|
echo -e " ${RED}FAIL${NC} $skill_name: invalid YAML frontmatter"
|
|
return 1
|
|
}
|
|
|
|
# Check required fields
|
|
for field in name description skill_api_version; do
|
|
if ! echo "$json_data" | jq -e ".[\"$field\"]" &>/dev/null; then
|
|
errors="${errors}missing required field '$field'; "
|
|
fi
|
|
done
|
|
|
|
# Check name is string
|
|
local name_type
|
|
name_type=$(echo "$json_data" | jq -r '.name | type' 2>/dev/null)
|
|
if [[ "$name_type" != "string" && -z "$errors" ]]; then
|
|
errors="${errors}'name' must be a string (got $name_type); "
|
|
fi
|
|
|
|
# Check description is string
|
|
local desc_type
|
|
desc_type=$(echo "$json_data" | jq -r '.description | type' 2>/dev/null)
|
|
if [[ "$desc_type" != "string" ]]; then
|
|
errors="${errors}'description' must be a string (got $desc_type); "
|
|
fi
|
|
|
|
# Check skill_api_version is 1
|
|
local api_ver
|
|
api_ver=$(echo "$json_data" | jq -r '.skill_api_version' 2>/dev/null)
|
|
if [[ "$api_ver" != "1" ]]; then
|
|
errors="${errors}'skill_api_version' must be 1 (got $api_ver); "
|
|
fi
|
|
|
|
# Check metadata.tier enum if present
|
|
local tier
|
|
tier=$(echo "$json_data" | jq -r '.metadata.tier // empty' 2>/dev/null)
|
|
if [[ -n "$tier" ]]; then
|
|
local valid_tiers="judgment execution library session product contribute meta background orchestration cross-vendor knowledge experimental"
|
|
if ! echo "$valid_tiers" | grep -qw "$tier"; then
|
|
errors="${errors}'metadata.tier' invalid value '$tier'; "
|
|
fi
|
|
fi
|
|
|
|
# Check for unknown top-level keys (additionalProperties: false)
|
|
local valid_keys="name description skill_api_version metadata user-invocable disable-model-invocation context allowed-tools license compatibility model output_contract practices hexagonal_role consumes produces context_rel"
|
|
local actual_keys
|
|
actual_keys=$(echo "$json_data" | jq -r 'keys[]' 2>/dev/null)
|
|
for key in $actual_keys; do
|
|
if ! echo "$valid_keys" | grep -qw "$key"; then
|
|
errors="${errors}unknown top-level property '$key'; "
|
|
fi
|
|
done
|
|
|
|
if [[ -n "$errors" ]]; then
|
|
echo -e " ${RED}FAIL${NC} $skill_name: $errors"
|
|
return 1
|
|
else
|
|
[[ $VERBOSE -eq 1 ]] && echo -e " ${GREEN}PASS${NC} $skill_name"
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
# --- Basic validation via grep ---
|
|
validate_basic() {
|
|
local skill_name="$1"
|
|
local frontmatter="$2"
|
|
local errors=""
|
|
|
|
# Check required fields exist
|
|
if ! echo "$frontmatter" | grep -q "^name:"; then
|
|
errors="${errors}missing 'name'; "
|
|
fi
|
|
if ! echo "$frontmatter" | grep -q "^description:"; then
|
|
errors="${errors}missing 'description'; "
|
|
fi
|
|
if ! echo "$frontmatter" | grep -q "^skill_api_version:"; then
|
|
errors="${errors}missing 'skill_api_version'; "
|
|
fi
|
|
|
|
# Check skill_api_version value
|
|
if echo "$frontmatter" | grep -q "^skill_api_version:"; then
|
|
local ver
|
|
ver=$(echo "$frontmatter" | grep "^skill_api_version:" | sed 's/skill_api_version:[[:space:]]*//')
|
|
if [[ "$ver" != "1" ]]; then
|
|
errors="${errors}'skill_api_version' must be 1 (got '$ver'); "
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$errors" ]]; then
|
|
echo -e " ${RED}FAIL${NC} $skill_name: $errors"
|
|
return 1
|
|
else
|
|
[[ $VERBOSE -eq 1 ]] && echo -e " ${GREEN}PASS${NC} $skill_name"
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
# --- Validate schema file exists ---
|
|
if [[ ! -f "$SCHEMA" ]]; then
|
|
echo -e "${YELLOW}WARNING${NC}: Schema file not found at $SCHEMA"
|
|
echo "Falling back to basic validation without schema."
|
|
TIER="basic"
|
|
fi
|
|
|
|
# --- Main loop ---
|
|
for skill_dir in "$SKILLS_DIR"/*/; do
|
|
[[ ! -d "$skill_dir" ]] && continue
|
|
skill_name=$(basename "$skill_dir")
|
|
# Leading-underscore dirs (e.g. skills/_fixtures/) are non-skill scaffolding
|
|
# (planted test fixtures); they are not real skills, so don't schema-validate.
|
|
[[ "$skill_name" == _* ]] && continue
|
|
skill_file="$skill_dir/SKILL.md"
|
|
|
|
if [[ ! -f "$skill_file" ]]; then
|
|
echo -e " ${YELLOW}SKIP${NC} $skill_name: no SKILL.md"
|
|
warn=$((warn + 1))
|
|
continue
|
|
fi
|
|
|
|
# Runtime compatibility pointers are redirect packages, not independent
|
|
# implementations. The redirect gate owns their compact schema.
|
|
if grep -Eq '^implementation:[[:space:]]+false([[:space:]]|$)' "$skill_file"; then
|
|
continue
|
|
fi
|
|
|
|
# Verify frontmatter delimiters exist
|
|
if ! head -1 "$skill_file" | grep -q "^---$"; then
|
|
echo -e " ${RED}FAIL${NC} $skill_name: SKILL.md does not start with ---"
|
|
fail=$((fail + 1))
|
|
failures="${failures} - $skill_name\n"
|
|
total=$((total + 1))
|
|
continue
|
|
fi
|
|
|
|
frontmatter=$(extract_frontmatter "$skill_file")
|
|
if [[ -z "$frontmatter" ]]; then
|
|
echo -e " ${RED}FAIL${NC} $skill_name: empty frontmatter"
|
|
fail=$((fail + 1))
|
|
failures="${failures} - $skill_name\n"
|
|
total=$((total + 1))
|
|
continue
|
|
fi
|
|
|
|
total=$((total + 1))
|
|
|
|
case "$TIER" in
|
|
full)
|
|
if validate_full "$skill_name" "$frontmatter"; then
|
|
pass=$((pass + 1))
|
|
else
|
|
fail=$((fail + 1))
|
|
failures="${failures} - $skill_name\n"
|
|
fi
|
|
;;
|
|
structural)
|
|
if validate_structural "$skill_name" "$frontmatter"; then
|
|
pass=$((pass + 1))
|
|
else
|
|
fail=$((fail + 1))
|
|
failures="${failures} - $skill_name\n"
|
|
fi
|
|
;;
|
|
basic)
|
|
if validate_basic "$skill_name" "$frontmatter"; then
|
|
pass=$((pass + 1))
|
|
else
|
|
fail=$((fail + 1))
|
|
failures="${failures} - $skill_name\n"
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# --- Summary ---
|
|
echo ""
|
|
echo "--- Results ---"
|
|
echo "Total: $total | Pass: $pass | Fail: $fail | Warn: $warn"
|
|
|
|
if [[ $fail -gt 0 ]]; then
|
|
echo ""
|
|
echo "Failed skills:"
|
|
echo -e "$failures"
|
|
echo "FAIL: $fail skill(s) failed schema validation"
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo "All $pass skill(s) passed schema validation"
|
|
exit 0
|