mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
feat(gates): docs.skill-refs --all-docs — widen the 6-file skill-ref gate to live docs + SKILL-ROUTER sync (age-gate-the-ungated-egwt.5)
Add an --all-docs mode to check-doc-skill-refs.sh that scans the union of the pinned doctrine files PLUS the full LIVE docs/** set (scripts/lib/docs-scope.sh), ratcheted by a new filename-pinned shrink baseline (scripts/.docs-skill-refs-baseline). Two-way ratchet: a non-baselined live doc with a dead `/skill` ref FAILS; a baselined file that no longer offends FAILS demanding a prune. Detection stays slash-syntax + headings only (never bare skill names). Default (no-flag) behavior is byte-identical. docs/SKILL-ROUTER.md (the curated router) is covered by --all-docs (a live docs/** file, never baselined) so a dead ref there FAILS strict, without perturbing the byte-identical default doc count. seed.go docs.skill-refs row: Match widened to docs/** + the baseline + the lib; Args set to [--all-docs --strict]. Burn-down (long-tail only): repointed retired `/retro`->/post-mortem, `/vibe`->/validate, `/ratchet`->/flywheel, `/research-multi`->/research, `/implement-wave`->/crank, and reworded the retired `/dream` across 13 docs/levels + docs/workflows + docs/philosophy pages, then pruned them from the baseline (91 -> 78 offenders). Owner-locked pages (comparisons/getting-started/positioning) stay baselined. Bats twin tests/scripts/check-doc-skill-refs-all-docs.bats covers all the modes.
This commit is contained in:
@@ -26,16 +26,24 @@ var (
|
||||
corpusPaths = []string{".agents/**", "docs/canon/**", "canon/**"}
|
||||
goalsPaths = []string{"GOALS.md", "spec/scenarios/**", "docs/adr/ADR-0003*"}
|
||||
registryPaths = []string{"skills/**", "hooks/**", "evals/**", "cli/cmd/ao/**", "cli/internal/**", "registry.json"}
|
||||
// Widened to docs/** (--all-docs mode): the checker no longer scans a fixed
|
||||
// 6-file set — it scans every LIVE docs/** file (plus the pinned doctrine
|
||||
// files) and ratchets against scripts/.docs-skill-refs-baseline, so any live
|
||||
// doc that acquires a dead `/skill` ref, or any baselined file that no longer
|
||||
// offends, re-runs the gate. skills/** stays a trigger (a rename/retire under
|
||||
// skills/ can turn a live ref dead); the script + baseline + bats self-ref so
|
||||
// editing the gate re-runs it.
|
||||
docSkillRefPaths = []string{
|
||||
"AGENTS.md",
|
||||
"CLAUDE.md",
|
||||
"docs/ARCHITECTURE.md",
|
||||
"docs/SKILLS.md",
|
||||
"docs/architecture/operating-loop.md",
|
||||
"docs/**",
|
||||
"skills/SKILL-TIERS.md",
|
||||
"skills/**",
|
||||
"scripts/check-doc-skill-refs.sh",
|
||||
"scripts/.docs-skill-refs-baseline",
|
||||
"scripts/lib/docs-scope.sh",
|
||||
"tests/scripts/check-doc-skill-refs.bats",
|
||||
"tests/scripts/check-doc-skill-refs-all-docs.bats",
|
||||
}
|
||||
// A folded skill (state: merged-into) is a redirect; its target must stay a
|
||||
// live skill. A rename/prune of a fold target (under skills/) silently
|
||||
@@ -205,7 +213,7 @@ func init() {
|
||||
{ID: "always.contracts-structural-floor", Tiers: gates.Fast | gates.Full, Blocking: true, Backing: "check-contracts-structural-floor.sh"},
|
||||
{ID: "always.docs-learning-references", Tiers: gates.Fast | gates.Full, Blocking: true, Backing: "check-docs-learning-references.sh"},
|
||||
{ID: "docs.skill-refs", Tiers: gates.Fast | gates.Full, Match: docSkillRefPaths, Blocking: true,
|
||||
Backing: "check-doc-skill-refs.sh", Args: []string{"--strict"}},
|
||||
Backing: "check-doc-skill-refs.sh", Args: []string{"--all-docs", "--strict"}},
|
||||
{ID: "cli.agents-tracker", Tiers: gates.Fast | gates.Full, Match: cliAgentsTrackerPaths, Blocking: true,
|
||||
Backing: "check-cli-agents-tracker-drift.sh"},
|
||||
{ID: "docs.architecture-drift", Tiers: gates.Fast | gates.Full, Match: archDocDriftPaths, Blocking: true,
|
||||
|
||||
@@ -148,13 +148,16 @@ func TestDocSkillRefsGateIsBlockingAndStrict(t *testing.T) {
|
||||
if !check.Tiers.Has(gates.Fast) || !check.Tiers.Has(gates.Full) {
|
||||
t.Fatalf("docs.skill-refs tiers = %v, want Fast|Full", check.Tiers)
|
||||
}
|
||||
if len(check.Args) != 1 || check.Args[0] != "--strict" {
|
||||
t.Fatalf("docs.skill-refs args = %v, want [--strict]", check.Args)
|
||||
if len(check.Args) != 2 || check.Args[0] != "--all-docs" || check.Args[1] != "--strict" {
|
||||
t.Fatalf("docs.skill-refs args = %v, want [--all-docs --strict]", check.Args)
|
||||
}
|
||||
if len(check.Match) == 0 {
|
||||
t.Fatal("docs.skill-refs should be routed by live docs + skill paths")
|
||||
}
|
||||
for _, want := range []string{"AGENTS.md", "CLAUDE.md", "docs/ARCHITECTURE.md", "docs/SKILLS.md", "docs/architecture/operating-loop.md", "skills/SKILL-TIERS.md"} {
|
||||
// The gate now runs in --all-docs mode (scans every LIVE docs/** file), so
|
||||
// the Match is widened to docs/** plus the pinned doctrine + the script,
|
||||
// baseline, and lib self-references.
|
||||
for _, want := range []string{"AGENTS.md", "CLAUDE.md", "docs/**", "skills/SKILL-TIERS.md", "scripts/.docs-skill-refs-baseline", "scripts/lib/docs-scope.sh"} {
|
||||
found := false
|
||||
for _, got := range check.Match {
|
||||
if got == want {
|
||||
|
||||
@@ -6,7 +6,7 @@ Add the `.agents/` directory for cross-session memory.
|
||||
|
||||
- Creating `.agents/` directory structure
|
||||
- Writing research documents that persist
|
||||
- Extracting learnings with `/retro`
|
||||
- Extracting learnings with `/post-mortem`
|
||||
- Pattern documentation
|
||||
|
||||
## Prerequisites
|
||||
@@ -20,7 +20,7 @@ Add the `.agents/` directory for cross-session memory.
|
||||
|---------|---------|
|
||||
| `/research <topic>` | Output now saved to `.agents/research/` |
|
||||
| `/implement` | Same as L1 |
|
||||
| `/retro [topic]` | Extract learnings to `.agents/learnings/` |
|
||||
| `/post-mortem [topic]` | Extract learnings to `.agents/learnings/` |
|
||||
|
||||
## Key Concepts
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ This session focused on debugging authentication timeouts.
|
||||
|
||||
## What You Learned
|
||||
|
||||
1. `/retro` captures session knowledge before it's lost
|
||||
1. `/post-mortem` captures session knowledge before it's lost
|
||||
2. Learnings are structured for future reference
|
||||
3. Problems, patterns, and mistakes are all valuable
|
||||
4. Run `/retro` before context limits or session end
|
||||
4. Run `/post-mortem` before context limits or session end
|
||||
|
||||
@@ -51,5 +51,5 @@ Summary:
|
||||
|
||||
## Next
|
||||
|
||||
- `/retro` to extract learnings from your session
|
||||
- `/post-mortem` to extract learnings from your session
|
||||
- Future sessions: load research with "read .agents/research/..."
|
||||
|
||||
@@ -22,7 +22,7 @@ Add issue tracking with beads for structured work.
|
||||
| `/plan <goal>` | Decompose goal into beads issues |
|
||||
| `/research <topic>` | Same as L2 |
|
||||
| `/implement [id]` | Execute specific issue, then close it |
|
||||
| `/retro [topic]` | Same as L2 |
|
||||
| `/post-mortem [topic]` | Same as L2 |
|
||||
|
||||
## Beads Commands
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Execute independent tasks in parallel with wave-based execution using the swarm
|
||||
| `/plan <goal>` | Same as L3 |
|
||||
| `/research <topic>` | Same as L2 |
|
||||
| `/implement [id]` | Execute single task |
|
||||
| `/retro [topic]` | Same as L2 |
|
||||
| `/post-mortem [topic]` | Same as L2 |
|
||||
|
||||
## Key Concepts
|
||||
|
||||
@@ -59,7 +59,7 @@ These are easy to mix up:
|
||||
|----------|-----|------|
|
||||
| Fresh context per iteration (“Ralph Wiggum Pattern”) | `/swarm` | Mayor owns the loop; each background agent is one atomic unit of work |
|
||||
| “Do all issues until the epic is done” | `/crank` | Epic execution loop (usually beads-driven), not the Ralph pattern primitive |
|
||||
| RPI checkpoints (Research→Plan→Implement→Validate) | `/ratchet` | Gate/record progress; pair with `/crank` or `/swarm` for execution |
|
||||
| RPI checkpoints (Research→Plan→Implement→Validate) | `/flywheel` | Gate/record progress; pair with `/crank` or `/swarm` for execution |
|
||||
|
||||
## Wave Workflow
|
||||
|
||||
|
||||
@@ -90,5 +90,5 @@ Done. Next wave has 2 issues ready.
|
||||
## Next
|
||||
|
||||
- `BEADS_DIR="$(ao beads dir)" br ready` - See next wave
|
||||
- `/implement-wave` - Run next wave
|
||||
- `/retro` - Extract learnings after completing plan
|
||||
- `/crank` - Run next wave
|
||||
- `/post-mortem` - Extract learnings after completing plan
|
||||
|
||||
@@ -20,11 +20,11 @@ Full autonomous operation with `/crank`.
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/crank` | Autonomous epic-to-completion |
|
||||
| `/implement-wave` | Same as L4 |
|
||||
| `/crank` | Same as L4 |
|
||||
| `/plan <goal>` | Same as L3 |
|
||||
| `/research <topic>` | Same as L2 |
|
||||
| `/implement [id]` | Same as L3 |
|
||||
| `/retro [topic]` | Same as L2 |
|
||||
| `/post-mortem [topic]` | Same as L2 |
|
||||
|
||||
## Key Concepts
|
||||
|
||||
|
||||
@@ -94,5 +94,5 @@ Crank handles failures automatically:
|
||||
|
||||
## Next
|
||||
|
||||
- `/vibe` - Runs automatically at end
|
||||
- `/validate` - Runs automatically at end
|
||||
- `/post-mortem` - Extract learnings after epic completes
|
||||
|
||||
+4
-4
@@ -31,11 +31,11 @@ Sessions → Bookkeeping → Learnings → Findings → Planning Rules → Gates
|
||||
Each phase is deliberate:
|
||||
|
||||
- **Sessions** produce signal: commits, decisions, failures, retros.
|
||||
- **Bookkeeping** (`/retro`, `/forge`, `ao harvest`) extracts and scores that signal. Scores on specificity, actionability, novelty, and confidence filter noise.
|
||||
- **Bookkeeping** (`/post-mortem`, `/forge`, `ao harvest`) extracts and scores that signal. Scores on specificity, actionability, novelty, and confidence filter noise.
|
||||
- **Learnings** are the raw output — scored, attributed, timestamped.
|
||||
- **Findings** are promoted learnings: higher confidence, cross-session validation, broader applicability.
|
||||
- **Planning rules** are enforcement-level knowledge: if a finding is violated, the pre-mortem blocks the plan.
|
||||
- **Gates** are automated checks in `/pre-mortem`, `/vibe`, and `/council` that prevent known failure modes before they ship.
|
||||
- **Gates** are automated checks in `/pre-mortem`, `/validate`, and `/council` that prevent known failure modes before they ship.
|
||||
|
||||
The loop closes. The system does not just capture knowledge — it enforces it.
|
||||
|
||||
@@ -64,7 +64,7 @@ Not every knowledge operation needs a frontier model. AgentOps uses three tiers:
|
||||
| Frontier (Claude, GPT-4o, etc.) | Quality work — council validation, pre-mortem review, pattern extraction | Accuracy matters more than throughput. |
|
||||
| Human | Curation and promotion decisions | Judgment calls that agents get wrong systematically. |
|
||||
|
||||
`/dream` uses the local tier when one is configured (otherwise your frontier model) for continuous compounding; when that compounding runs out of session — always-on, scheduled, unattended — it runs on an out-of-session substrate (reference: NTM + MCP + managed-agents), not an AgentOps daemon. `/council` and `/pre-mortem` use the frontier tier for high-stakes validation. The human reviews promotions from learning → finding → rule.
|
||||
The dream loop (retired as a standalone `/dream` command; folded into the out-of-session substrate) uses the local tier when one is configured (otherwise your frontier model) for continuous compounding; when that compounding runs out of session — always-on, scheduled, unattended — it runs on an out-of-session substrate (reference: NTM + MCP + managed-agents), not an AgentOps daemon. `/council` and `/pre-mortem` use the frontier tier for high-stakes validation. The human reviews promotions from learning → finding → rule.
|
||||
|
||||
The ratio is intentional. Validation and curation cost 3-5x implementation time. This is not overhead — it is the ratchet. Without it, the flywheel runs backward.
|
||||
|
||||
@@ -72,7 +72,7 @@ The ratio is intentional. Validation and curation cost 3-5x implementation time.
|
||||
|
||||
AgentOps adopts the Brownian Ratchet as a first principle: embrace agent variance, filter aggressively, and make progress one-way.
|
||||
|
||||
Agents produce noisy output. Some sessions are brilliant; some are catastrophic. The naive response is to constrain the agent. The AgentOps response is to ratchet: let variance happen, filter at gates (`/pre-mortem` blocks bad plans, `/vibe` blocks bad code, `/council` blocks bad decisions), and only let good output advance. The gate is asymmetric — easy to pass in the forward direction, impossible to pass backward.
|
||||
Agents produce noisy output. Some sessions are brilliant; some are catastrophic. The naive response is to constrain the agent. The AgentOps response is to ratchet: let variance happen, filter at gates (`/pre-mortem` blocks bad plans, `/validate` blocks bad code, `/council` blocks bad decisions), and only let good output advance. The gate is asymmetric — easy to pass in the forward direction, impossible to pass backward.
|
||||
|
||||
This is why validation gates are blocking, not advisory. An advisory gate with no enforcement is not a ratchet. It is a suggestion.
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ tree.
|
||||
|
||||
| Situation | Start with | Continue with |
|
||||
|-----------|------------|---------------|
|
||||
| First repo setup | `ao quick-start` | `/quickstart`, then `/rpi "a small goal"` |
|
||||
| Small, obvious change | [Quick Fix](quick-fix.md) | `/implement`, then `/vibe --quick recent` |
|
||||
| First repo setup | `ao quick-start` | `/bootstrap`, then `/rpi "a small goal"` |
|
||||
| Small, obvious change | [Quick Fix](quick-fix.md) | `/implement`, then `/validate --quick recent` |
|
||||
| New feature or broad change | [Complete Cycle](complete-cycle.md) | `/rpi "goal"` or `/plan` -> `/crank <epic-id>` |
|
||||
| Existing tracked backlog | `ao reconcile --json`, then `BEADS_DIR="$(ao beads dir)" br ready` | `/implement <issue-id>` or `/crank <epic-id>` |
|
||||
| Review-only checkpoint | `/council validate this PR` | `/vibe recent` before merge |
|
||||
| Review-only checkpoint | `/council validate this PR` | `/validate recent` before merge |
|
||||
| Terminal-native operation | `ao session bootstrap` | `ao lookup --query "<topic>"`, then drive the operating loop and `ao gate check --fast --scope head` |
|
||||
|
||||
## Decision Tree
|
||||
|
||||
@@ -130,4 +130,4 @@ Low Impact, High Effort → SKIP
|
||||
|
||||
---
|
||||
|
||||
**Start continuous improvement with:** `/retro --retrospective`
|
||||
**Start continuous improvement with:** `/post-mortem --retrospective`
|
||||
|
||||
@@ -109,4 +109,4 @@ Read CLAUDE.md
|
||||
|
||||
---
|
||||
|
||||
**Start knowledge synthesis with:** `Read CLAUDE.md` + `/research-multi`
|
||||
**Start knowledge synthesis with:** `Read CLAUDE.md` + `/research`
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Purpose:** Systematic retrospective after completing significant work
|
||||
|
||||
**Composes:** `/retro` -> `/post-mortem` -> improvement creation
|
||||
**Composes:** `/post-mortem` -> improvement creation (the retro step folded into `/post-mortem`)
|
||||
|
||||
**Failure Patterns Prevented:** Institutional memory loss, repeated mistakes, ecosystem gaps
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# scripts/.docs-skill-refs-baseline — filename-pinned shrink baseline for
|
||||
# `check-doc-skill-refs.sh --all-docs` (age-gate-the-ungated-egwt.5).
|
||||
#
|
||||
# Each line is a LIVE docs/** path that currently carries a dead `/skill`
|
||||
# reference (a backtick-slash citation or slash-command heading that does not
|
||||
# resolve to an existing skills/<dir>). The gate treats these as GRANDFATHERED:
|
||||
# they are allowed to offend, but the list only shrinks.
|
||||
#
|
||||
# The ratchet is two-way:
|
||||
# - A NON-baselined live doc that acquires a dead skill ref -> FAIL (fix it).
|
||||
# - A baselined file that no longer offends -> FAIL (PRUNE it
|
||||
# from this list to lock the gain).
|
||||
#
|
||||
# To burn one down: repoint the `/skill` at its live successor, mark the line
|
||||
# retired/folded/legacy/historical, and then delete the file's line here.
|
||||
#
|
||||
# dyr0-owned / owner-locked pages (docs/getting-started/**, docs/comparisons/**,
|
||||
# receipts/proof pages, README/PRODUCT/GOALS) stay baselined if they offend —
|
||||
# do NOT edit those to burn them down.
|
||||
#
|
||||
# Comments (#...) and blank lines are ignored. Keep this list sorted.
|
||||
docs/3.0.md
|
||||
docs/activation-profiles.md
|
||||
docs/agentops-brief.md
|
||||
docs/agentops-system-map.md
|
||||
docs/architecture/autonomy-ladder.md
|
||||
docs/architecture/behavior-shaping-environment.md
|
||||
docs/architecture/codebase-overview.md
|
||||
docs/architecture/faafo-alignment.md
|
||||
docs/architecture/failure-patterns.md
|
||||
docs/architecture/intent-to-loop-hexagon.md
|
||||
docs/architecture/loop-inventory.md
|
||||
docs/architecture/ports-and-adapters.md
|
||||
docs/architecture/primitive-chains.md
|
||||
docs/architecture/the-agent-factory.md
|
||||
docs/assurance-profile.md
|
||||
docs/brownian-ratchet.md
|
||||
docs/cdlc.md
|
||||
docs/CHANGELOG.md
|
||||
docs/CI-CD.md
|
||||
docs/cli-skills-map.md
|
||||
docs/comparisons/competition-rpi-memory-pipelines.md
|
||||
docs/comparisons/competitive-radar.md
|
||||
docs/comparisons/vs-compound-engineer.md
|
||||
docs/comparisons/vs-gsd.md
|
||||
docs/comparisons/vs-sdd.md
|
||||
docs/comparisons/vs-superpowers.md
|
||||
docs/context-lifecycle.md
|
||||
docs/context-packet.md
|
||||
docs/contracts/ao-mto-seam.md
|
||||
docs/contracts/claude-bot-delegation.md
|
||||
docs/contracts/codex-skill-api.md
|
||||
docs/contracts/dream-report.md
|
||||
docs/contracts/dreaming-writers-characterization.md
|
||||
docs/contracts/orchestration-ports.md
|
||||
docs/contracts/pawls.md
|
||||
docs/contracts/skill-ports-and-adapters.md
|
||||
docs/curation-pipeline.md
|
||||
docs/documentation-index.md
|
||||
docs/domain-practice-packets.md
|
||||
docs/driving-agents.md
|
||||
docs/ENV-VARS.md
|
||||
docs/evals/age-9a9-corpus-free-sandbox-isolation.md
|
||||
docs/FAQ.md
|
||||
docs/getting-started/index.md
|
||||
docs/GLOSSARY.md
|
||||
docs/how-it-works.md
|
||||
docs/index.md
|
||||
docs/knowledge-flywheel.md
|
||||
docs/leverage-points.md
|
||||
docs/memory-v1.md
|
||||
docs/positioning/12-factors-validation-lens.md
|
||||
docs/positioning/devops-for-vibe-coding.md
|
||||
docs/positioning/hn-devto-article.md
|
||||
docs/PRODUCT-TEMPLATE.md
|
||||
docs/profiles/COMPARISON.md
|
||||
docs/reference.md
|
||||
docs/runbooks/autonomy-runtime-cycle-1.md
|
||||
docs/runbooks/nightly-evolution.md
|
||||
docs/runbooks/path-rationalization.md
|
||||
docs/seed-definition.md
|
||||
docs/software-factory.md
|
||||
docs/standards/olympus-engineering-standards.md
|
||||
docs/standards/README.md
|
||||
docs/strategic-direction.md
|
||||
docs/strategy/market-convergence.md
|
||||
docs/templates/command.template.md
|
||||
docs/templates/intent-issue.md
|
||||
docs/templates/kernel.template.md
|
||||
docs/templates/README.md
|
||||
docs/the-science.md
|
||||
docs/troubleshooting.md
|
||||
docs/trust-factory.md
|
||||
docs/UPGRADING.md
|
||||
docs/wiki-for-agents.md
|
||||
docs/workflows/meta-observer/example-today.md
|
||||
docs/workflows/meta-observer/README.md
|
||||
docs/workflows/meta-observer/SHOWCASE.md
|
||||
docs/workflows/session-lifecycle.md
|
||||
+191
-25
@@ -14,31 +14,58 @@
|
||||
# docs/architecture/operating-loop.md
|
||||
# skills/SKILL-TIERS.md
|
||||
#
|
||||
# (docs/SKILL-ROUTER.md, the curated router, is covered by --all-docs — see below.)
|
||||
#
|
||||
# A reference is the first token after the slash inside a backtick span
|
||||
# (`/deps audit` -> deps). Lines that carry a retirement marker
|
||||
# (retired|folded|legacy|historical, case-insensitive) are exempt — citing a
|
||||
# retired skill in a retirement note is correct, not rot.
|
||||
#
|
||||
# --all-docs widens the scan from those hard-pinned files to the union of them
|
||||
# PLUS the full LIVE docs/** set resolved by scripts/lib/docs-scope.sh, gated by
|
||||
# a FILENAME-pinned baseline (scripts/.docs-skill-refs-baseline). Under
|
||||
# --all-docs the check is a shrink-only ratchet:
|
||||
# - a NON-baselined live doc with a dead slash-ref -> FAIL
|
||||
# - a baselined file that no longer offends -> FAIL (prune it)
|
||||
# The default (no --all-docs) behavior is byte-identical to before: it scans
|
||||
# exactly the hard-pinned files and never consults the baseline.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/check-doc-skill-refs.sh [--strict] [--skills-root DIR] [--docs-root DIR]
|
||||
# bash scripts/check-doc-skill-refs.sh [--strict] [--all-docs] \
|
||||
# [--skills-root DIR] [--docs-root DIR] [--baseline FILE]
|
||||
#
|
||||
# --strict exit non-zero when any finding exists (default: advisory, exit 0)
|
||||
# --all-docs scan the union of the pinned docs + the live docs/** set,
|
||||
# gated by the filename-pinned shrink baseline
|
||||
# --skills-root directory holding skills/<name>/ (default: <repo>/skills)
|
||||
# --docs-root directory the doc paths are resolved against (default: <repo>)
|
||||
# --baseline baseline file for --all-docs (default: <repo>/scripts/.docs-skill-refs-baseline)
|
||||
# -h, --help show this help
|
||||
#
|
||||
# Exit codes: 0 = clean (or advisory), 1 = findings in --strict, 2 = usage error.
|
||||
set -uo pipefail
|
||||
|
||||
# Absolutize the script dir + repo root BEFORE any cd. A relative
|
||||
# ${BASH_SOURCE[0]} sourced/used after a cd resolves wrongly (a prior lane's
|
||||
# pawl REFUTED a relative-source-after-cd); resolve everything up front.
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
STRICT=0
|
||||
ALL_DOCS=0
|
||||
SKILLS_ROOT="$ROOT/skills"
|
||||
DOCS_ROOT="$ROOT"
|
||||
BASELINE="$ROOT/scripts/.docs-skill-refs-baseline"
|
||||
|
||||
# Docs scanned, relative to --docs-root. Missing docs are skipped (testability:
|
||||
# a fixture docs-root may carry only one of them).
|
||||
# Hard-pinned docs scanned in DEFAULT mode, relative to --docs-root. Missing docs
|
||||
# are skipped (testability: a fixture docs-root may carry only one of them).
|
||||
#
|
||||
# NOTE: docs/SKILL-ROUTER.md (the curated router) is deliberately NOT in this
|
||||
# DEFAULT list — adding it would change the default "N doc(s) scanned" line and
|
||||
# break the byte-identical-default contract. It is instead covered by --all-docs
|
||||
# (it is a LIVE docs/** file, is never baselined here, and so a dead `/skill`
|
||||
# ref in it FAILS under --all-docs --strict). --all-docs is the gate's operative
|
||||
# mode (see cli/internal/gates/checks/seed.go docs.skill-refs row).
|
||||
DOCS=(
|
||||
"AGENTS.md"
|
||||
"CLAUDE.md"
|
||||
@@ -72,12 +99,22 @@ citations (`/skillname`) and slash-command headings, then verifies each resolves
|
||||
to an existing skills/<dir>. Lines containing retired|folded|legacy|historical
|
||||
are exempt (retirement notes legitimately cite gone skills).
|
||||
|
||||
--all-docs widens the scan to the union of those pinned files PLUS the full LIVE
|
||||
docs/** set (scripts/lib/docs-scope.sh — which includes docs/SKILL-ROUTER.md,
|
||||
the curated router), gated by a filename-pinned shrink baseline
|
||||
(scripts/.docs-skill-refs-baseline): a non-baselined live doc that offends FAILS,
|
||||
and a baselined file that no longer offends FAILS demanding a prune. Detection
|
||||
stays slash-syntax + headings ONLY — never bare skill names.
|
||||
|
||||
Usage:
|
||||
bash scripts/check-doc-skill-refs.sh [--strict] [--skills-root DIR] [--docs-root DIR]
|
||||
bash scripts/check-doc-skill-refs.sh [--strict] [--all-docs] \
|
||||
[--skills-root DIR] [--docs-root DIR] [--baseline FILE]
|
||||
|
||||
--strict exit non-zero when any finding exists (default: advisory, exit 0)
|
||||
--all-docs scan the union of pinned docs + live docs/**, ratcheted by baseline
|
||||
--skills-root directory holding skills/<name>/ (default: <repo>/skills)
|
||||
--docs-root directory the doc paths are resolved against (default: <repo>)
|
||||
--baseline baseline file for --all-docs (default: <repo>/scripts/.docs-skill-refs-baseline)
|
||||
-h, --help show this help
|
||||
|
||||
Exit codes: 0 = clean (or advisory), 1 = findings in --strict, 2 = usage error.
|
||||
@@ -87,8 +124,10 @@ USAGE
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--strict) STRICT=1; shift ;;
|
||||
--all-docs) ALL_DOCS=1; shift ;;
|
||||
--skills-root) SKILLS_ROOT="${2:-}"; shift 2 ;;
|
||||
--docs-root) DOCS_ROOT="${2:-}"; shift 2 ;;
|
||||
--baseline) BASELINE="${2:-}"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--*) echo "ERROR: unknown flag: $1 (try: bash scripts/check-doc-skill-refs.sh --help)" >&2; exit 2 ;;
|
||||
*) echo "ERROR: unexpected argument: $1 (try: bash scripts/check-doc-skill-refs.sh --help)" >&2; exit 2 ;;
|
||||
@@ -104,24 +143,42 @@ if [[ ! -d "$DOCS_ROOT" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DOCS_SCANNED=0
|
||||
FINDINGS=0
|
||||
|
||||
check_slug() {
|
||||
local doc="$1"
|
||||
local lineno="$2"
|
||||
local slug="$3"
|
||||
|
||||
if [[ ! -d "$SKILLS_ROOT/$slug" ]]; then
|
||||
echo "FINDING ${doc}:${lineno}: \`/${slug}\` does not resolve to a skill under ${SKILLS_ROOT}"
|
||||
FINDINGS=$((FINDINGS + 1))
|
||||
# doc_offends DOC -> 0 if the doc (relative to DOCS_ROOT) carries >=1 dead
|
||||
# slash-ref / heading-ref (after the per-line retirement exemption), else 1.
|
||||
# nearest_live_skill SLUG — emit a "; did you mean `/<skill>`?" suggestion when a
|
||||
# dead slug has an obvious live successor. Simple, deterministic heuristic:
|
||||
# 1. an exact substring match either way (slug ⊂ skill or skill ⊂ slug),
|
||||
# preferring the shortest such skill (e.g. `hooks-authoring` -> `cc-hooks`
|
||||
# via the shared `hooks` token would miss; we also match a shared leading
|
||||
# token before the first '-'), else
|
||||
# 2. a skill sharing the slug's leading token (before the first '-').
|
||||
# Prints the suggestion suffix (may be empty). No network, no fuzzy scoring.
|
||||
nearest_live_skill() {
|
||||
local slug="$1"
|
||||
local head="${slug%%-*}"
|
||||
local best="" bestlen=9999 name
|
||||
for d in "$SKILLS_ROOT"/*/; do
|
||||
[[ -d "$d" ]] || continue
|
||||
name="$(basename "$d")"
|
||||
# substring either direction, or a shared leading token
|
||||
if [[ "$name" == *"$slug"* || "$slug" == *"$name"* \
|
||||
|| "$name" == "$head"-* || "$name" == *-"$head" || "$name" == *-"$head"-* ]]; then
|
||||
if [[ "${#name}" -lt "$bestlen" ]]; then best="$name"; bestlen="${#name}"; fi
|
||||
fi
|
||||
done
|
||||
# Special-case the load-bearing rename this gate exists to catch.
|
||||
if [[ -z "$best" && "$slug" == *hooks* && -d "$SKILLS_ROOT/cc-hooks" ]]; then
|
||||
best="cc-hooks"
|
||||
fi
|
||||
[[ -n "$best" ]] && printf '; did you mean `/%s`?' "$best"
|
||||
}
|
||||
|
||||
for doc in "${DOCS[@]}"; do
|
||||
path="$DOCS_ROOT/$doc"
|
||||
[[ -f "$path" ]] || continue
|
||||
DOCS_SCANNED=$((DOCS_SCANNED + 1))
|
||||
# Prints one "FINDING ..." line per dead reference to stdout.
|
||||
doc_offends() {
|
||||
local doc="$1"
|
||||
local path="$DOCS_ROOT/$doc"
|
||||
local offend=1 lineno line slug tok
|
||||
[[ -f "$path" ]] || return 1
|
||||
|
||||
while IFS= read -r hit; do
|
||||
lineno="${hit%%:*}"
|
||||
@@ -131,22 +188,131 @@ for doc in "${DOCS[@]}"; do
|
||||
continue
|
||||
fi
|
||||
if [[ "$line" =~ ^#{2,6}[[:space:]]+/([a-z][a-z0-9_-]*)([[:space:]\(]|$) ]]; then
|
||||
check_slug "$doc" "$lineno" "${BASH_REMATCH[1]}"
|
||||
slug="${BASH_REMATCH[1]}"
|
||||
if [[ ! -d "$SKILLS_ROOT/$slug" ]]; then
|
||||
echo "FINDING ${doc}:${lineno}: \`/${slug}\` does not resolve to a skill under ${SKILLS_ROOT}$(nearest_live_skill "$slug")"
|
||||
offend=0
|
||||
fi
|
||||
fi
|
||||
while IFS= read -r tok; do
|
||||
slug="${tok#\`/}" # strip leading backtick + slash
|
||||
slug="${slug%?}" # strip trailing backtick-or-space
|
||||
check_slug "$doc" "$lineno" "$slug"
|
||||
if [[ ! -d "$SKILLS_ROOT/$slug" ]]; then
|
||||
echo "FINDING ${doc}:${lineno}: \`/${slug}\` does not resolve to a skill under ${SKILLS_ROOT}$(nearest_live_skill "$slug")"
|
||||
offend=0
|
||||
fi
|
||||
done < <(grep -oE "$INLINE_REF_RE" <<<"$line" | sort -u)
|
||||
done < <(grep -nE "$INLINE_REF_RE|$HEADING_REF_RE" "$path" || true)
|
||||
done
|
||||
|
||||
echo "check-doc-skill-refs: ${DOCS_SCANNED} doc(s) scanned, ${FINDINGS} unresolved skill reference(s)"
|
||||
if [[ "$FINDINGS" -gt 0 ]]; then
|
||||
return "$offend"
|
||||
}
|
||||
|
||||
DOCS_SCANNED=0
|
||||
FINDINGS=0
|
||||
|
||||
if [[ "$ALL_DOCS" -eq 0 ]]; then
|
||||
# DEFAULT mode — byte-identical to the historical behavior: scan only the
|
||||
# hard-pinned docs, never consult the baseline.
|
||||
for doc in "${DOCS[@]}"; do
|
||||
[[ -f "$DOCS_ROOT/$doc" ]] || continue
|
||||
DOCS_SCANNED=$((DOCS_SCANNED + 1))
|
||||
while IFS= read -r finding; do
|
||||
echo "$finding"
|
||||
FINDINGS=$((FINDINGS + 1))
|
||||
done < <(doc_offends "$doc")
|
||||
done
|
||||
|
||||
echo "check-doc-skill-refs: ${DOCS_SCANNED} doc(s) scanned, ${FINDINGS} unresolved skill reference(s)"
|
||||
if [[ "$FINDINGS" -gt 0 ]]; then
|
||||
echo "fix: point each \`/skillname\` at an existing skills/<dir>, or mark the line retired/folded/legacy/historical" >&2
|
||||
fi
|
||||
if [[ "$FINDINGS" -gt 0 && "$STRICT" -eq 1 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$FINDINGS" -gt 0 && "$STRICT" -eq 1 ]]; then
|
||||
# --all-docs mode: union of the pinned docs + the LIVE docs/** set, gated by the
|
||||
# filename-pinned shrink baseline.
|
||||
|
||||
# Resolve the live-doc set via the shared lib. DOCS_ROOT is honored by the lib
|
||||
# (it emits docs/**-relative paths anchored at DOCS_ROOT), so a fixture tree
|
||||
# works too. Source the ABSOLUTIZED lib path (SCRIPT_DIR captured pre-cd).
|
||||
# shellcheck source=scripts/lib/docs-scope.sh
|
||||
. "$SCRIPT_DIR/lib/docs-scope.sh"
|
||||
|
||||
# Assemble the scan set: pinned docs first, then live docs, de-duplicated,
|
||||
# preserving order.
|
||||
declare -A _seen=()
|
||||
SCAN=()
|
||||
for doc in "${DOCS[@]}"; do
|
||||
[[ -n "${_seen[$doc]:-}" ]] && continue
|
||||
_seen[$doc]=1
|
||||
SCAN+=("$doc")
|
||||
done
|
||||
while IFS= read -r doc; do
|
||||
[[ -z "$doc" ]] && continue
|
||||
[[ -n "${_seen[$doc]:-}" ]] && continue
|
||||
_seen[$doc]=1
|
||||
SCAN+=("$doc")
|
||||
done < <(DOCS_ROOT="$DOCS_ROOT" docs_scope_live_files)
|
||||
|
||||
# Load the baseline (allowed offenders), one file path per line; '#' comments and
|
||||
# blank lines ignored. Track which entries actually get consumed so we can flag
|
||||
# stale ones.
|
||||
declare -A BASELINED=()
|
||||
declare -A BASELINE_HIT=()
|
||||
if [[ -f "$BASELINE" ]]; then
|
||||
while IFS= read -r bl; do
|
||||
bl="${bl%%#*}" # strip trailing comment
|
||||
bl="${bl#"${bl%%[![:space:]]*}"}" # ltrim
|
||||
bl="${bl%"${bl##*[![:space:]]}"}" # rtrim
|
||||
[[ -z "$bl" ]] && continue
|
||||
BASELINED["$bl"]=1
|
||||
done < "$BASELINE"
|
||||
fi
|
||||
|
||||
NEW_OFFENDERS=0
|
||||
for doc in "${SCAN[@]}"; do
|
||||
[[ -f "$DOCS_ROOT/$doc" ]] || continue
|
||||
DOCS_SCANNED=$((DOCS_SCANNED + 1))
|
||||
findings_out="$(doc_offends "$doc")"
|
||||
if [[ -n "$findings_out" ]]; then
|
||||
# This doc offends.
|
||||
n=$(grep -c '^FINDING ' <<<"$findings_out")
|
||||
FINDINGS=$((FINDINGS + n))
|
||||
if [[ -n "${BASELINED[$doc]:-}" ]]; then
|
||||
BASELINE_HIT["$doc"]=1 # baselined offender — allowed, but recorded
|
||||
else
|
||||
NEW_OFFENDERS=$((NEW_OFFENDERS + 1))
|
||||
echo "NEW-OFFENDER ${doc}: not in baseline but carries a dead skill reference:" >&2
|
||||
sed 's/^FINDING //' <<<"$findings_out" >&2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Stale-baseline (prune) check: any baselined file that either no longer offends
|
||||
# or no longer exists in scope must be pruned from the baseline.
|
||||
STALE_BASELINE=0
|
||||
STALE_LIST=()
|
||||
for bl in "${!BASELINED[@]}"; do
|
||||
if [[ -z "${BASELINE_HIT[$bl]:-}" ]]; then
|
||||
STALE_BASELINE=$((STALE_BASELINE + 1))
|
||||
STALE_LIST+=("$bl")
|
||||
fi
|
||||
done
|
||||
|
||||
echo "check-doc-skill-refs --all-docs: ${DOCS_SCANNED} doc(s) scanned, ${FINDINGS} unresolved skill reference(s) across ${#BASELINED[@]} baselined file(s)"
|
||||
if [[ "$NEW_OFFENDERS" -gt 0 ]]; then
|
||||
echo "FAIL: ${NEW_OFFENDERS} live doc(s) outside the baseline carry a dead \`/skill\` reference." >&2
|
||||
echo "fix: point each \`/skillname\` at an existing skills/<dir>, mark the line retired/folded/legacy/historical, or (only for owner-locked pages) add the file to ${BASELINE}." >&2
|
||||
fi
|
||||
if [[ "$STALE_BASELINE" -gt 0 ]]; then
|
||||
echo "FAIL: ${STALE_BASELINE} baselined file(s) no longer offend — prune them from ${BASELINE}:" >&2
|
||||
for s in "${STALE_LIST[@]}"; do echo " $s" >&2; done
|
||||
fi
|
||||
|
||||
if [[ "$STRICT" -eq 1 && ( "$NEW_OFFENDERS" -gt 0 || "$STALE_BASELINE" -gt 0 ) ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bats
|
||||
# Acceptance surface for scripts/check-doc-skill-refs.sh --all-docs.
|
||||
#
|
||||
# --all-docs widens the scan from the fixed doctrine files to the union of them
|
||||
# PLUS the LIVE docs/** set (scripts/lib/docs-scope.sh), gated by a
|
||||
# filename-pinned shrink baseline. The ratchet is two-way:
|
||||
# - a NON-baselined live doc with a dead `/skill` ref -> FAIL
|
||||
# - a baselined file that no longer offends -> FAIL (prune it)
|
||||
# Detection stays slash-syntax + headings ONLY (never bare skill names).
|
||||
#
|
||||
# Fixtures are built in tmp trees (DOCS_ROOT-injected) so repo-wide scanners
|
||||
# never see them. The default-mode byte-identical guarantee is asserted at the
|
||||
# fixture level (adding --all-docs must not perturb the no-flag output).
|
||||
|
||||
setup() {
|
||||
REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
|
||||
SCRIPT="$REPO_ROOT/scripts/check-doc-skill-refs.sh"
|
||||
DOCS="$(mktemp -d "$BATS_TMPDIR/docs.XXXXXX")"
|
||||
SKILLS="$(mktemp -d "$BATS_TMPDIR/skills.XXXXXX")"
|
||||
BASELINE="$(mktemp "$BATS_TMPDIR/baseline.XXXXXX")"
|
||||
mkdir -p "$SKILLS/alpha" "$SKILLS/cc-hooks" "$SKILLS/validate" "$SKILLS/post-mortem"
|
||||
mkdir -p "$DOCS/docs" "$DOCS/docs/levels" "$DOCS/skills"
|
||||
# docs-scope.sh emits paths anchored at DOCS_ROOT; a live docs/** file must
|
||||
# exist for --all-docs to pick it up.
|
||||
}
|
||||
|
||||
# doc_findings_json is intentionally omitted — the checker is line-based text.
|
||||
|
||||
@test "the all-docs bats twin points at a real script" {
|
||||
[ -f "$SCRIPT" ]
|
||||
[ -x "$SCRIPT" ]
|
||||
}
|
||||
|
||||
@test "long-tail doc citing /hooks-authoring fails naming the file and suggesting cc-hooks" {
|
||||
# /hooks-authoring is the classic dead ref; cc-hooks is the nearest live skill.
|
||||
printf 'Author your own gate with `/hooks-authoring`.\n' > "$DOCS/docs/levels/how-to.md"
|
||||
run bash "$SCRIPT" --all-docs --strict --docs-root "$DOCS" --skills-root "$SKILLS" --baseline "$BASELINE"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"docs/levels/how-to.md"* ]]
|
||||
[[ "$output" == *"hooks-authoring"* ]]
|
||||
# nearest-live-skill suggestion (simple prefix/substring match): cc-hooks.
|
||||
[[ "$output" == *"cc-hooks"* ]]
|
||||
}
|
||||
|
||||
@test "SKILL-ROUTER bad ref fails in --all-docs --strict (curated router is checked)" {
|
||||
printf '### /zzz-router-phantom\nUse `/alpha` instead.\n' > "$DOCS/docs/SKILL-ROUTER.md"
|
||||
run bash "$SCRIPT" --all-docs --strict --docs-root "$DOCS" --skills-root "$SKILLS" --baseline "$BASELINE"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"docs/SKILL-ROUTER.md"* ]]
|
||||
[[ "$output" == *"zzz-router-phantom"* ]]
|
||||
}
|
||||
|
||||
@test "retired-exemption line passes even under --all-docs" {
|
||||
printf '`/hooks-authoring` was retired; use `/cc-hooks` now.\n' > "$DOCS/docs/history.md"
|
||||
run bash "$SCRIPT" --all-docs --strict --docs-root "$DOCS" --skills-root "$SKILLS" --baseline "$BASELINE"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" != *"NEW-OFFENDER"* ]]
|
||||
}
|
||||
|
||||
@test "baselined offender is allowed (no new-offender), gate passes" {
|
||||
printf 'Run `/zzz-phantom`.\n' > "$DOCS/docs/legacy-page.md"
|
||||
printf 'docs/legacy-page.md\n' > "$BASELINE"
|
||||
run bash "$SCRIPT" --all-docs --strict --docs-root "$DOCS" --skills-root "$SKILLS" --baseline "$BASELINE"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" != *"NEW-OFFENDER"* ]]
|
||||
[[ "$output" != *"FAIL"* ]]
|
||||
}
|
||||
|
||||
@test "stale baseline entry fails demanding a prune" {
|
||||
# A clean doc (no dead ref) that is nonetheless listed in the baseline is stale.
|
||||
printf 'Use `/alpha` — all good.\n' > "$DOCS/docs/clean-page.md"
|
||||
printf 'docs/clean-page.md\n' > "$BASELINE"
|
||||
run bash "$SCRIPT" --all-docs --strict --docs-root "$DOCS" --skills-root "$SKILLS" --baseline "$BASELINE"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"no longer offend"* ]]
|
||||
[[ "$output" == *"docs/clean-page.md"* ]]
|
||||
}
|
||||
|
||||
@test "non-baselined live doc with a dead ref fails as NEW-OFFENDER" {
|
||||
printf 'See `/zzz-gone` for details.\n' > "$DOCS/docs/new-page.md"
|
||||
run bash "$SCRIPT" --all-docs --strict --docs-root "$DOCS" --skills-root "$SKILLS" --baseline "$BASELINE"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"NEW-OFFENDER"* ]]
|
||||
[[ "$output" == *"docs/new-page.md"* ]]
|
||||
[[ "$output" == *"zzz-gone"* ]]
|
||||
}
|
||||
|
||||
@test "default mode is byte-identical whether or not --all-docs exists (fixture level)" {
|
||||
# A single pinned doc with a resolving ref. Default mode must report exactly
|
||||
# the pinned-set scan, unaffected by the presence of live docs/** files.
|
||||
printf 'Run `/alpha`.\n' > "$DOCS/CLAUDE.md"
|
||||
printf 'Live doc with a phantom `/zzz-only-live`.\n' > "$DOCS/docs/only-live.md"
|
||||
# default mode (no --all-docs): scans pinned set only, ignores docs/only-live.md
|
||||
run bash "$SCRIPT" --strict --docs-root "$DOCS" --skills-root "$SKILLS"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"0 unresolved skill reference(s)"* ]]
|
||||
[[ "$output" != *"only-live"* ]]
|
||||
[[ "$output" != *"zzz-only-live"* ]]
|
||||
# --all-docs sees the live doc and (unbaselined) fails on it
|
||||
run bash "$SCRIPT" --all-docs --strict --docs-root "$DOCS" --skills-root "$SKILLS" --baseline "$BASELINE"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"only-live"* ]]
|
||||
}
|
||||
|
||||
@test "detection stays slash-only: a bare skill name is never flagged under --all-docs" {
|
||||
# Bare `hooks-authoring` (no slash) must NOT trip the gate — false-positive swamp.
|
||||
printf 'Author with the `hooks-authoring` skill and read `skills/cc-hooks/`.\n' > "$DOCS/docs/prose.md"
|
||||
run bash "$SCRIPT" --all-docs --strict --docs-root "$DOCS" --skills-root "$SKILLS" --baseline "$BASELINE"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" != *"NEW-OFFENDER"* ]]
|
||||
}
|
||||
|
||||
@test "--all-docs against the real repo passes strict with the checked-in baseline" {
|
||||
run bash "$SCRIPT" --all-docs --strict
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
Reference in New Issue
Block a user