review
Front door for review of any kind — code, a plan, a document that needs named reviewer perspectives, or pending changes with a security question. Classifies the subject in front of it and routes to exactly one review skill; does no reviewing itself. Use for 'review this', '/revie
Install
npx skills add https://github.com/ConnorGriffin/skills/tree/main/skills/workflows/review
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install connorgriffin-skills@llmmart
git clone https://github.com/ConnorGriffin/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole connorgriffin/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Review
Front door for review, the way scope is the front door for work that isn't ready to
build. Classify what is in front of it, announce the route, invoke that route's
skill. This skill does none of the reviewing itself — the standards-and-spec pass it
used to run now lives in code-review.
Routes
Routes are data, not prose, layered from two files:
- Shipped:
routes.jsonin this directory. Four rows ship today:code→code-review— changed code against the repo's documented standards and the originating issue.plan→plan-review— a plan, spec, work order, or agent brief, before anything is built.personas→persona-review— a document that needs named reviewer perspectives.security→ the security review that ships with the agent — pending changes carrying a security question.
- Operator:
~/.config/review/routes.json. A row whoseroutematches a shipped row replaces it; any otherrouteextends the table. See Registering a review type below.
Process
- Classify. Read what's in front of you — a diff, a document, the user's own
words — and pick the route whose
fortext matches it. - Announce. Say the route in one line before invoking anything: "routing to
code-review" or equivalent. This is how the caller knows which review ran, not a request for approval. - Invoke. Call that route's skill (or, for
security, the agent's built-in security review), return its result to the caller, and let the caller continue its own completion boundary. Nothing here re-runs the review or second-guesses its output.
Ambiguity
Matching scope: pick a route and announce it. Ask exactly one framing question
only when the subject genuinely admits two routes — a spec with code already written
against it, say. Never ask when the subject is clearly one thing; a clear subject
paired with a manufactured question is stalling, not scoping.
The stop rule
A registered route whose skill is missing stops and reports what is missing and
how to install it. It never runs a nearby review instead. This is the load-bearing
rule in this skill: a missing security route that silently becomes a code review
produces a passing verdict nobody should trust, which is worse than no review at all.
Route resolution decides this mechanically — see Resolving a route below — and its answer is final, not a suggestion to route around.
The not-a-route case reads differently from not-installed, on purpose: one means "review has no idea what that is," the other means "review knows what that is and can't reach it yet." Conflating them either hides a real gap behind "not supported," or manufactures support behind a name nobody registered.
Resolving a route
scripts/resolve_route.py makes the outcomes above machine-decidable instead of
judgment calls:
python3 scripts/resolve_route.py <route>
python3 scripts/resolve_route.py --list
Exit statuses:
- 0 — installed. The route is registered and its skill was found (or, for an
agent-builtinrow, ships with the agent — presence not verified on disk). - 3 — registered but missing. The route is registered, its skill is a
skillkind, and no skill directory was found. The message names the skill and, for a skill this pack ships, the install command; for one it doesn't ship, the row's source file instead. It never names another route. - 4 — not a route. The name matches no row. The message lists the registered route names.
- 2 — malformed config.
~/.config/review/routes.jsonis not valid JSON, or a row is missing a field or carries an unknownkind. Names the file and the problem. This exit is never returned for the three outcomes above.
--list prints every registered row as route<TAB>skill<TAB>kind<TAB>for, exit 0.
Registering a review type
An installation with its own review skill — an infra-plan review, a compliance
review, whatever it runs internally — registers it by adding a row to
~/.config/review/routes.json:
[
{ "route": "code", "skill": "internal-code-review", "kind": "skill",
"for": "changed code, using our internal standards checker" },
{ "route": "infra", "skill": "infra-plan-review", "kind": "skill",
"for": "a pulumi or terraform plan before it's applied" }
]
The first row replaces the shipped code route (same route value); the second adds
a new one. Registering a row does not install the skill it names — the operator
still installs infra-plan-review separately, and until then resolve_route.py infra
reports it registered but missing.
Files (skills)
-
agents
-
openai.yaml 230 B
interface: display_name: "Review" short_description: "Route to one review type: code, plan, personas, or security" default_prompt: "Use $review to route this to the right review skill — code, plan, personas, or security."
-
-
scripts
-
resolve_route.py 5.8 KB
#!/usr/bin/env python3 """Resolve a review route to its skill, or report why it can't be resolved. Route rows are data, layered from two files: the shipped ``routes.json`` beside this script's parent directory, and an operator's ``~/.config/review/routes.json`` (or ``$REVIEW_ROUTES_CONFIG``). An operator row replaces a shipped row with the same ``route`` value; any other ``route`` value extends the table. Three outcomes are machine-decidable for a single route: installed (exit 0), registered but its skill is missing (exit 3), and not a registered route at all (exit 4). A fourth, malformed config (exit 2), is a trust-boundary failure, not a normal outcome, and is never returned for the other three. """ from __future__ import annotations import json import os import sys from collections import OrderedDict from pathlib import Path SHIPPED_ROUTES = Path(__file__).resolve().parent.parent / "routes.json" ROUTE_FIELDS = {"route", "skill", "kind", "for"} VALID_KINDS = {"skill", "agent-builtin"} # Skill names this pack itself ships a directory for. A row naming one of these # gets the pack's own install command when missing; any other skill name gets a # pointer at the row's source file instead, since this pack can't install it. PACK_SHIPPED_SKILLS = {"code-review", "plan-review", "persona-review"} INSTALL_COMMAND = "npx skills add ConnorGriffin/skills --skill {skill}" EXIT_INSTALLED = 0 EXIT_USAGE = 1 EXIT_MALFORMED_CONFIG = 2 EXIT_MISSING = 3 EXIT_NOT_A_ROUTE = 4 class ConfigError(Exception): pass def operator_config_path() -> Path: override = os.environ.get("REVIEW_ROUTES_CONFIG") if override: return Path(override) return Path(os.path.expanduser("~/.config/review/routes.json")) def load_rows(path: Path, *, required: bool) -> list[dict]: if not path.exists(): if required: raise ConfigError(f"{path}: missing") return [] try: text = path.read_text(encoding="utf-8") except OSError as error: raise ConfigError(f"{path}: {error}") try: data = json.loads(text) except json.JSONDecodeError as error: raise ConfigError(f"{path}: invalid JSON: {error.msg}") if not isinstance(data, list): raise ConfigError(f"{path}: must be a JSON list of route rows") rows = [] for index, row in enumerate(data): if not isinstance(row, dict) or set(row) != ROUTE_FIELDS: raise ConfigError( f"{path}: row {index} must have exactly route, skill, kind, for" ) if not all(isinstance(row[field], str) and row[field] for field in ROUTE_FIELDS): raise ConfigError(f"{path}: row {index} fields must be nonempty strings") if row["kind"] not in VALID_KINDS: raise ConfigError(f"{path}: row {index} has an unknown kind {row['kind']!r}") rows.append({**row, "_source": str(path)}) return rows def merged_rows() -> "OrderedDict[str, dict]": merged: "OrderedDict[str, dict]" = OrderedDict() for row in load_rows(SHIPPED_ROUTES, required=True): merged[row["route"]] = row for row in load_rows(operator_config_path(), required=False): merged[row["route"]] = row return merged def skill_roots() -> list[Path]: override = os.environ.get("REVIEW_SKILL_ROOTS") if override is not None: return [Path(part) for part in override.split(os.pathsep) if part] roots: list[Path] = [] project_dir = os.environ.get("CLAUDE_PROJECT_DIR") if project_dir: roots.append(Path(project_dir) / ".claude" / "skills") roots.append(Path(".claude") / "skills") roots.append(Path(os.path.expanduser("~/.claude/skills"))) roots.append(Path(".agents") / "skills") roots.append(Path(os.path.expanduser("~/.agents/skills"))) return roots def find_skill(skill: str) -> Path | None: for root in skill_roots(): candidate = root / skill / "SKILL.md" if candidate.is_file(): return candidate return None def cmd_list(rows: "OrderedDict[str, dict]") -> int: for row in rows.values(): print(f"{row['route']}\t{row['skill']}\t{row['kind']}\t{row['for']}") return EXIT_INSTALLED def cmd_resolve(rows: "OrderedDict[str, dict]", route: str) -> int: row = rows.get(route) if row is None: names = ", ".join(rows.keys()) print( f"{route!r} is not a registered review type here. " f"Registered routes: {names}" ) return EXIT_NOT_A_ROUTE if row["kind"] == "agent-builtin": print( f"{route} -> {row['skill']} ships with the agent itself; its presence " f"was not verified on disk. for: {row['for']}" ) return EXIT_INSTALLED path = find_skill(row["skill"]) if path is not None: print(f"{route} -> {row['skill']} at {path}. for: {row['for']}") return EXIT_INSTALLED if row["skill"] in PACK_SHIPPED_SKILLS: print( f"{route} -> {row['skill']} is registered but not installed. " f"Install it with: {INSTALL_COMMAND.format(skill=row['skill'])}" ) else: print( f"{route} -> {row['skill']} is registered but not installed, and this " f"pack does not ship it. See {row['_source']} for where this row came from." ) return EXIT_MISSING def main(argv: list[str]) -> int: try: rows = merged_rows() except ConfigError as error: print(f"resolve_route: {error}", file=sys.stderr) return EXIT_MALFORMED_CONFIG if argv == ["--list"]: return cmd_list(rows) if len(argv) == 1 and argv[0] != "--list": return cmd_resolve(rows, argv[0]) print("usage: resolve_route.py <route> | --list", file=sys.stderr) return EXIT_USAGE if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))
-
-
routes.json 635 B
[ { "route": "code", "skill": "code-review", "kind": "skill", "for": "changed code against the repo's documented standards and the originating issue" }, { "route": "plan", "skill": "plan-review", "kind": "skill", "for": "a plan, spec, work order, or agent brief, before anything is built" }, { "route": "personas", "skill": "persona-review", "kind": "skill", "for": "a document that needs named reviewer perspectives" }, { "route": "security", "skill": "security-review", "kind": "agent-builtin", "for": "pending changes carrying a security question" } ] -
SKILL.md 5 KB
--- name: review description: Front door for review of any kind — code, a plan, a document that needs named reviewer perspectives, or pending changes with a security question. Classifies the subject in front of it and routes to exactly one review skill; does no reviewing itself. Use for 'review this', '/review', or any request to review a PR, diff, plan, spec, brief, document, or security-sensitive change. --- # Review Front door for review, the way `scope` is the front door for work that isn't ready to build. Classify what is in front of it, announce the route, invoke that route's skill. This skill does none of the reviewing itself — the standards-and-spec pass it used to run now lives in `code-review`. ## Routes Routes are data, not prose, layered from two files: - **Shipped:** [`routes.json`](routes.json) in this directory. Four rows ship today: - `code` → `code-review` — changed code against the repo's documented standards and the originating issue. - `plan` → `plan-review` — a plan, spec, work order, or agent brief, before anything is built. - `personas` → `persona-review` — a document that needs named reviewer perspectives. - `security` → the security review that ships with the agent — pending changes carrying a security question. - **Operator:** `~/.config/review/routes.json`. A row whose `route` matches a shipped row replaces it; any other `route` extends the table. See *Registering a review type* below. ## Process 1. **Classify.** Read what's in front of you — a diff, a document, the user's own words — and pick the route whose `for` text matches it. 2. **Announce.** Say the route in one line before invoking anything: "routing to `code-review`" or equivalent. This is how the caller knows which review ran, not a request for approval. 3. **Invoke.** Call that route's skill (or, for `security`, the agent's built-in security review), return its result to the caller, and let the caller continue its own completion boundary. Nothing here re-runs the review or second-guesses its output. ## Ambiguity Matching `scope`: pick a route and announce it. Ask exactly **one** framing question only when the subject genuinely admits two routes — a spec with code already written against it, say. Never ask when the subject is clearly one thing; a clear subject paired with a manufactured question is stalling, not scoping. ## The stop rule A registered route whose skill is missing **stops** and reports what is missing and how to install it. It never runs a nearby review instead. This is the load-bearing rule in this skill: a missing `security` route that silently becomes a `code` review produces a passing verdict nobody should trust, which is worse than no review at all. Route resolution decides this mechanically — see *Resolving a route* below — and its answer is final, not a suggestion to route around. The **not-a-route** case reads differently from **not-installed**, on purpose: one means "review has no idea what that is," the other means "review knows what that is and can't reach it yet." Conflating them either hides a real gap behind "not supported," or manufactures support behind a name nobody registered. ## Resolving a route `scripts/resolve_route.py` makes the outcomes above machine-decidable instead of judgment calls: ``` python3 scripts/resolve_route.py <route> python3 scripts/resolve_route.py --list ``` Exit statuses: - **0 — installed.** The route is registered and its skill was found (or, for an `agent-builtin` row, ships with the agent — presence not verified on disk). - **3 — registered but missing.** The route is registered, its skill is a `skill` kind, and no skill directory was found. The message names the skill and, for a skill this pack ships, the install command; for one it doesn't ship, the row's source file instead. It never names another route. - **4 — not a route.** The name matches no row. The message lists the registered route names. - **2 — malformed config.** `~/.config/review/routes.json` is not valid JSON, or a row is missing a field or carries an unknown `kind`. Names the file and the problem. This exit is never returned for the three outcomes above. `--list` prints every registered row as `route<TAB>skill<TAB>kind<TAB>for`, exit 0. ## Registering a review type An installation with its own review skill — an infra-plan review, a compliance review, whatever it runs internally — registers it by adding a row to `~/.config/review/routes.json`: ```json [ { "route": "code", "skill": "internal-code-review", "kind": "skill", "for": "changed code, using our internal standards checker" }, { "route": "infra", "skill": "infra-plan-review", "kind": "skill", "for": "a pulumi or terraform plan before it's applied" } ] ``` The first row replaces the shipped `code` route (same `route` value); the second adds a new one. Registering a row does **not** install the skill it names — the operator still installs `infra-plan-review` separately, and until then `resolve_route.py infra` reports it registered but missing.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.