mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
092b99dad9
## What
Adds `scripts/skill-eval.sh` — a thin gate that runs one skill's
`SKILL.md` through Jeff Emanuel's `ms` (meta_skill) `lint` + `validate`.
The wrapper decides the gate purely on whether a **blocking** `rule_id`
appears (ms's own severity/exit is not trusted):
- Blocking: `no-secrets`, `no-injection`, `safe-paths`,
`required-metadata`, `no-cycle`, `valid-version` → non-zero exit.
- Everything else → annotated only (`::warning::`), never fails.
- `ms` not on PATH (or `MS_BIN` missing) → **loud `::error::` hard-fail
(exit 3)**, never skip-and-pass — a silent skip would recreate the
no-evaluation gap this gate closes.
Planted `skills/_fixtures/{bad,good}-skill/SKILL.md` fixtures +
`tests/scripts/skill-eval.bats` (10 tests) exercise the
bad/good/ms-absent contracts.
## Fixture scoping
`skills/_fixtures/` is non-skill scaffolding (planted fixtures). The
skill-enumerating gates/generators are taught to skip `skills/_*` so the
fixtures never inflate the skill count (stays 82) or fail registry /
schema / wiring / runtime / count gates. Touched enumerators:
`generate-registry`, `validate-skill-schema`, `sync-skill-counts`,
`validate-skill-count`, `generate-skill-catalog`,
`generate-context-map`, `validate-manifests`, `check-wiring-closure`,
`validate-headless-runtime-skills`, `lib/sku_catalog.py`,
`check-alias-collisions`, `run-all`, `test-runtime-opencode-smoke`,
`release-smoke-test`, `smoke-test`, `validate-skill`.
Does **not** touch `validate.yml` — wiring skill-eval.sh into CI is a
separate bead.
Epic: ag-czzf
## Verification
- `bats tests/scripts/skill-eval.bats` → 10/10 pass (ms 0.1.2 on PATH)
- `shellcheck scripts/skill-eval.sh` → clean
- `check-registry-drift`, `validate-skill-schema`, `sync-skill-counts`,
`run-all`, opencode-smoke → all PASS with fixtures present, skill count
= 82 (fixtures excluded)
Closes-scenario: ag-yzoz#skill-eval
Bounded-context: BC2-Validation
Evidence: scripts/skill-eval.sh
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
604 lines
23 KiB
Bash
Executable File
604 lines
23 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# scripts/generate-registry.sh — Generate registry.json from repo source of truth
|
|
#
|
|
# Walks skills/, hooks/, .agents/, evals/, cli/cmd/ao/, and daemon types
|
|
# to produce a single queryable manifest of everything AgentOps manages.
|
|
#
|
|
# Usage:
|
|
# bash scripts/generate-registry.sh # write registry.json
|
|
# bash scripts/generate-registry.sh --check # exit 1 if registry.json is stale
|
|
# bash scripts/generate-registry.sh --stdout # print to stdout, don't write
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
OUTPUT="${REPO_ROOT}/registry.json"
|
|
|
|
MODE="write"
|
|
if [[ "${1:-}" == "--check" ]]; then MODE="check"; fi
|
|
if [[ "${1:-}" == "--stdout" ]]; then MODE="stdout"; fi
|
|
|
|
# ─── Skills ──────────────────────────────────────────────────────────────────
|
|
|
|
build_skills() {
|
|
local skills_dir="${REPO_ROOT}/skills"
|
|
local tiers_file="${skills_dir}/SKILL-TIERS.md"
|
|
local result="[]"
|
|
|
|
for skill_dir in "${skills_dir}"/*/; do
|
|
[[ -d "$skill_dir" ]] || continue
|
|
local name
|
|
name="$(basename "$skill_dir")"
|
|
# Leading-underscore dirs (e.g. skills/_fixtures/) are non-skill scaffolding
|
|
# — planted test fixtures, shared helpers — not real skills. Skip them so
|
|
# they never inflate the registry / skill count.
|
|
[[ "$name" == _* ]] && continue
|
|
|
|
local tier="unknown"
|
|
if [[ -f "$tiers_file" ]]; then
|
|
local valid_tiers="judgment execution knowledge product session contribute cross-vendor library background meta utility"
|
|
local found=""
|
|
# User-facing: | **name** | tier | description |
|
|
while IFS= read -r line; do
|
|
local candidate
|
|
candidate=$(echo "$line" | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/, "", $3); print $3}')
|
|
for vt in $valid_tiers; do
|
|
if [[ "$candidate" == "$vt" ]]; then found="$candidate"; break 2; fi
|
|
done
|
|
done < <(grep -E "^\| \*\*${name}\*\* \|" "$tiers_file" 2>/dev/null || true)
|
|
# Internal skills: | name | tier | category | purpose |
|
|
if [[ -z "$found" ]]; then
|
|
while IFS= read -r line; do
|
|
local candidate
|
|
candidate=$(echo "$line" | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/, "", $3); print $3}')
|
|
for vt in $valid_tiers; do
|
|
if [[ "$candidate" == "$vt" ]]; then found="$candidate"; break 2; fi
|
|
done
|
|
done < <(grep -E "^\| ${name} \|" "$tiers_file" 2>/dev/null || true)
|
|
fi
|
|
[[ -n "$found" ]] && tier="$found"
|
|
fi
|
|
|
|
local has_references=false
|
|
local ref_count=0
|
|
if [[ -d "${skill_dir}references" ]]; then
|
|
has_references=true
|
|
ref_count=$(find "${skill_dir}references" -name '*.md' -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
fi
|
|
|
|
local has_skill_md=false
|
|
[[ -f "${skill_dir}SKILL.md" ]] && has_skill_md=true
|
|
|
|
result=$(echo "$result" | jq --arg name "$name" \
|
|
--arg tier "$tier" \
|
|
--arg path "skills/${name}/" \
|
|
--argjson has_references "$has_references" \
|
|
--argjson ref_count "$ref_count" \
|
|
--argjson has_skill_md "$has_skill_md" \
|
|
'. + [{
|
|
name: $name,
|
|
tier: $tier,
|
|
path: $path,
|
|
has_skill_md: $has_skill_md,
|
|
has_references: $has_references,
|
|
reference_count: $ref_count
|
|
}]')
|
|
done
|
|
|
|
echo "$result" | jq 'sort_by(.tier, .name)'
|
|
}
|
|
|
|
# ─── Hooks ───────────────────────────────────────────────────────────────────
|
|
|
|
build_hooks() {
|
|
local hooks_file="${REPO_ROOT}/hooks/hooks.json"
|
|
[[ -f "$hooks_file" ]] || { echo "[]"; return; }
|
|
|
|
jq '[
|
|
.hooks | to_entries[] |
|
|
.key as $lifecycle |
|
|
.value[] |
|
|
(.matcher // "all") as $matcher |
|
|
.hooks[] |
|
|
{
|
|
name: (.command | split("/") | last | sub("\\.sh$"; "")),
|
|
lifecycle: $lifecycle,
|
|
matcher: $matcher,
|
|
timeout: .timeout,
|
|
path: (.command | sub("\\$\\{CLAUDE_PLUGIN_ROOT\\}/"; "")),
|
|
type: .type
|
|
}
|
|
] | sort_by(.lifecycle, .name)' "$hooks_file"
|
|
}
|
|
|
|
# ─── Knowledge Stores (.agents/ dirs) ────────────────────────────────────────
|
|
|
|
build_knowledge_stores() {
|
|
# soc-k47k: don't early-return on missing .agents/ — `git ls-files` reads the
|
|
# index, which is authoritative regardless of working-tree state. CI clean
|
|
# checkout always populates the tracked files, so the only case where the
|
|
# tracked list is empty is when the repo genuinely has no tracked .agents/
|
|
# entries.
|
|
|
|
# Known purpose map — manually maintained since .agents/ has no manifest
|
|
local -A purposes=(
|
|
[ao]="CLI runtime state and configuration"
|
|
[archive]="Retired/archived knowledge entries"
|
|
[brainstorm]="Brainstorm session outputs"
|
|
[compaction-snapshots]="Pre-compaction context snapshots"
|
|
[compiled]="Compiled wiki output from ao compile"
|
|
[constraints]="Operational constraints and rules"
|
|
[council]="Council validation session outputs"
|
|
[crank]="Crank epic execution state"
|
|
[daemon]="Daemon runtime state (jobs, ledger)"
|
|
[defrag]="Defragmentation outputs"
|
|
[design]="Design decision records"
|
|
[discovery]="Discovery phase outputs"
|
|
[dream-cycle]="Dream cycle runtime state"
|
|
[evals]="Evaluation results and reports"
|
|
[evolution]="Evolution loop state"
|
|
[evolve]="Evolve skill session outputs"
|
|
[findings]="Bug hunt and research findings"
|
|
[handoff]="Session handoff documents"
|
|
[handoffs]="Session handoff archives"
|
|
[harvest]="Harvest consolidation outputs"
|
|
[knowledge]="Promoted knowledge entries"
|
|
[learnings]="Session learnings and insights"
|
|
[ledger]="Append-only event ledger"
|
|
[mine]="Mining extraction outputs"
|
|
[nightly]="Nightly run outputs and digests"
|
|
[overnight]="Overnight dream run outputs"
|
|
[patterns]="Extracted code and workflow patterns"
|
|
[planning-rules]="Reusable planning constraints"
|
|
[plans]="Plan outputs from /plan skill"
|
|
[pool]="Knowledge pool (ingested raw material)"
|
|
[pre-mortem-checks]="Pre-mortem validation outputs"
|
|
[pre-mortems]="Pre-mortem analysis documents"
|
|
[products]="Product definition outputs"
|
|
[releases]="Release notes and changelogs"
|
|
[research]="Research outputs and reports"
|
|
[retros]="Retrospective session outputs"
|
|
[rpi]="RPI run state and registry"
|
|
[sessions]="Session metadata and transcripts"
|
|
[signals]="Quality and context signals"
|
|
[specs]="Technical specifications"
|
|
[staging]="Staging area for promotion"
|
|
[test]="Test generation outputs"
|
|
[tests]="Test results and reports"
|
|
[validation]="Validation phase outputs"
|
|
[vibe-context]="Vibe check context cache"
|
|
[wiki]="Internal wiki entries"
|
|
)
|
|
|
|
# soc-k47k: walk `git ls-files .agents/` instead of filesystem so the registry
|
|
# is deterministic across environments. CI's clean checkout and a session-built
|
|
# local checkout will produce identical output. Only directories containing
|
|
# tracked files appear in knowledge_stores; the file_count is the tracked-file
|
|
# count (which is reproducible — `find` was not).
|
|
local tracked
|
|
tracked=$(cd "$REPO_ROOT" && git ls-files .agents/ 2>/dev/null || true)
|
|
if [[ -z "$tracked" ]]; then
|
|
echo "[]"
|
|
return
|
|
fi
|
|
|
|
# Build (name → file_count) map from tracked .agents/<name>/... entries.
|
|
declare -A counts=()
|
|
while IFS= read -r tracked_path; do
|
|
[[ -z "$tracked_path" ]] && continue
|
|
# Extract the immediate child of .agents/ (e.g., "nightly" from ".agents/nightly/2026-05-07/foo.json").
|
|
local subdir
|
|
subdir="${tracked_path#.agents/}"
|
|
subdir="${subdir%%/*}"
|
|
[[ -n "$subdir" && "$subdir" != "$tracked_path" ]] || continue
|
|
counts[$subdir]=$((${counts[$subdir]:-0} + 1))
|
|
done <<< "$tracked"
|
|
|
|
local result="[]"
|
|
for name in "${!counts[@]}"; do
|
|
local purpose="${purposes[$name]:-"Unknown — needs documentation"}"
|
|
local file_count="${counts[$name]}"
|
|
result=$(echo "$result" | jq --arg name "$name" \
|
|
--arg purpose "$purpose" \
|
|
--arg path ".agents/${name}/" \
|
|
--argjson file_count "$file_count" \
|
|
'. + [{
|
|
name: $name,
|
|
path: $path,
|
|
purpose: $purpose,
|
|
file_count: $file_count
|
|
}]')
|
|
done
|
|
|
|
echo "$result" | jq 'sort_by(.name)'
|
|
}
|
|
|
|
# ─── Daemon Job Types ────────────────────────────────────────────────────────
|
|
|
|
build_job_types() {
|
|
local types_file="${REPO_ROOT}/cli/internal/daemon/types.go"
|
|
[[ -f "$types_file" ]] || { echo "[]"; return; }
|
|
|
|
# Extract JobType constants from Go source
|
|
grep -E 'JobType\w+\s+JobType\s*=\s*"' "$types_file" | while read -r line; do
|
|
echo "$line" | sed -E 's/.*"([^"]+)".*/\1/'
|
|
done | jq -R -s 'split("\n") | map(select(length > 0)) | map({
|
|
job_type: .,
|
|
domain: (split(".")[0]),
|
|
action: (split(".")[1])
|
|
}) | sort_by(.job_type)'
|
|
}
|
|
|
|
# ─── Scheduled Jobs (from example) ──────────────────────────────────────────
|
|
|
|
build_schedules() {
|
|
local schedule_example="${REPO_ROOT}/docs/templates/schedule.yaml.example"
|
|
local legacy_schedule_example="${REPO_ROOT}/.agents/schedule.yaml.example"
|
|
local schedule_live="${REPO_ROOT}/.agents/schedule.yaml"
|
|
|
|
local result='{"example": [], "live": []}'
|
|
|
|
# Parse the tracked canonical example first. Ignore operator-local .agents
|
|
# copies unless they are intentionally tracked, so registry generation is
|
|
# deterministic across CI and developer machines.
|
|
if [[ -f "$schedule_example" ]]; then
|
|
local entries
|
|
entries=$(parse_schedule_yaml "$schedule_example")
|
|
result=$(echo "$result" | jq --argjson entries "$entries" '.example = $entries')
|
|
elif git -C "$REPO_ROOT" ls-files --error-unmatch ".agents/schedule.yaml.example" >/dev/null 2>&1; then
|
|
local entries
|
|
entries=$(parse_schedule_yaml "$legacy_schedule_example")
|
|
result=$(echo "$result" | jq --argjson entries "$entries" '.example = $entries')
|
|
fi
|
|
|
|
# Parse live schedules only when tracked. A live .agents/schedule.yaml is
|
|
# normally operator-local runtime state and must not make registry.json drift.
|
|
if git -C "$REPO_ROOT" ls-files --error-unmatch ".agents/schedule.yaml" >/dev/null 2>&1; then
|
|
local entries
|
|
entries=$(parse_schedule_yaml "$schedule_live")
|
|
result=$(echo "$result" | jq --argjson entries "$entries" '.live = $entries')
|
|
fi
|
|
|
|
echo "$result"
|
|
}
|
|
|
|
parse_schedule_yaml() {
|
|
local file="$1"
|
|
# Extract schedule entries from YAML using awk
|
|
awk '
|
|
/^ - name:/ { if (name != "") print_entry(); name=$NF; cron=""; job_type=""; timeout="" }
|
|
/cron:/ { gsub(/^[^"]*"|"[^"]*$/, "", $0); cron=$0 }
|
|
/job_type:/ { job_type=$NF }
|
|
/timeout:/ { timeout=$NF; gsub(/"/, "", timeout) }
|
|
END { if (name != "") print_entry() }
|
|
function print_entry() {
|
|
printf "{\"name\":\"%s\",\"cron\":\"%s\",\"job_type\":\"%s\",\"timeout\":\"%s\"}\n", name, cron, job_type, timeout
|
|
}
|
|
' "$file" | jq -s '.'
|
|
}
|
|
|
|
# ─── Evals ───────────────────────────────────────────────────────────────────
|
|
|
|
build_evals() {
|
|
local evals_dir="${REPO_ROOT}/evals"
|
|
[[ -d "$evals_dir" ]] || { echo "[]"; return; }
|
|
|
|
# soc-k47k pattern: walk `git ls-files evals/` instead of filesystem `find`
|
|
# so untracked artifacts (e.g. evals/workbench/scorecard-latest.json written
|
|
# by local eval runs) don't drift the registry vs CI's clean checkout.
|
|
local tracked
|
|
tracked=$(cd "$REPO_ROOT" && git ls-files 'evals/*/*.json' 2>/dev/null || true)
|
|
|
|
local result="[]"
|
|
declare -A suites=()
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
local rel suite_name leaf
|
|
rel="${line#evals/}"
|
|
suite_name="${rel%%/*}"
|
|
leaf="${rel#*/}"
|
|
[[ -n "$suite_name" && -n "$leaf" && "$leaf" != */* ]] || continue
|
|
suites[$suite_name]=1
|
|
done <<< "$tracked"
|
|
|
|
for suite_name in "${!suites[@]}"; do
|
|
|
|
local eval_files=()
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
# Match only direct children of the suite (one level deep).
|
|
[[ "$line" == "evals/${suite_name}/"*.json ]] || continue
|
|
[[ "${line#evals/${suite_name}/}" == */*.json ]] && continue
|
|
eval_files+=("$(basename "$line" .json)")
|
|
done <<< "$tracked"
|
|
|
|
local file_count=${#eval_files[@]}
|
|
local files_json
|
|
files_json=$(printf '%s\n' "${eval_files[@]}" | jq -R -s 'split("\n") | map(select(length > 0)) | sort')
|
|
|
|
result=$(echo "$result" | jq --arg suite "$suite_name" \
|
|
--arg path "evals/${suite_name}/" \
|
|
--argjson file_count "$file_count" \
|
|
--argjson files "$files_json" \
|
|
'. + [{
|
|
suite: $suite,
|
|
path: $path,
|
|
eval_count: $file_count,
|
|
evals: $files
|
|
}]')
|
|
done
|
|
|
|
echo "$result" | jq 'sort_by(.suite)'
|
|
}
|
|
|
|
# ─── SKU catalog (schema v2) ─────────────────────────────────────────────────
|
|
#
|
|
# The SKU block (capabilities + capability_summary + cli_top_level_commands) is
|
|
# the schema-v2 capability catalog: a JOIN of SKILL.md frontmatter +
|
|
# skill-dispositions.yaml + SKILL-TIERS.md + the live `ao` cobra tree +
|
|
# validate.yml gate jobs + the packs/agentops reference City. It needs a built
|
|
# `ao` binary, so generate-registry.sh builds one once and reuses it for both the
|
|
# SKU block and the real CLI-command list. AGENTOPS_AO_BIN short-circuits the
|
|
# build (used by CI to avoid double-building).
|
|
|
|
# ensure_ao_bin echoes a path to a usable `ao` binary, or empty string if one
|
|
# cannot be produced (e.g. no cli/ dir, no Go toolchain). Callers degrade
|
|
# gracefully: an empty SKU block is emitted instead of crashing. The
|
|
# validate-sku-catalog-drift gate (which always runs in a real checkout with
|
|
# cli/) is the authoritative enforcer that the catalog is fully populated, so a
|
|
# silently-empty block on main is impossible — the gate would fail on drift.
|
|
ensure_ao_bin() {
|
|
if [[ -n "${AGENTOPS_AO_BIN:-}" && -x "${AGENTOPS_AO_BIN}" ]]; then
|
|
echo "${AGENTOPS_AO_BIN}"
|
|
return
|
|
fi
|
|
if [[ ! -d "${REPO_ROOT}/cli" ]] || ! command -v go >/dev/null 2>&1; then
|
|
echo ""
|
|
return
|
|
fi
|
|
local bin
|
|
bin="$(mktemp -d)/ao"
|
|
if ( cd "${REPO_ROOT}/cli" && go build -o "$bin" ./cmd/ao ) >&2; then
|
|
echo "$bin"
|
|
else
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
# build_sku_block prints the SKU catalog JSON. Returns an empty-but-valid block
|
|
# when the ao binary or the generator helper is unavailable.
|
|
build_sku_block() {
|
|
local ao_bin="$1"
|
|
local empty='{"capabilities":[],"capability_summary":{"total":0,"skills":0,"cli_commands":0,"gates":0,"reference_impls":0},"cli_top_level_commands":[]}'
|
|
if [[ -z "$ao_bin" || ! -f "${REPO_ROOT}/scripts/generate-sku-catalog.sh" ]]; then
|
|
echo "$empty"
|
|
return
|
|
fi
|
|
AGENTOPS_AO_BIN="$ao_bin" bash "${REPO_ROOT}/scripts/generate-sku-catalog.sh" 2>/dev/null || echo "$empty"
|
|
}
|
|
|
|
# ─── CLI Commands ────────────────────────────────────────────────────────────
|
|
#
|
|
# schema v2: the cli_commands surface is now the REAL top-level cobra command
|
|
# nodes (from the live tree via the SKU catalog), not a count of cli/cmd/ao/*.go
|
|
# files. The old file-count produced the misleading "163 commands" number
|
|
# (ag-cbm / oracle Gap #6); the truth is the top-level cobra command count.
|
|
|
|
build_cli_commands() {
|
|
local sku_block="$1"
|
|
echo "$sku_block" | jq '[.capabilities[] | select(.type == "cli-command") | {
|
|
name: .name,
|
|
path: .path,
|
|
purpose: .purpose,
|
|
bounded_context: .bounded_context,
|
|
driven_by_skills: .driven_by_skills
|
|
}] | sort_by(.name)'
|
|
}
|
|
|
|
# ─── Cadence Recommendations ────────────────────────────────────────────────
|
|
|
|
build_cadence_recommendations() {
|
|
# Opinionated baseline: what should run and when
|
|
jq -n '[
|
|
{
|
|
"name": "dream-cycle",
|
|
"cadence": "nightly",
|
|
"cron": "0 3 * * *",
|
|
"job_type": "dream.run",
|
|
"description": "Full knowledge consolidation: harvest → forge → inject → defrag",
|
|
"skills": ["dream", "harvest", "forge", "compile", "inject"]
|
|
},
|
|
{
|
|
"name": "knowledge-forge",
|
|
"cadence": "hourly",
|
|
"cron": "5 * * * *",
|
|
"job_type": "wiki.forge",
|
|
"description": "Mine session transcripts into learnings",
|
|
"skills": ["forge"]
|
|
},
|
|
{
|
|
"name": "eval-suite",
|
|
"cadence": "nightly",
|
|
"cron": "0 4 * * *",
|
|
"job_type": "eval.suite",
|
|
"description": "Run behavioral eval suite against skill changes",
|
|
"skills": ["scenario"]
|
|
},
|
|
{
|
|
"name": "wiki-build",
|
|
"cadence": "weekly",
|
|
"cron": "0 5 * * 0",
|
|
"job_type": "wiki.build",
|
|
"description": "Full .agents/compiled rebuild",
|
|
"skills": ["compile"]
|
|
},
|
|
{
|
|
"name": "flywheel-health",
|
|
"cadence": "weekly",
|
|
"cron": "0 6 * * 1",
|
|
"job_type": null,
|
|
"description": "Knowledge flywheel staleness and pool depth check",
|
|
"skills": ["flywheel"]
|
|
},
|
|
{
|
|
"name": "deps-audit",
|
|
"cadence": "weekly",
|
|
"cron": "0 7 * * 2",
|
|
"job_type": null,
|
|
"description": "Dependency vulnerability and license audit",
|
|
"skills": ["deps"]
|
|
},
|
|
{
|
|
"name": "security-scan",
|
|
"cadence": "weekly",
|
|
"cron": "0 7 * * 3",
|
|
"job_type": null,
|
|
"description": "Repository security scan",
|
|
"skills": ["security"]
|
|
},
|
|
{
|
|
"name": "evolve-loop",
|
|
"cadence": "weekly",
|
|
"cron": "0 2 * * 6",
|
|
"job_type": "rpi.run",
|
|
"description": "Autonomous fitness-scored improvement cycle",
|
|
"skills": ["evolve", "rpi"]
|
|
},
|
|
{
|
|
"name": "pre-push-gate",
|
|
"cadence": "per-push",
|
|
"cron": null,
|
|
"job_type": null,
|
|
"description": "CI validation before push to main",
|
|
"skills": []
|
|
},
|
|
{
|
|
"name": "session-inject",
|
|
"cadence": "per-session",
|
|
"cron": null,
|
|
"job_type": null,
|
|
"description": "Context injection at session start",
|
|
"skills": ["inject", "recover"]
|
|
}
|
|
]'
|
|
}
|
|
|
|
# ─── Assemble ────────────────────────────────────────────────────────────────
|
|
|
|
main() {
|
|
local generated_at
|
|
generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
|
|
local skills hooks knowledge_stores job_types schedules evals cli_commands cadence
|
|
local ao_bin sku_block
|
|
|
|
# Build ao once; the SKU block + the real CLI-command list both need it.
|
|
ao_bin="$(ensure_ao_bin)"
|
|
sku_block="$(build_sku_block "$ao_bin")"
|
|
|
|
# Build each surface
|
|
skills=$(build_skills)
|
|
hooks=$(build_hooks)
|
|
knowledge_stores=$(build_knowledge_stores)
|
|
job_types=$(build_job_types)
|
|
schedules=$(build_schedules)
|
|
evals=$(build_evals)
|
|
cli_commands=$(build_cli_commands "$sku_block")
|
|
cadence=$(build_cadence_recommendations)
|
|
|
|
# Count totals
|
|
local skill_count hook_count store_count job_type_count eval_suite_count cli_count
|
|
skill_count=$(echo "$skills" | jq 'length')
|
|
hook_count=$(echo "$hooks" | jq 'length')
|
|
store_count=$(echo "$knowledge_stores" | jq 'length')
|
|
job_type_count=$(echo "$job_types" | jq 'length')
|
|
eval_suite_count=$(echo "$evals" | jq '[.[].eval_count] | add // 0')
|
|
cli_count=$(echo "$cli_commands" | jq 'length')
|
|
|
|
# SKU catalog block (schema v2)
|
|
local capabilities capability_summary cli_top_level capability_count
|
|
capabilities=$(echo "$sku_block" | jq '.capabilities')
|
|
capability_summary=$(echo "$sku_block" | jq '.capability_summary')
|
|
cli_top_level=$(echo "$sku_block" | jq '.cli_top_level_commands')
|
|
capability_count=$(echo "$capabilities" | jq 'length')
|
|
|
|
# Assemble the registry (schema v2 — superset of v1; existing consumers read
|
|
# summary/surfaces/cadence_recommendations unchanged, new consumers read
|
|
# capabilities/capability_summary).
|
|
local registry
|
|
registry=$(jq -n \
|
|
--arg generated_at "$generated_at" \
|
|
--argjson skill_count "$skill_count" \
|
|
--argjson hook_count "$hook_count" \
|
|
--argjson store_count "$store_count" \
|
|
--argjson job_type_count "$job_type_count" \
|
|
--argjson eval_suite_count "$eval_suite_count" \
|
|
--argjson cli_count "$cli_count" \
|
|
--argjson capability_count "$capability_count" \
|
|
--argjson skills "$skills" \
|
|
--argjson hooks "$hooks" \
|
|
--argjson knowledge_stores "$knowledge_stores" \
|
|
--argjson job_types "$job_types" \
|
|
--argjson schedules "$schedules" \
|
|
--argjson evals "$evals" \
|
|
--argjson cli_commands "$cli_commands" \
|
|
--argjson cadence "$cadence" \
|
|
--argjson capabilities "$capabilities" \
|
|
--argjson capability_summary "$capability_summary" \
|
|
--argjson cli_top_level "$cli_top_level" \
|
|
'{
|
|
schema_version: 2,
|
|
generated_at: $generated_at,
|
|
summary: {
|
|
skills: $skill_count,
|
|
hooks: $hook_count,
|
|
knowledge_stores: $store_count,
|
|
job_types: $job_type_count,
|
|
eval_files: $eval_suite_count,
|
|
cli_commands: $cli_count,
|
|
capabilities: $capability_count
|
|
},
|
|
surfaces: {
|
|
skills: $skills,
|
|
hooks: $hooks,
|
|
knowledge_stores: $knowledge_stores,
|
|
job_types: $job_types,
|
|
schedules: $schedules,
|
|
evals: $evals,
|
|
cli_commands: $cli_commands
|
|
},
|
|
capability_summary: $capability_summary,
|
|
cli_top_level_commands: $cli_top_level,
|
|
capabilities: $capabilities,
|
|
cadence_recommendations: $cadence
|
|
}')
|
|
|
|
case "$MODE" in
|
|
stdout)
|
|
echo "$registry"
|
|
;;
|
|
check)
|
|
if [[ ! -f "$OUTPUT" ]]; then
|
|
echo "FAIL: registry.json does not exist. Run: bash scripts/generate-registry.sh" >&2
|
|
exit 1
|
|
fi
|
|
# Compare ignoring generated_at timestamp
|
|
local current_no_ts new_no_ts
|
|
current_no_ts=$(jq 'del(.generated_at)' "$OUTPUT")
|
|
new_no_ts=$(echo "$registry" | jq 'del(.generated_at)')
|
|
if [[ "$current_no_ts" != "$new_no_ts" ]]; then
|
|
echo "FAIL: registry.json is stale. Run: bash scripts/generate-registry.sh" >&2
|
|
diff <(echo "$current_no_ts" | jq -S .) <(echo "$new_no_ts" | jq -S .) >&2 || true
|
|
exit 1
|
|
fi
|
|
echo "OK: registry.json is up to date"
|
|
;;
|
|
write)
|
|
echo "$registry" > "$OUTPUT"
|
|
echo "Wrote ${OUTPUT} (schema v2: ${skill_count} skills, ${hook_count} hooks, ${store_count} stores, ${job_type_count} job types, ${eval_suite_count} evals, ${cli_count} CLI commands, ${capability_count} SKU capabilities)"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|