test: add reviewer support verification
This commit is contained in:
Executable
+186
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
export ROOT_DIR
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "Missing required command: python3" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
python3 <<'PY'
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = Path(os.environ["ROOT_DIR"])
|
||||
WORKFLOWS = {
|
||||
"create-plan": {
|
||||
"payload": "/tmp/plan-",
|
||||
"review_output": "/tmp/plan-review-",
|
||||
},
|
||||
"implement-plan": {
|
||||
"payload": "/tmp/milestone-",
|
||||
"review_output": "/tmp/milestone-review-",
|
||||
},
|
||||
"do-task": {
|
||||
"payload": "/tmp/do-task-",
|
||||
"review_output": "/tmp/do-task-",
|
||||
},
|
||||
}
|
||||
VARIANTS = ["claude-code", "codex", "cursor", "opencode", "pi"]
|
||||
PI_FLAGS = [
|
||||
"--no-session",
|
||||
"--no-skills",
|
||||
"--no-prompt-templates",
|
||||
"--no-extensions",
|
||||
"--no-context-files",
|
||||
]
|
||||
FORBIDDEN_PI_TOOLS = {"write", "edit", "bash"}
|
||||
errors: list[str] = []
|
||||
|
||||
|
||||
def compact(text: str) -> str:
|
||||
return re.sub(r"\\\s*\n", " ", text)
|
||||
|
||||
|
||||
def has_reviewer_choice(text: str) -> bool:
|
||||
normalized = compact(text)
|
||||
patterns = [
|
||||
r"Reviewer CLI:\s*`codex`,\s*`claude`,\s*`cursor`,\s*`opencode`,\s*`pi`,\s*or\s*`skip`",
|
||||
r"Reviewer CLI\s*\|[^\n]*\bpi\b[^\n]*\bskip\b",
|
||||
r"REVIEWER_CLI[^\n]*`pi`[^\n]*`skip`",
|
||||
]
|
||||
return any(re.search(pattern, normalized, re.I) for pattern in patterns)
|
||||
|
||||
|
||||
def fenced_code_blocks(text: str) -> list[str]:
|
||||
return [match.group(1) for match in re.finditer(r"```(?:bash|sh|shell)?\s*\n(.*?)```", text, re.S)]
|
||||
|
||||
|
||||
def pi_command_blocks(text: str) -> list[str]:
|
||||
blocks: list[str] = []
|
||||
for block in fenced_code_blocks(text):
|
||||
normalized = compact(block)
|
||||
if re.search(r"\bpi\s+", normalized) and "--no-session" in normalized:
|
||||
blocks.append(normalized)
|
||||
|
||||
# Fallback for inline examples outside fenced code. Walk from the pi command
|
||||
# until the next blank line/heading instead of relying on a fixed window.
|
||||
lines = text.splitlines()
|
||||
for index, line in enumerate(lines):
|
||||
if re.search(r"\bpi\s+.*--no-session", line):
|
||||
collected = []
|
||||
for candidate in lines[index:]:
|
||||
if collected and (not candidate.strip() or candidate.startswith("#")):
|
||||
break
|
||||
collected.append(candidate)
|
||||
block = compact("\n".join(collected))
|
||||
if block not in blocks:
|
||||
blocks.append(block)
|
||||
return blocks
|
||||
|
||||
|
||||
def block_tools(block: str) -> list[str] | None:
|
||||
match = re.search(r"--tools(?:=|\s+)(['\"]?)([^\s'\"]+)\1", block)
|
||||
if not match:
|
||||
return None
|
||||
return [tool.strip() for tool in match.group(2).split(",") if tool.strip()]
|
||||
|
||||
|
||||
def block_has_exact_tools(block: str) -> bool:
|
||||
return block_tools(block) == ["read", "grep", "find", "ls"]
|
||||
|
||||
|
||||
for workflow, spec in WORKFLOWS.items():
|
||||
for variant in VARIANTS:
|
||||
path = ROOT / "skills" / workflow / variant / "SKILL.md"
|
||||
if not path.exists():
|
||||
errors.append(f"missing workflow variant: {path}")
|
||||
continue
|
||||
text = path.read_text()
|
||||
|
||||
if not has_reviewer_choice(text):
|
||||
errors.append(f"{path}: reviewer choices must include pi and skip")
|
||||
|
||||
if "pi/<pi-model-name>" not in text and "pi/claude-opus-4-7" not in text:
|
||||
errors.append(f"{path}: must document pi/<pi-model-name> reviewer shorthand")
|
||||
|
||||
if "pi --list-models [search]" not in text:
|
||||
errors.append(f"{path}: must mention pi --list-models [search] for unavailable models")
|
||||
|
||||
if not re.search(r"reviewer model is configured independently|model is configured independently", text, re.I):
|
||||
errors.append(f"{path}: must state Pi reviewer model is configured independently")
|
||||
|
||||
blocks = pi_command_blocks(text)
|
||||
if not blocks:
|
||||
errors.append(f"{path}: missing isolated pi reviewer command block")
|
||||
continue
|
||||
|
||||
matching_blocks = [block for block in blocks if spec["payload"] in block]
|
||||
if not matching_blocks:
|
||||
errors.append(f"{path}: pi reviewer command must reference {spec['payload']} payload path")
|
||||
matching_blocks = blocks
|
||||
|
||||
if not any(all(flag in block for flag in PI_FLAGS) for block in matching_blocks):
|
||||
errors.append(f"{path}: pi reviewer command missing one or more isolation flags")
|
||||
|
||||
if not any(block_has_exact_tools(block) for block in matching_blocks):
|
||||
errors.append(f"{path}: pi reviewer command must use exactly --tools read,grep,find,ls")
|
||||
|
||||
# The forbidden-tool check is scoped to the --tools allowlist in the Pi
|
||||
# reviewer command. Prose that says these tools are forbidden is allowed.
|
||||
for block in matching_blocks:
|
||||
tools = block_tools(block)
|
||||
if tools is None:
|
||||
continue
|
||||
forbidden = sorted(set(tools) & FORBIDDEN_PI_TOOLS)
|
||||
if forbidden:
|
||||
errors.append(f"{path}: pi reviewer command includes forbidden tools: {', '.join(forbidden)}")
|
||||
|
||||
if variant != "pi" and ".pi/skills/reviewer-runtime/pi" in text:
|
||||
errors.append(f"{path}: non-Pi variant must not use Pi reviewer-runtime helper path as its own runtime")
|
||||
|
||||
for variant in VARIANTS:
|
||||
template = ROOT / "skills" / "do-task" / variant / "templates" / "task-plan.md"
|
||||
if not template.exists():
|
||||
errors.append(f"missing do-task template: {template}")
|
||||
continue
|
||||
text = template.read_text()
|
||||
if "Reviewer CLI | codex \\| claude \\| cursor \\| opencode \\| pi" not in text:
|
||||
errors.append(f"{template}: Reviewer CLI metadata must include pi")
|
||||
|
||||
canonical = ROOT / "docs" / "PI-COMMON-REVIEWER.md"
|
||||
if not canonical.exists():
|
||||
errors.append("docs/PI-COMMON-REVIEWER.md is missing")
|
||||
else:
|
||||
text = canonical.read_text()
|
||||
for flag in PI_FLAGS:
|
||||
if flag not in text:
|
||||
errors.append(f"docs/PI-COMMON-REVIEWER.md: missing {flag}")
|
||||
if "--tools read,grep,find,ls" not in compact(text):
|
||||
errors.append("docs/PI-COMMON-REVIEWER.md: missing exact read-only tool allowlist")
|
||||
if "MUST NOT include `write`, `edit`, or `bash`" not in text:
|
||||
errors.append("docs/PI-COMMON-REVIEWER.md: must forbid write/edit/bash tools")
|
||||
|
||||
for doc in ["CREATE-PLAN.md", "IMPLEMENT-PLAN.md", "DO-TASK.md"]:
|
||||
path = ROOT / "docs" / doc
|
||||
if not path.exists():
|
||||
errors.append(f"docs/{doc} is missing")
|
||||
continue
|
||||
text = path.read_text()
|
||||
if "PI-COMMON-REVIEWER.md" not in text:
|
||||
errors.append(f"docs/{doc}: must link to PI-COMMON-REVIEWER.md")
|
||||
if "pi/claude-opus-4-7" not in text and "pi/<pi-model-name>" not in text:
|
||||
errors.append(f"docs/{doc}: must document Pi reviewer shorthand")
|
||||
|
||||
if errors:
|
||||
print("Reviewer support verification failed:", file=sys.stderr)
|
||||
for error in errors:
|
||||
print(f"- {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("reviewer support verified")
|
||||
PY
|
||||
Reference in New Issue
Block a user