agents/reviewer-architecture.md
# Architecture Reviewer
You are an architecture specialist reviewing a code diff. Your job is finding structural problems that will compound over time: coupling that should not exist, contracts that will break callers, abstractions that leak, and dependencies that point the wrong direction.
You receive a diff. Return a list of findings only. No prose, no praise, no explanation beyond what is in each finding.
## Focus Areas
**Coupling:** New dependencies between modules that should be independent. A component importing from a layer above it. Two features that could evolve independently now sharing state or a direct call.
**Interface contracts:** Changes to public APIs, exported types, or function signatures that break existing callers without a migration path. Optional parameters added in a position that shifts existing positional arguments.
**Abstraction leaks:** Implementation details exposed in a public interface. A type that forces callers to know about internal representation. A function that returns a raw database row where a domain object was expected.
**Dependency direction:** A core module importing from a peripheral one. Business logic importing from infrastructure. A shared utility importing from a feature module.
**Scalability concerns:** A design that works at current load but has a fixed bottleneck (single lock, single table scan, single process) that will fail under 10x load. Flag only if the bottleneck is introduced by this diff, not pre-existing.
## Output Format
Return findings as a plain list. For each finding:
```
[SEVERITY] file:line -- {what the structural problem is}
Impact: {what gets harder or breaks as the system grows, one sentence}
Fix: {specific corrective action}
Class: architecture
Autofix: manual
```
Severity: HIGH (will cause a breakage or forces a rewrite), MEDIUM (will slow future development), LOW (worth noting, not urgent).
## Scope Rules
Flag only issues introduced or made significantly worse by this diff. Do not re-report pre-existing structural problems unless the diff extends or entrenches them.
Suppress LOW confidence findings. If you cannot articulate a concrete consequence, do not file the finding.
Do not flag: security issues, performance micro-optimizations, missing tests, code style. Those belong to other reviewers.
agents/reviewer-security.md
# Security Reviewer
You are a security specialist reviewing a code diff. Your job is finding vulnerabilities that would survive correctness review: injection paths, authentication bypass, credential exposure, and trust boundary violations.
You receive a diff. Return a list of findings only. No prose, no praise, no explanation beyond what is in each finding.
## Focus Areas
**Injection:** SQL, command, path, LDAP, XSS. Trace every user-controlled value from entry point to sink. Flag cases where the value reaches a sink without sanitization or parameterization.
**Authentication bypass:** Routes or functions accessible without verifying identity. JWT or session checks that can be skipped by header manipulation. Permission checks applied after the sensitive operation rather than before.
**Credential exposure:** API keys, tokens, passwords in code, comments, log statements, or error messages. Environment variable names that reveal the existence of a secret without protecting its value.
**Input validation gaps:** Missing length checks, type checks, or format validation on fields that flow to storage or execution. Validation applied at the wrong layer (UI only, not API).
**Trust boundary violations:** Data from one trust zone (user input, external API, LLM output) used without sanitization in a higher-trust zone (database, shell, filesystem). Output from a lower-trust component treated as authoritative.
## Output Format
Return findings as a plain list. For each finding:
```
[SEVERITY] file:line -- {what the vulnerability is}
Mechanism: {how it can be exploited, one sentence}
Fix: {specific corrective action}
Class: security
Autofix: manual
```
Severity: CRITICAL (exploitable now), HIGH (exploitable with effort), MEDIUM (hardening gap), LOW (defense-in-depth).
## Scope Rules
Flag only issues introduced or made worse by this diff. Do not re-report pre-existing issues unless the diff makes them materially easier to exploit.
Suppress findings below HIGH confidence. A finding without a concrete exploit path is noise. State the exploit path or do not file the finding.
Do not flag: code style, missing tests, performance issues, architectural concerns. Those belong to other reviewers.
references/durable-context.md
# Durable Context Preflight
Shared preamble for every skill that reads optional memory or prior-decision context. Each `SKILL.md` links to this file and then adds skill-specific guidance.
## Scope
Read durable context when the user names memory, a prior decision, or a memory path, or when the project exposes an obvious local memory summary (a `MEMORY.md` or a documented memory directory). List titles first and open at most one or two summaries; do not hard-code machine-specific memory roots, and do not read raw transcripts. Treat cross-project entries as transferable patterns, not as facts about this project.
## Current state wins
Current code, diff, screenshots, logs, tests, docs, CI, remote state, and live probes always override memory, including memory the runtime injects on its own. A remembered fact is a lead to re-verify, never evidence. When current state conflicts with a remembered claim, name the conflict and follow current state.
## Memory is not authorization
Memory may explain preferences, but it must never grant or broaden authorization for writes, commits, pushes, publishing, public replies, deletion, or other state changes. Current-turn instructions and current project rules decide authorization. Historical phrases such as `push` or `check` are context to re-evaluate, not reusable action tokens.
## Redaction gate
When turning prior chats, durable memory, or cross-project notes into reusable Waza guidance, promote only workflow rules. Strip raw transcript text, screenshots, local paths, project-specific commands, issue or PR numbers, release tags, commit hashes, private product boundaries, paid or license details, support routing, user names, and one-machine state.
If an example is necessary, use neutral placeholders such as `ExampleCLI`, `ExampleApp`, `<issue>`, `<release>`, or `<command>`. Do not copy a private answer, maintainer reply, screenshot observation, or project-specific incident as a durable rule.
The skill-specific overrides and constraints live in each `SKILL.md`, in the paragraph that follows its link to this file.
references/mode-audit.md
# Project Audit Mode (project-wide scorecard)
Loaded from `check` Mode Picker when the request is a project-wide quality scorecard. Distinct from default review (diff-scoped) and triage (issue batching).
Single-pass project-wide quality assessment.
**Flow**
1. Run `python3 <skill-base-dir>/scripts/audit_signals.py --root <project>` from the target repo, with `<skill-base-dir>` replaced by this skill's base directory. The script emits labelled blocks (`=== FILE SIZE HOTSPOTS ===` ... `=== DENYLIST IN BUILD ===`) each ending with `status: PASS|WARN|FAIL|N/A`.
2. Skim the largest source files surfaced by `FILE SIZE HOTSPOTS` (typically 3-5; stop sooner if the architecture is already clear).
3. Read `CLAUDE.md` / `AGENTS.md` / `README.md` to learn the project's own stated conventions before judging it against generic ones. The repo's agent guidance itself is part of the audited surface: verify its commands and paths still exist, and report stale, conflicting, or deletable rules as findings.
4. Apply the four-axis rubric below. Each axis is independently scored 0-10. Overall = arithmetic mean.
5. Report every finding that moves an axis score, each with file:line citation when possible, severity (CRIT/STRUCT/INCR), and a one-line fix. Zero findings on an axis is a valid result; do not pad to a quota.
6. Output to **terminal only**. Do not create files in the target repo. If the user follows up with "save it", offer `./docs/<project>-audit.md` then; default is ephemeral.
**Rubric**
| Axis | What it covers |
|---|---|
| Architecture | Module boundaries, coupling, abstraction layers vs flat duplication, single source of truth |
| Code Quality | File size discipline, dedup, readability, comments on non-obvious behavior |
| Engineering | Tests, CI gates, version coordination, install URL pinning, packaging posture |
| Perf and Risk | Hazards, scope creep, distribution risk, privacy posture, third-party blast radius |
**Scoring anchors**
- 9-10: exceptional discipline, polish-only items
- 7-8.5: solid with clear targeted improvements
- 5-7: working but with structural debt
- below 5: significant rework recommended
A WARN that the project has explicitly justified (in its own docs or a comment) is not a finding; cite the justification and skip. Do not mechanically convert WARN to CRIT. A block with `status: N/A` means the surface does not exist (e.g. no packaging script); treat as silence, not as a positive signal.
**Output template (terminal)**
```
Project: <name>
Overall: X.X / 10
Architecture: X / 10 -- one-line summary
Code Quality: X / 10 -- one-line summary
Engineering: X / 10 -- one-line summary
Perf & Risk: X / 10 -- one-line summary
Findings
[CRIT] <file:line> -- <issue>
why: <reason grounded in signal or read>
fix: <concrete action>
[STRUCT] ...
[INCR] ...
Top 3 highest-leverage moves
1. ...
2. ...
3. ...
```
Stop after the report unless the user asks for follow-up implementation. Audit mode does not modify files in the target repo.
references/mode-ship.md
# Release Worthiness And Ship Follow-through
Loaded from `check` Mode Picker for "is this worth a release" and for commit / push / publish / tag / issue-closure follow-through. Ship extends review; it does not replace it.
## Release Worthiness Analysis
Activate when the user asks "深入分析 X 是不是值得发新版本", "is this worth a new release", "值不值得发版", or similar.
Classify every commit since the last published tag (the tag is the baseline, not a local VERSION file), then output:
- **Commit summary**: N feat, N fix, N chore since last release
- **Verdict**: release / skip (one line)
- **Recommended version bump**: patch (fixes only), minor (feat present), major (breaking change)
- **Key risk**: one sentence on the biggest risk in this batch
If the verdict is "release", offer to transition into Ship mode.
## Ship / Release Follow-through
Activate when the user asks to commit, tag, release, publish, push, reply on an issue/PR, or close an issue after a change is ready.
Treat an explicitly authorized chain such as review, fix, verify, commit, push, and public follow-through as one delivery ledger. Do not return control between its internal stages while safe authorized work remains. A local commit is not completion when push was included, and a no-op push is not completion when intended local changes remain uncommitted. Do not create an empty commit when the intended scope is already clean; prove the clean/up-to-date state instead.
This mode extends review; it does not skip review. Before any public or irreversible action:
1. Extract release rules from public project context: README, manifests, CI workflows, release notes, package scripts, changelogs, and explicit user instructions in the current thread.
2. Fill the Release Gate 2.0 matrix from `references/project-context.md`. Seed the deterministic rows with `python3 <skill-base-dir>/scripts/release_gate.py --root <project>` (worktree state, remote sync, tag baseline, version field sync, changelog mention) and paste its status lines as evidence; the remaining rows (generated artifacts, package/archive contents, release assets, registry/appcast/CI, public issue/PR state) stay judgment calls with their own evidence.
3. Verify generated or bundled outputs, version fields, release notes, package contents, and required artifacts are in sync. Prefer dry-run commands when the ecosystem provides them. When drafting release notes or update-feed copy, follow `/write` and its release-note mode; for Chinese copy, load its zh release-notes rules before the first draft, not after a tone complaint -- translation-flavored Chinese notes are a defect, not a polish item.
Before drafting release notes, read the repo's previous published release (`gh release view` the latest tag) and preserve its title convention, per-item length, and language layout. Treat its item count as history, not a target: use the smallest complete set of distinct user outcomes in the candidate artifact.
Generated deliverables include tracked archives, ignored dist files, appcasts, site/download copy, registry packages, checksums, and release assets. If project docs require them, regenerate, inspect, and stage or upload them explicitly even when they are ignored by git; do not infer readiness from source-only tests. For remote assets, prefer downloading or reading back the published artifact and comparing entries, checksums, or manifest contents; release page text, file size, or workflow success alone is not artifact proof.
If the project has preview, beta, nightly, stable, or App Store lanes, name the lane explicitly. Do not use a preview or beta artifact to claim stable release readiness, and do not touch stable appcast, registry, or download surfaces when the requested lane is preview-only unless project docs require it.
Classify each change by deployment surface before concluding what is live: code that ships inside a packaged artifact (app binary, bundled CLI, release archive) reaches users only at the next release, while sites, serverless functions, CDN config, and infrastructure deploy automatically when the default branch updates. One batch of changes can be unreleased on the first surface and already in production on the second; state each surface separately instead of letting "not released yet" cover auto-deployed code.
4. Commit only intended files under the Worktree Safety Preflight in `SKILL.md` (HEAD and status re-read before commit and again before push), and serialize git operations so index locks or overlapping adds do not corrupt the workflow.
5. Push, publish, tag, or create a release only when the user has explicitly approved that action. Before the first push in a project, check `git remote -v`, the current branch, and the authenticated identity; when the user names an exact account, verify the authenticated service identity immediately before the first remote write and stop on mismatch; never substitute another account silently. If auth, OTP, CI, registry, or network state blocks the operation, pause and report the exact blocker.
6. For issue/PR follow-through, confirm the item identity with the host's read command before posting. On GitHub, use `gh issue view` or `gh pr view`; on other hosts, use the CLI/API named by project docs or the current request. Use `references/public-reply.md` for the maintainer reply template (mention, single thanks, facts, explicit next release or verification step) and its closure criteria.
7. For GitHub release reaction follow-through, only do it when project context or the current thread asks for it. After the release exists and required assets are verified, resolve the release id from the tag, POST every positive release reaction to `repos/<owner>/<repo>/releases/<id>/reactions` with `gh api` or the available GitHub tool, and re-read reactions to confirm. Positive release reactions are `+1`, `laugh`, `heart`, `hooray`, `rocket`, and `eyes`.
8. After network or API failures, re-read the end state instead of assuming success or failure.
Before handoff, reconcile every authorized item as `done`, `not applicable`, or `blocked`, then re-read the local `HEAD`, target remote ref/SHA, worktree status, CI or published artifact lane, and any public thread changed in this run. Never collapse source, CI, package, deployed channel, and public-thread state into one "done" claim.
### Reworked Or Cancelled Release Gate
Activate this gate when a release candidate was cancelled, a preview or beta had repeated bug-fix churn, or the user asks whether a delayed release is finally safe. Load `references/release-surfaces.md` (Reworked Or Cancelled Release Gate): review from the last public stable tag through `HEAD` by shipped risk surface, and output two decisions, whether the preview keeps taking user testing and whether stable release prep can start.
Lead the verdict with an explicit go / no-go (ship, or the named blockers), then the concrete shipped state: commit hash, tag, release URL, registry/version result, pushed branch, release asset state, release reaction state, issue/PR state, and any remaining blockers. Omit fields that do not apply.
references/mode-triage.md
# Triage Mode (issue / PR queues)
Loaded from `check` Mode Picker when the request is issue/PR triage. Shared review surface (Scope, Hard Rules, Hard Stops, Autofix, Specialist Review, Verification, Sign-off) still applies from `SKILL.md`.
Activate when the user mentions: issue, PR, "review all", triage, "batch", or "批量处理". Skip the diff flow and run this instead.
**Action-first rule:** Items with a clear disposition (already fixed, duplicate, already released) get acted on immediately without analysis paragraphs. When analyzing screenshots or images, state what you see and the suggested action in one message. Only ask the user when the disposition is genuinely ambiguous.
**Bundled request classification:** When one issue, PR, or support thread contains several asks, split them before acting: core bug, existing affordance, cosmetic preference, and out-of-scope request. Fix or close only the validated core bug; answer existing affordances with the current path; defer or decline cosmetic and out-of-scope asks instead of treating the whole report as a to-do list.
**Status answer order:** For "都解决了吗", "is this fixed", "is this ready", or similar status checks, answer in this order: code or commit state, branch or CI state, release artifact or registry state, then public issue or PR state. Do not collapse fixed-on-main, available in pre-release, next stable release, and already shipped.
**Flow:** Identify the project's issue/PR host from public context and use that platform's CLI/API; if none exists, stop and report the missing integration instead of pretending GitHub commands apply. Freeze the initial queue as exact IDs plus counts before acting. For each open item, check current state against the project's release boundary: latest public release, main branch, preview/nightly/beta channel, registry/appcast, and target issue/PR status. Already in a public release or documented pre-release channel: close with that exact upgrade path. Fixed on `main` but unreleased: reply "已修复,等下一个版本 release" and close only when project convention or the current user request allows fixed-on-main closure, otherwise leave it open with the next-release note. No fix yet: analyze and act. Fix now if possible (`fix: closes #N` commit); for valid-but-unreleased items acknowledge and leave open; for invalid items give a one-two sentence reason and close.
Before final conclusions in a live queue, refresh the issue/PR list once more and re-read any item that changed during the run. Reconcile every initial ID as done, deferred, or blocked, then report the final IDs and counts; list newly arrived items separately. If evidence is incomplete, hold the item instead of closing it on a guess.
**PR handling:** Read the check state as a count, not a color: zero check runs with a non-green mergeability status means nothing was verified, not that something failed, and a contributor's "CI is green" may refer to a different base than their patch. Reproduce the verification locally before merging, and say which layer the green came from.
Every PR gets one of exactly three dispositions, named in the analysis output before authorization is requested: merge as written, push fixes onto the contributor's branch then merge, or close as not planned. "Not mergeable as written" without naming the fix-on-their-branch option is an incomplete triage, and it is the most common one to skip because the patch's flaws are the most visible thing about it. If the PR direction is accepted but the patch needs changes, prefer pushing the maintainer's fixes to the contributor's PR branch and then merging the PR. Check `maintainerCanModify` first, then confirm the push remote, target branch, and current HEAD immediately before pushing so you do not overwrite contributor work or push maintainer fixes to the wrong repository. If branch edits are not allowed, ask the contributor to enable maintainer edits or push the needed revision; only fall back to a separate maintainer commit when timing or release safety requires it, and say so in the PR. Close without merging only when the direction is rejected, unsafe, no longer needed, or explicitly not part of the project's scope. Do not silently absorb an accepted PR into `main` and close it.
**Public reply shape:** load `references/public-reply.md` for the full template (mention, single thanks, factual paragraphs, next-release step, editing rules, closure criteria). Ship Mode uses the same template; the file is the single source.
**Sign-off line (append to standard sign-off):**
```
triage: N reviewed, N closed, N deferred
```
references/persona-catalog.md
# Specialist Reviewer Activation Catalog
The orchestrator reads the full diff and uses judgment (not keyword matching) to decide which specialists to activate. This catalog defines the signals to reason about.
## Always-On (no condition required)
The base /check skill runs as always-on. Specialist reviewers are additive.
## Conditional Specialists
### Security Reviewer
**Agent file:** `agents/reviewer-security.md`
**Activate at:** Standard or Deep depth
Activate when the diff changes code an attacker could reach or influence: trust-boundary input, auth or crypto, credentials, or query/shell/path construction.
**Do not activate** for: pure UI changes, config file updates, test-only changes, documentation.
### Architecture Reviewer
**Agent file:** `agents/reviewer-architecture.md`
**Activate at:** Standard or Deep depth
Activate when the diff changes how modules relate: boundaries, public APIs or signatures, cross-module dependencies, or a major dependency, rather than logic inside one module.
**Do not activate** for: single-file bug fixes, test additions, style changes, documentation updates.
## Adversarial Pass (Deep only)
No dedicated agent file. When the environment has an agent facility, the orchestrator runs the four angles as parallel agents, each blind to the others' findings; otherwise it runs them as an extra reasoning pass after all findings are collected.
**Activate at:** Deep depth only; the Deep criteria live in SKILL.md's Scope table.
Adversarial pass asks: "If I wanted to break this system through this specific diff, what would I do?"
Four attack angles:
1. **Assumption violation** -- What does this code assume is always true? (format, ordering, range) What happens when it is not?
2. **Composition failures** -- What breaks when this new code interacts with the existing system under concurrent load or partial failure?
3. **Cascade construction** -- What sequence of valid operations leads to an invalid state?
4. **Abuse cases** -- What happens on the 1000th request, during a deployment, with two users editing the same resource simultaneously?
Report adversarial findings with confidence score; the suppression threshold lives in SKILL.md's Adversarial Pass section.
references/project-context.md
# Project Review Context Template
Use this template to compress repository context before running Waza `/check`. The context must come from public project files, the diff, CI configuration, or explicit user instructions. Do not depend on private machine paths or unpublished project instructions.
## What Belongs In Waza `/check`
- Diff depth classification.
- Scope drift detection.
- Hard stops such as destructive automation, missing release artifacts, generated artifact drift, version skew, unknown identifiers, injection risks, credential leakage, and dependency surprises.
- Release Gate 2.0 matrix for release readiness.
- Safety sink review for destructive operations, command construction, path boundaries, signing/appcast, sandbox/approval, and auth prompts.
- Security and architecture specialist routing.
- Autofix policy.
- Sign-off format.
- Verification expectations.
## What Belongs In Project Context
- Verification commands discovered from public docs, manifests, Makefiles, scripts, or CI workflows.
- Protected files and directories.
- Generated or bundled artifacts that must stay in sync with source changes.
- Packaging source of truth: whether archives are built from `git ls-files`, explicit allowlists, generated manifests, or source directories.
- Delivery surfaces: whether generated outputs are tracked, ignored, external release assets, registry uploads, appcasts, installer metadata, checksums, or site/download copy; how they are regenerated, inspected, staged, or uploaded.
- Distribution lanes: preview, beta, nightly, stable, App Store, or registry channels, and which generated artifacts belong to each lane.
- CLI command surfaces: entrypoints, subcommands, flags, help/version behavior, exit codes, stdout/stderr contract, TTY and non-interactive paths, config/env precedence, and installed-runtime checks.
- Runtime dependencies introduced by the diff: Python packages, CLIs, network services, package managers, or platform tools that are not already declared in CI/docs.
- Skill, plugin, marketplace, or package install surfaces: installer default ref, marketplace source path, generated mirror, package allowlist, archive root, executable bits, and the installed-runtime smoke command.
- Domain-specific safety rules.
- Release artifacts that must exist.
- GitHub release reactions or other public release follow-through expected by the project.
- Release-asset verification method: download, archive entry comparison, checksum manifest, package metadata readback, appcast readback, or registry query.
- Public issue or PR reply conventions.
- Known CI or test flakes documented by the project and how to distinguish them from real failures.
- Release, publish, push, or issue-closure prerequisites documented by the project.
## What Does Not Belong In Public Context
- Credential paths, private key filenames, passwords, tokens, or secret values.
- Maintainer-only machine paths.
- One-off personal preferences that do not affect project behavior.
- One-off review reports, scorecards, or diagnostic snapshots copied as guidance instead of distilled into stable project rules.
- Raw memory, chat excerpts, screenshots, private support details, local paths, project-specific commands, issue/PR numbers, release tags, or commit hashes from another project.
- Full copies of Waza `/check` sections.
## Recommended Context Shape
```markdown
## Project Commands
- Format: `<command>`
- Fast check: `<command>`
- Full verification: `<command>`
## CLI Command Surface
- Entrypoints: `<command or bin>`.
- Command contract: help/version, subcommands, flags, exit codes, stdout/stderr, JSON/schema output.
- Runtime shape: TTY vs non-interactive behavior, env/config precedence, completion/manpage or shell integration.
- Install/run proof: built package, temp prefix, PATH shim, shebang/executable bit, or package-manager path checked with `<command>`.
- Mutating commands: dry-run/confirmation, operation log, rollback/retry behavior, signal/partial-failure handling.
## Skill Or Plugin Install Surface
- User install path: `<package manager / release archive / marketplace entry / plugin id / installer script>`.
- Source path and generated mirror: `<source dir>` -> `<installed dir>`.
- Package/archive inclusion: new scripts, references, templates, rules, manifests, and executable bits checked with `<command>`.
- Isolated install smoke: fresh temp home/config/cache plus `<install command>` and `<list or invoke command>`.
- Noise filtering: cache files, local logs, screenshots, and temp outputs excluded or intentionally shipped.
## Project Hard Stops
- Do not modify `<protected path>` unless explicitly requested.
- If `<artifact>` is generated from `<source>`, verify it was regenerated.
- If `<artifact>` is ignored by git but required for release, verify the regeneration and force-stage, upload, or registry publish path named by the project.
- If `<package script>` builds from tracked files or an allowlist, verify newly introduced helpers, references, templates, and scripts are included in `<archive>`.
- If an installer fetches remote content, verify the default ref is pinned to a release tag or checksum-protected; floating `main` must be an explicit override.
- If a helper introduces a non-stdlib package or external CLI, verify CI installs it or the helper fails with a clear setup path.
- If `<artifact>` is listed in release notes, verify it exists before sign-off.
## Project-Specific Risks
- `<risk>`: `<how to inspect it>`
## Public Replies
See `public-reply.md` for the full reply template (language match, `@user` + thanks, factual paragraphs, ship-state line, closure criteria). It is the single source; do not restate the rules here.
## Release Follow-through
- Version fields to check: `<manifest>`, `<app config>`, `<lockfile>`.
- Generated artifacts to check: `<artifact>` from `<source>`.
- Distribution lane: `<preview/beta/nightly/stable/etc.>` and which public surfaces it is allowed to touch.
- Dry-run command before publishing: `<command>`.
- Remote asset proof: `<download/readback command>` that checks content, manifest, digest, appcast, or registry state.
- GitHub release reactions to add after asset verification: `<+1/laugh/heart/hooray/rocket/eyes or none>`.
- Public state to re-read after publishing or closing: `<registry/release/issue URL or command>`.
```
Keep this context brief. It should guide the review, not replace the review method.
## Release Gate 2.0 Matrix
Fill this before claiming a change is release-ready. Use "n/a" only when the project clearly has no such surface.
| Surface | Evidence |
|---|---|
| Review base | Base branch, latest tag, and commit range reviewed |
| Worktree state | Dirty, staged, and untracked files accounted for |
| Remote state | `origin/main` or release branch sync checked |
| Version fields | Manifest, app config, changelog, appcast, and lockfile versions aligned |
| Distribution lane | Preview, beta, nightly, stable, registry, or app-store lane named, with unrelated lanes left untouched |
| Runtime dependencies | Newly introduced Python packages, CLIs, package managers, and network tools declared and available in CI |
| Generated artifacts | Tracked archives, ignored dist outputs, bundled/minified files, appcasts, installer metadata, checksums, and site/download copy regenerated or proven not needed |
| Package/archive contents | Built package inspected for required files, newly introduced helpers/references, and missing extras |
| Installed runtime | Package, skill, plugin, CLI, or marketplace install exercised from a clean environment when the diff changes installable surfaces |
| Release assets | GitHub release, appcast, download archive, checksum, or installer assets downloaded or read back and verified beyond page text or file size |
| Registry/appcast | npm/crates/Homebrew/appcast/App Store or equivalent state re-read after publish |
| CI status | Latest required checks passed or blocker named |
| Issue/PR state | Target issue or PR re-read before commenting, closing, merging, or saying shipped |
## Safety Sink Review
Any diff that touches one of these sinks needs explicit validation and rollback thinking:
- Deleting, moving, or overwriting user files, caches, history, preferences, or generated outputs.
- Building shell, AppleScript, SQL, URL, or filesystem paths from user input.
- Changing cwd handling, symlink resolution, path traversal guards, sandbox permissions, approval checks, or auth prompts.
- Changing signing, notarization, appcast, update, license, payment, or release asset generation.
Review the smallest entry point that reaches the sink, then the downstream call. If validation is missing or rollback is unclear, treat it as a hard stop.
references/public-reply.md
# Public Reply Shape (maintainer, issue or PR)
Reusable by both Triage Mode and Ship / Release Follow-through. Default to this shape unless `AGENTS.md` or `CLAUDE.md` in the target repo contradicts it.
1. Resolve `@<login>` from `gh issue view` / `gh pr view --json author` before posting.
2. **Language:** Match the **opener's** language when it is Chinese or English. If the opener used Japanese or Korean, use English for the maintainer reply unless project docs override.
3. Open with `@<login>` and **at most one** short thanks (`感谢反馈`, `thank you for the report`, etc.). Do **not** add closing thanks stacks (`再次感谢`, `Thanks again`, long courtesy endings).
4. Default to one paragraph and one or two sentences: `@reporter` + one thanks, then the factual state/boundary and the reporter's next step. Include root cause only when it changes what the reporter should do. Internal files, CI approval, and maintainer process stay out.
5. Name the exact boundary: already released, fixed on `main` but unreleased, available in nightly/beta/preview, next release, not planned, duplicate, or still needs evidence. Every sentence must be true at the moment of posting: do not write "landed on main" while the change sits uncommitted, do not write "shipped", "released", or "verified" unless that state was checked in the current turn, and do not imply a verification step (built a branch, ran an artifact) that did not happen.
6. Always give a **next step tied to releases or verification**: next App Store or GitHub release, nightly upgrade command, cache path to clear once, or exactly what info is still needed.
7. For diagnostic bundles, logs, crash dumps, screenshots, or local-state archives, do not ask reporters to paste or attach sensitive material publicly. Ask for the minimum public facts, then use the project's private support channel only when public project context provides one.
8. Prefer **editing** an existing maintainer comment (`PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}`) when updating wording; avoid delete plus repost unless the old text must disappear from history.
9. After posting or editing, re-read the comment body, author, target item, and issue/PR state. The public action is not complete without that receipt.
## When closing
The closing comment is the reporter's answer, not the investigation. State the fix state, the channel it arrives on, and when, then stop; root cause, file names, and the reasoning that got there belong in the commit. A reply that needs scrolling to read is over-length however well structured it is.
Close only when the fix is shipped, already available in the latest release, the report is invalid, the report is a duplicate, or the maintainer explicitly asked for closure. Otherwise leave open with the next-release acknowledgement.
references/release-surfaces.md
# Release Surface Review Methods
Loaded on demand from `/check` when a diff or release question touches one of these surfaces. The fill-in template shapes live in `project-context.md`; this file owns the review method.
## CLI Command Surface
Check command contract and installed-runtime behavior, not just library tests: help/version, subcommands/flags, exit codes, stdout/stderr, JSON/schema output, TTY/non-interactive paths, env/config precedence, shebang/executable bit, PATH shim, and package-manager install path when applicable.
For mutating CLI commands, also run the Safety Sink Review (see `project-context.md`): dry-run or confirmation path, operation log or rollback story, retry/idempotency, signal/partial-failure handling, and test-mode guards for auth prompts or real system changes. For cleanup, uninstall, prune, reset, or cache-removal commands, add two checks before approval: can a normal user verify each selected item is safe, and is the deleted content locally rebuildable rather than a downloaded dependency or user data? If either answer is no, require narrower matching, explicit user selection, or leave the item visible but non-destructive.
## Packaged Install Surface
Verify the installed runtime contract, not just the source tree:
1. Identify the install path a real user will get: package manager, release archive, marketplace entry, plugin source path, or installer script default ref.
2. Build or regenerate the package exactly as project docs require, then inspect the archive or generated mirror for every new script, reference, template, rule, manifest, and executable bit.
3. Run an isolated install smoke when the surface is installable: fresh temp home/config/cache, add the marketplace or package, install the skill or plugin, list it, and invoke the smallest command or entrypoint that proves scripts and references resolve from the installed path.
4. Filter generated mirrors and archives for cache/noise files such as `__pycache__`, `*.pyc`, `.pytest_cache`, `.ruff_cache`, `.mypy_cache`, `.DS_Store`, local logs, and screenshots unless the project explicitly ships them.
5. If network, auth, or host tooling prevents the install smoke, state the missing layer as a blocker or gap. Do not replace installed-runtime proof with manifest JSON, source tests, or a successful local import.
## Reworked Or Cancelled Release Gate
1. Lock the review base to the last public stable tag or release artifact, then review through current `HEAD`. Do not limit the review to recent commits or the latest local diff.
2. Record the exact base, `HEAD`, dirty state, origin sync, version fields, generated artifacts, release notes, package contents, CI, and remote distribution state. If any state changes mid-review, refresh the range and rerun the fast gates.
3. Review by shipped risk surface: user-reported regressions, crash or hang paths, destructive operations, privilege or permission boundaries, background workers, startup or first-frame work, update feeds, package contents, and public support claims.
4. Output two release decisions, not one: whether the preview or beta can keep taking user testing, and whether stable release prep can start.
5. Every conclusion must name blockers, deferrable maintenance, commands that ran, and runtime or user-smoke coverage. Source tests alone cannot prove a reworked UI/native release ready.
references/review-patterns.md
# Conditional Review Patterns
Load only the sections whose trigger appears in the diff. These are hard stops when the described failure can reach users; otherwise report them as advisory.
## Recurring or hard-to-observe bugs
For a recurring visual, layout, timing, or stateful-UI bug, do not accept another tuned constant as durable coverage. Pull the decision into a pure function and test the violated invariant, such as nonzero width, half-open hit regions, or bounded offsets. Runtime inspection proves one instance; the invariant prevents recurrence.
Do not demand a fake seam. If a shallow helper cannot exercise the real failure at its call site, report `no correct test seam` as the architectural defect. A pure function covers one wrong decision. It does not cover future callers bypassing a guard. For a silent, costly primitive such as direct deletion, raw privilege escalation, or an unbounded external command, add a source-invariant test that enumerates call sites and rejects raw usage outside an explicit allowlist.
## Captured output and asynchronous completion
When code branches on an error message or captured command output, probe what the string contains at runtime. A subprocess using inherited stdio may show diagnostics in the terminal while `error.message` contains only the command line. Prefer structured facts such as exit codes or known targets over reparsing prose.
Flag fixed `sleep`, `asyncAfter`, `setTimeout`, frame counts, or guessed timeouts that stand in for an observable completion signal. They vary across CPU speed, display refresh rate, and networks. Drive the next step from callbacks, navigation completion, frame changes, state flags, or wall-clock state as appropriate.
## Simplification and deletion
For prose, rule, skill, or guidance consolidation, read the deletions back and classify every removed behavior as `folded into X`, `redundant with Y`, or `behavior removed`. List behavior-removed entries explicitly. Deletion volume is not evidence of a good pass.
For dead-code or YAGNI claims, search the whole repository: entrypoints, docs, tests, generated dispatch tables, scripts, CI, packaging allowlists, manifests, and dynamic lookup patterns. Separate test-only from production references and chase data written but only read indirectly. If a dev tool is merely exposed by the wrong package or mirror, tighten distribution rather than deleting the tool. Partial search scope cannot justify deletion.
## History-sensitive normalization
When a diff restores a recently removed symbol, string, asset, enum case, localization entry, or config field, confirm current main still consumes it. A parity test or stale rule is not proof of life.
Before making an outlier match its siblings, inspect the change or comment that introduced the divergence. The asymmetry may deliberately avoid a known defect; normalization must preserve that protection.
## Non-atomic replacement of user files
When a diff writes to a path the user already has (`curl -o`, `>`, `tee`, open-truncate-write), ask what survives a failure partway through. Truncating the destination first means a dropped connection, timeout, or non-zero exit leaves a corrupt file and no original. Require staging into a sibling temp file, swapped in only once the content is complete.
Staging covers the paths the code tests for. It does not cover signals: with no trap, an interrupt mid-write can both strand the temp file and let the shell run past the interrupt to install partial content. A fetch running with `-fsSL`, `2>/dev/null`, or a swallowed exit code compounds this by telling the user nothing about what broke or what was left intact.
## Destructive matcher breadth
For recursion, mass deletion, traversal, ID-prefix wildcards, or fallback regex branches feeding a destructive sink, inspect:
- matcher breadth in every primary and fallback branch;
- protected-path coverage at the new entry point;
- user-confirmation paths; and
- whether the guard lives inside the deletion primitive rather than only at one caller.
Ask for the narrowest evidence authorizing deletion. Exact identifiers and exact paths can pass. Display names, vendor prefixes, common tokens, and user labels cannot safely authorize neighboring deletion.
## Duplicated derivations
Flag a classification, ordering, threshold, count, or eligibility rule computed independently in two places. Summary/list, preview/executor, score/explanation, and ordering/control pairs drift after the first one-sided change. Require one constant or pure function and have both consumers use it. When one side changes, search for its sibling.
## Test surface fidelity
A test is not coverage when it pins a helper that production never reaches or asserts the literal source form of a command/config string instead of the shipped entry point. Ask whether it fails on the unfixed code and whether users execute the asserted path. If either answer is no, the test is a finding.
## Never-shipped migrations
Reject migration scaffolding, version-gated defaults, or old-key carry-forward logic when the underlying preference, schema, or feature first appears in the current unreleased work. Compare with the last published tag. If the key did not ship, use the default path; migration is dead-on-arrival complexity.
## Unknown identifiers
Search every new function, type, variable, asset, command target, and config key that the diff assumes already exists. No result outside the new diff means the dependency is unproven. Dynamic registries require checking their generation or lookup path rather than trusting a name match.
scripts/audit_signals.py
#!/usr/bin/env python3
"""Project audit signals (Phase 1) for /check audit mode.
Walks a project root and emits structured signal blocks to stdout.
Each block ends with `status: PASS|WARN|FAIL|N/A` so the LLM driving the
4-axis Linus-style scorecard can skim quickly.
Pure stdlib. Read-only. Exits 0 even on WARN/FAIL so the harness does
not confuse "finding surfaced" with "script broken".
Run as: python3 skills/check/scripts/audit_signals.py --root <path>
"""
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
from pathlib import Path
EXCLUDED_DIRS = {
".git", ".hg", ".svn", "node_modules", "dist", "build", ".next",
"__pycache__", ".turbo", "target", ".venv", "venv", "vendor",
"coverage", ".cache", ".parcel-cache", ".pytest_cache", ".mypy_cache",
".ruff_cache", "Pods", "Carthage", ".swiftpm", ".gradle",
}
# Kept identical with skills/health/scripts/check_maintainability.py by
# tests/python/test_auditor_alignment.py (thresholds stay per-product).
SOURCE_EXTS = {
".bash", ".c", ".cc", ".cpp", ".cs", ".css", ".go", ".h", ".hpp",
".html", ".java", ".js", ".jsx", ".kt", ".lua", ".m", ".mjs", ".mm",
".md", ".php", ".py", ".rb", ".rs", ".scss", ".sh", ".swift", ".ts",
".tsx", ".vue", ".yaml", ".yml", ".zsh",
}
HOTSPOT_LINES = 500
HOTSPOT_FAIL = 1500
HEREDOC_LINES = 100
DRIFT_WARN = 50
DRIFT_FAIL = 150
DUP_JACCARD = 0.70
MAX_TEXT_BYTES = 2_000_000
MARKER_RE = re.compile(r"\b(TODO|FIXME|HACK|XXX)\b", re.IGNORECASE)
HEREDOC_OPEN_RE = re.compile(
r"(python3?|node|ruby|perl|php)\b[^|\n]*?<<-?\s*['\"]?(\w+)['\"]?"
)
INSTALL_URL_RE = re.compile(
r"raw\.githubusercontent\.com/[^/\s]+/[^/\s]+/([^/\s]+)/"
)
# --exclude requires = or trailing value to avoid matching git's --exclude-standard
DENYLIST_HINT_RE = re.compile(
r"(^\s*(skip|exclude)\s*=|\s--exclude=|!\*\.\w+|grep\s+-v\b|--ignore=)",
re.IGNORECASE,
)
MINIFIED_RE = re.compile(r"\.min\.[a-z]+$", re.IGNORECASE)
CLI_CONTRACT_BUCKETS: tuple[tuple[str, re.Pattern[str]], ...] = (
("help_or_usage", re.compile(r"(--help|\busage\b|\bhelp output\b)", re.IGNORECASE)),
("version", re.compile(r"(--version|\bversion output\b)", re.IGNORECASE)),
("exit_code", re.compile(r"\b(exit code|exit status|return code|exit_code|\$\?)\b", re.IGNORECASE)),
("stdout", re.compile(r"\b(stdout|standard output)\b|>\s*\"\$?[A-Za-z0-9_./-]*stdout", re.IGNORECASE)),
("stderr", re.compile(r"\b(stderr|standard error)\b|2>\s*\"\$?[A-Za-z0-9_./-]*stderr", re.IGNORECASE)),
("non_interactive_or_tty", re.compile(r"\b(non-interactive|noninteractive|tty|isatty|/dev/null|CI=1)\b", re.IGNORECASE)),
(
"install_run",
re.compile(
r"(\binstall\s+-m\b|\binstalled command\b|\binstalled-runtime\b|"
r"\binstall/run\b|\binstall run\b|\btemp prefix\b|\bPATH shim\b|"
r"\bpackage-manager path\b|\bnpm link\b|\bpipx install\b|"
r"\bcargo install\b|\bbrew install\b|\bmake install\b)",
re.IGNORECASE,
),
),
("json_or_schema", re.compile(r"\b(json|schema)\b", re.IGNORECASE)),
("completion", re.compile(r"\bcompletion\b", re.IGNORECASE)),
)
CLI_CORE_BUCKETS = (
"help_or_usage",
"version",
"exit_code",
"stdout",
"stderr",
"install_run",
)
# The file-walk helpers below are deliberately duplicated in
# skills/health/scripts/check_maintainability.py. Both scripts ship
# standalone (see packaging.allowlist) and run inside an arbitrary target
# project, so they import only stdlib. Do not hoist them into a shared
# scripts/ module: it is dev-only, not on the ship allowlist, and would
# couple a standalone tool to the install layout.
def is_excluded(path: Path, root: Path) -> bool:
try:
parts = path.relative_to(root).parts
except ValueError:
parts = path.parts
if any(p in EXCLUDED_DIRS for p in parts):
return True
return bool(MINIFIED_RE.search(path.name))
def is_repo_file(path: Path, root: Path) -> bool:
"""Return true only for a regular file reached without any symlink hop."""
try:
relative = path.relative_to(root)
except ValueError:
return False
if not relative.parts:
return False
current = root
try:
for part in relative.parts:
current /= part
if current.is_symlink():
return False
return current.is_file()
except OSError:
return False
def is_repo_dir(path: Path, root: Path) -> bool:
"""Return true only for a directory reached without any symlink hop."""
try:
relative = path.relative_to(root)
except ValueError:
return False
current = root
try:
for part in relative.parts:
current /= part
if current.is_symlink():
return False
return current.is_dir()
except OSError:
return False
def iter_files(root: Path) -> list[Path]:
try:
proc = subprocess.run(
["git", "-c", "core.fsmonitor=false", "-C", str(root), "ls-files",
"--cached", "--others", "--exclude-standard", "-z"],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, check=False,
)
if proc.returncode == 0 and proc.stdout:
out = []
for raw_path in proc.stdout.split(b"\0"):
if not raw_path:
continue
p = root / os.fsdecode(raw_path)
if is_repo_file(p, root) and not is_excluded(p, root):
out.append(p)
return out
except OSError:
pass
out = []
for dirpath, dirnames, filenames in os.walk(root):
current = Path(dirpath)
dirnames[:] = [
d for d in dirnames
if d not in EXCLUDED_DIRS and is_repo_dir(current / d, root)
]
if is_excluded(current, root):
continue
for fname in filenames:
p = current / fname
if is_repo_file(p, root) and not is_excluded(p, root):
out.append(p)
return out
def line_count(path: Path, root: Path) -> int:
if not is_repo_file(path, root):
return 0
try:
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
with os.fdopen(os.open(path, flags), "rb") as fh:
return sum(1 for _ in fh)
except OSError:
return 0
def read_text(path: Path, root: Path, limit: int = 0) -> str:
if not is_repo_file(path, root):
return ""
byte_limit = limit or MAX_TEXT_BYTES
try:
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(path, flags)
except OSError:
return ""
try:
chunks: list[bytes] = []
remaining = byte_limit
while remaining:
chunk = os.read(descriptor, min(65_536, remaining))
if not chunk:
break
chunks.append(chunk)
remaining -= len(chunk)
except OSError:
return ""
finally:
os.close(descriptor)
return b"".join(chunks).decode("utf-8", errors="replace")
def rel(path: Path, root: Path) -> str:
try:
value = path.relative_to(root).as_posix()
except ValueError:
value = path.as_posix()
return safe_label(value)
def safe_label(value: str, limit: int = 500) -> str:
if any(ord(char) < 32 or ord(char) == 127 for char in value):
value = json.dumps(value, ensure_ascii=False)
return value if len(value) <= limit else f"{value[: limit - 3]}..."
def header(name: str) -> None:
print(f"=== {name} ===")
def status(label: str) -> None:
print(f"status: {label}")
def block_hotspots(files: list[Path], root: Path) -> None:
header("FILE SIZE HOTSPOTS")
sized = ((p, line_count(p, root)) for p in files if p.suffix.lower() in SOURCE_EXTS)
big = sorted(
(item for item in sized if item[1] >= HOTSPOT_LINES),
key=lambda x: -x[1],
)[:10]
if not big:
print(f"(no source files >= {HOTSPOT_LINES} lines)")
status("PASS")
return
for path, n in big:
print(f" {n:>5} {rel(path, root)}")
status("FAIL" if any(n >= HOTSPOT_FAIL for _, n in big) else "WARN")
def block_heredoc(files: list[Path], root: Path) -> None:
header("HEREDOC BLOAT")
hits: list[tuple[str, int, str, int]] = []
for path in files:
if path.suffix.lower() not in {".sh", ".bash", ".zsh"}:
continue
text = read_text(path, root)
if not text:
continue
lines = text.splitlines()
i = 0
while i < len(lines):
m = HEREDOC_OPEN_RE.search(lines[i])
if not m:
i += 1
continue
lang, marker = m.group(1), m.group(2)
j = i + 1
close = re.compile(r"^\s*" + re.escape(marker) + r"\s*$")
while j < len(lines) and not close.match(lines[j]):
j += 1
size = j - i
if size >= HEREDOC_LINES:
hits.append((rel(path, root), i + 1, lang, size))
i = j + 1
if not hits:
print("(no python/node/ruby/perl/php heredocs >= 100 lines)")
status("PASS")
return
for f, ln, lang, sz in hits:
print(f" {f}:{ln} lang={lang} block_lines={sz}")
status("WARN")
def block_test_ci(files: list[Path], root: Path) -> None:
header("TEST AND CI SURFACE")
test_files = [
p for p in files
if p.suffix.lower() in SOURCE_EXTS
and (("test" in p.name.lower()) or ("spec" in p.name.lower()))
]
src_files = [p for p in files if p.suffix.lower() in SOURCE_EXTS]
wf_dir = root / ".github" / "workflows"
workflows = []
if is_repo_dir(wf_dir, root):
workflows = sorted(
path
for path in list(wf_dir.glob("*.yml")) + list(wf_dir.glob("*.yaml"))
if is_repo_file(path, root)
)
job_names: list[str] = []
for wf in workflows:
text = read_text(wf, root, 50_000)
for m in re.finditer(r"^name:\s*(.+?)\s*$", text, re.MULTILINE):
job_names.append(safe_label(f"{wf.name}: {m.group(1)[:60]}"))
break
ratio = len(test_files) / max(len(src_files), 1)
print(f"tests_count={len(test_files)} source_count={len(src_files)} "
f"ratio={ratio:.1%}")
print(f"ci_workflow_files={len(workflows)}")
for j in job_names[:10]:
print(f" workflow: {j}")
if not test_files and not workflows:
status("FAIL")
elif not test_files or not workflows:
status("WARN")
else:
status("PASS")
def _package_bin_entrypoints(root: Path) -> list[str]:
path = root / "package.json"
if not is_repo_file(path, root):
return []
text = read_text(path, root, 200_000)
try:
data = json.loads(text)
except json.JSONDecodeError:
return []
bin_field = data.get("bin")
name = str(data.get("name") or "package")
if isinstance(bin_field, str):
return [f"package.json bin:{name} -> {bin_field}"]
if isinstance(bin_field, dict):
return [
f"package.json bin:{cmd} -> {target}"
for cmd, target in sorted(bin_field.items())
if isinstance(cmd, str) and isinstance(target, str)
]
return []
def _pyproject_script_entrypoints(root: Path) -> list[str]:
path = root / "pyproject.toml"
if not is_repo_file(path, root):
return []
text = read_text(path, root, 200_000)
entries: list[str] = []
in_scripts = False
for line in text.splitlines():
stripped = line.strip()
if stripped.startswith("[") and stripped.endswith("]"):
in_scripts = stripped in {
"[project.scripts]",
"[tool.poetry.scripts]",
}
continue
if not in_scripts or not stripped or stripped.startswith("#"):
continue
m = re.match(r'([A-Za-z0-9_.-]+)\s*=\s*["\']([^"\']+)["\']', stripped)
if m:
entries.append(f"pyproject.toml script:{m.group(1)} -> {m.group(2)}")
return entries
def _cargo_entrypoints(root: Path) -> list[str]:
entries: list[str] = []
cargo = root / "Cargo.toml"
if is_repo_file(cargo, root):
text = read_text(cargo, root, 200_000)
if "[[bin]]" in text:
names = re.findall(r'(?m)^\s*name\s*=\s*["\']([^"\']+)["\']', text)
if names:
entries.extend(f"Cargo.toml bin:{name}" for name in sorted(set(names)))
else:
entries.append("Cargo.toml [[bin]]")
if is_repo_file(root / "src" / "main.rs", root):
entries.append("src/main.rs")
return entries
def cli_entrypoints(files: list[Path], root: Path) -> list[str]:
entries: set[str] = set()
entries.update(_package_bin_entrypoints(root))
entries.update(_pyproject_script_entrypoints(root))
entries.update(_cargo_entrypoints(root))
for path in files:
try:
parts = path.relative_to(root).parts
except ValueError:
continue
if not parts:
continue
if parts[0] == "bin" and len(parts) >= 2:
entries.add("/".join(parts[:2]))
if parts[0] == "cmd" and len(parts) >= 3 and path.suffix == ".go":
entries.add(f"cmd/{parts[1]}")
return sorted(entries)
def _is_cli_contract_candidate(path: Path, root: Path) -> bool:
try:
parts = path.relative_to(root).parts
except ValueError:
return False
if not parts:
return False
lower_parts = tuple(p.lower() for p in parts)
name = lower_parts[-1]
if name in {"readme.md", "readme.txt", "agents.md", "claude.md"}:
return True
if lower_parts[0] in {"tests", "test", "spec", "scripts"}:
return True
if "test" in name or "spec" in name:
return True
if len(lower_parts) >= 3 and lower_parts[:2] == (".github", "workflows"):
return True
return False
def cli_contract_evidence(files: list[Path], root: Path) -> dict[str, list[tuple[str, str]]]:
hits: dict[str, list[tuple[str, str]]] = {}
for path in files:
if not _is_cli_contract_candidate(path, root):
continue
text = read_text(path, root, 200_000)
if not text:
continue
for bucket, pattern in CLI_CONTRACT_BUCKETS:
m = pattern.search(text)
if m:
hits.setdefault(bucket, []).append((rel(path, root), m.group(0)))
return {bucket: sorted(values) for bucket, values in sorted(hits.items())}
def block_cli_contract_surface(files: list[Path], root: Path) -> None:
header("CLI CONTRACT SURFACE")
entries = cli_entrypoints(files, root)
if not entries:
print("(no CLI entrypoints detected)")
status("N/A")
return
print(f"entrypoints={len(entries)}")
for entry in entries[:12]:
print(f" entry: {safe_label(entry)}")
if len(entries) > 12:
print(f" ... {len(entries) - 12} more")
evidence = cli_contract_evidence(files, root)
covered = tuple(bucket for bucket, _ in CLI_CONTRACT_BUCKETS if bucket in evidence)
missing = tuple(bucket for bucket in CLI_CORE_BUCKETS if bucket not in evidence)
print(f"covered={','.join(covered) if covered else 'none'}")
print(f"missing={','.join(missing) if missing else 'none'}")
printed = 0
for bucket in covered:
for path, signal in evidence[bucket][:3]:
print(
f" evidence: {bucket} {safe_label(path)} "
f"signal={safe_label(signal)}"
)
printed += 1
if printed >= 12:
break
if printed >= 12:
break
if not missing:
status("PASS")
else:
status("WARN")
def _grep_version(path: Path, root: Path, pattern: str) -> str | None:
text = read_text(path, root, 20_000)
if not text:
return None
m = re.search(pattern, text, re.MULTILINE)
return m.group(1).strip() if m else None
def block_version_sources(root: Path) -> None:
header("VERSION SOURCE COUNT")
found: list[tuple[str, str]] = []
v = root / "VERSION"
if is_repo_file(v, root):
first = read_text(v, root).strip().splitlines()
if first:
found.append(("VERSION", first[0]))
probes = [
("package.json", r'"version"\s*:\s*"([^"]+)"'),
("Cargo.toml", r'^\s*version\s*=\s*"([^"]+)"'),
("pyproject.toml", r'^\s*version\s*=\s*"([^"]+)"'),
("setup.py", r"version\s*=\s*['\"]([^'\"]+)['\"]"),
]
for fname, pat in probes:
p = root / fname
if is_repo_file(p, root):
v_str = _grep_version(p, root, pat)
if v_str:
found.append((fname, v_str))
for pat in ("*.podspec", "*.csproj"):
for path in root.glob(pat):
if not is_repo_file(path, root):
continue
v_str = _grep_version(
path, root, r'(?i)version\s*[:=]\s*["\']?(\d+\.\d+\.\d+[\w.-]*)'
)
if v_str:
found.append((path.name, v_str))
for path in list(root.glob("build.gradle*")):
if not is_repo_file(path, root):
continue
v_str = _grep_version(
path, root, r'(?i)version\s*[:=]\s*["\']?(\d+\.\d+\.\d+[\w.-]*)'
)
if v_str:
found.append((path.name, v_str))
if not found:
print("(no declared version source found)")
status("PASS")
return
for f, val in found:
print(f" {safe_label(f)}: {safe_label(val)}")
distinct = {val for _, val in found if val}
print(f"sources={len(found)} distinct_values={len(distinct)}")
if len(found) > 1 and len(distinct) > 1:
status("WARN")
else:
status("PASS")
def block_packaging_posture(root: Path) -> None:
header("PACKAGING FILTER POSTURE")
allowlist_files = [
path for path in list(root.glob("*.allowlist")) + list(root.glob("MANIFEST.in"))
if is_repo_file(path, root)
]
pkg_scripts = [
path for path in (
list(root.glob("scripts/package*.sh"))
+ list(root.glob("scripts/release*.sh"))
)
if is_repo_file(path, root)
]
denylist_hits = 0
for sp in pkg_scripts:
for line in read_text(sp, root).splitlines():
if DENYLIST_HINT_RE.search(line):
denylist_hits += 1
if allowlist_files:
for f in allowlist_files:
print(f" allowlist: {rel(f, root)}")
print(f"posture=allowlist denylist_hits_in_scripts={denylist_hits}")
status("PASS")
return
if denylist_hits:
for sp in pkg_scripts:
print(f" script: {rel(sp, root)}")
print(f"posture=denylist denylist_hits_in_scripts={denylist_hits}")
status("WARN")
return
print("posture=none (no packaging scripts)")
status("N/A")
def block_install_url(root: Path) -> None:
header("INSTALL URL PINNING")
targets: list[Path] = [root / "README.md"]
targets += list(root.glob("scripts/setup*.sh"))
targets += list(root.glob("scripts/install*.sh"))
findings: list[tuple[str, int, str]] = []
for path in targets:
if not is_repo_file(path, root):
continue
text = read_text(path, root, 200_000)
for i, line in enumerate(text.splitlines(), start=1):
for m in INSTALL_URL_RE.finditer(line):
findings.append((rel(path, root), i, m.group(1)))
if not findings:
print("(no raw.githubusercontent.com refs found)")
status("PASS")
return
moving = [f for f in findings if f[2] in ("main", "master", "HEAD")]
for f, ln, ref in findings[:20]:
marker = " [MOVING]" if ref in ("main", "master", "HEAD") else ""
print(f" {f}:{ln} ref={ref}{marker}")
print(f"total={len(findings)} moving={len(moving)}")
if moving:
status("WARN")
else:
status("PASS")
def block_agent_doc_dedup(root: Path) -> None:
header("AGENT DOC DEDUP")
claude = root / "CLAUDE.md"
agents = root / "AGENTS.md"
have_c = claude.exists() or claude.is_symlink()
have_a = agents.exists() or agents.is_symlink()
if not have_c and not have_a:
print("posture=none")
status("PASS")
return
if not (have_c and have_a):
print(f"posture=single-file ({'CLAUDE.md' if have_c else 'AGENTS.md'} only)")
status("PASS")
return
if claude.is_symlink() and claude.resolve(strict=False).name == "AGENTS.md":
print("posture=symlink (CLAUDE.md -> AGENTS.md)")
status("PASS")
return
if agents.is_symlink() and agents.resolve(strict=False).name == "CLAUDE.md":
print("posture=symlink (AGENTS.md -> CLAUDE.md)")
status("PASS")
return
a = read_text(claude, root)
b = read_text(agents, root)
if a and a == b:
print("posture=identical (consider symlink to dedup)")
status("WARN")
return
cross = ("AGENTS.md" in a) or ("CLAUDE.md" in b)
a_set = {ln.strip() for ln in a.splitlines()
if ln.strip() and not ln.strip().startswith("#")}
b_set = {ln.strip() for ln in b.splitlines()
if ln.strip() and not ln.strip().startswith("#")}
union = a_set | b_set
jaccard = len(a_set & b_set) / len(union) if union else 0.0
print(f"jaccard={jaccard:.2f} cross_refs={cross}")
if jaccard >= 0.20:
print("posture=divergent-overlap (drift risk; consider symlink)")
status("WARN")
return
if cross:
print("posture=cross-ref (one references the other)")
status("WARN")
return
print("posture=independent")
status("PASS")
def block_drift_markers(files: list[Path], root: Path) -> None:
header("DRIFT MARKERS")
counts: list[tuple[str, int]] = []
total = 0
for path in files:
if path.suffix.lower() not in SOURCE_EXTS:
continue
text = read_text(path, root, 200_000)
if not text:
continue
n = sum(1 for line in text.splitlines() if MARKER_RE.search(line))
if n:
counts.append((rel(path, root), n))
total += n
counts.sort(key=lambda x: -x[1])
for f, n in counts[:5]:
print(f" {n:>4} {f}")
print(f"total={total}")
if total >= DRIFT_FAIL:
status("FAIL")
elif total >= DRIFT_WARN:
status("WARN")
else:
status("PASS")
def block_duplicate_setup(root: Path) -> None:
header("DUPLICATE SETUP SCRIPTS")
scripts = [
path for path in (
list(root.glob("scripts/setup-*.sh"))
+ list(root.glob("scripts/install-*.sh"))
)
if is_repo_file(path, root)
]
if len(scripts) < 2:
print("(fewer than 2 setup-* scripts to compare)")
status("N/A")
return
sets: dict[Path, set[str]] = {}
for sp in scripts:
sets[sp] = {ln.strip() for ln in read_text(sp, root).splitlines()
if ln.strip() and not ln.strip().startswith("#")}
pairs: list[tuple[str, str, float]] = []
names = list(sets.keys())
for i, a in enumerate(names):
for b in names[i + 1:]:
union = sets[a] | sets[b]
if not union:
continue
j = len(sets[a] & sets[b]) / len(union)
if j >= DUP_JACCARD:
pairs.append((rel(a, root), rel(b, root), j))
if not pairs:
print("(no setup pairs with jaccard >= 0.70)")
status("PASS")
return
for a, b, j in pairs:
print(f" {a} vs {b} jaccard={j:.2f}")
status("WARN")
def block_denylist_in_build(root: Path) -> None:
header("DENYLIST IN BUILD")
targets = (list(root.glob("scripts/package*.sh"))
+ list(root.glob("scripts/release*.sh"))
+ [root / "Makefile", root / "Justfile"])
real_targets = [p for p in targets if is_repo_file(p, root)]
if not real_targets:
print("(no build scripts present)")
status("N/A")
return
hits: list[tuple[str, int, str]] = []
for path in real_targets:
text = read_text(path, root, 100_000)
for i, line in enumerate(text.splitlines(), start=1):
if DENYLIST_HINT_RE.search(line):
hits.append((rel(path, root), i, line.strip()[:80]))
if not hits:
print("(no denylist patterns found in build scripts)")
status("PASS")
return
for f, ln, s in hits[:20]:
print(f" {f}:{ln} {s}")
status("WARN")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--root", type=Path, default=Path.cwd(),
help="Project root to audit (default: current working directory)",
)
args = parser.parse_args()
root = args.root.resolve()
if not root.is_dir():
print(
f"audit_signals: not a directory: {safe_label(root.as_posix())}",
file=sys.stderr,
)
return 2
files = iter_files(root)
print(f"project_root: {safe_label(root.as_posix())}")
print(f"files_scanned: {len(files)}")
print()
block_hotspots(files, root); print()
block_heredoc(files, root); print()
block_test_ci(files, root); print()
block_cli_contract_surface(files, root); print()
block_version_sources(root); print()
block_packaging_posture(root); print()
block_install_url(root); print()
block_agent_doc_dedup(root); print()
block_drift_markers(files, root); print()
block_duplicate_setup(root); print()
block_denylist_in_build(root)
return 0
if __name__ == "__main__":
sys.exit(main())
scripts/release_gate.py
#!/usr/bin/env python3
"""Deterministic release-gate signals for /check Ship mode.
Emits the machine-checkable half of the Release Gate 2.0 matrix as labelled
blocks, each ending with `status: PASS|WARN|FAIL|N/A`, so the reviewer can
paste evidence instead of re-deriving it. Judgment surfaces (distribution
lane, package contents, release assets, registry state) stay with the skill.
Pure stdlib. Read-only. No network. Exits 0 even on WARN/FAIL so the harness
does not confuse "finding surfaced" with "script broken".
Run as: python3 skills/check/scripts/release_gate.py --root <path>
"""
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
from pathlib import Path
SEMVER_RE = re.compile(
r"^[vV]?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
r"(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$"
)
INPUT_LIMITS = {
"VERSION": 4_096,
"package.json": 1_000_000,
"Cargo.toml": 1_000_000,
"pyproject.toml": 1_000_000,
"CHANGELOG.md": 5_000_000,
"CHANGELOG": 5_000_000,
"CHANGES.md": 5_000_000,
"HISTORY.md": 5_000_000,
}
def run_git(root: Path, *args: str) -> tuple[int, str]:
try:
proc = subprocess.run(
["git", "-c", "core.fsmonitor=false", "-C", str(root), *args],
capture_output=True,
text=True,
timeout=30,
)
except (OSError, subprocess.TimeoutExpired) as exc:
return 1, str(exc)
# Preserve leading spaces because porcelain status uses them as the index
# column. Trimming them turns the first unstaged file into a staged one.
return proc.returncode, (proc.stdout or "").rstrip()
def block(name: str, lines: list[str], status: str) -> None:
print(f"=== {name} ===")
for line in lines:
print(line)
print(f"status: {status}")
print()
def is_git_repo(root: Path) -> bool:
code, out = run_git(root, "rev-parse", "--is-inside-work-tree")
return code == 0 and out == "true"
def repo_file(root: Path, name: str) -> tuple[Path | None, str | None]:
"""Resolve a repository input without following it outside the root."""
candidate = root / name
if not candidate.exists() and not candidate.is_symlink():
return None, None
if candidate.is_symlink():
return None, f"{name} must not be a symlink"
try:
resolved = candidate.resolve(strict=True)
resolved.relative_to(root)
except (OSError, ValueError):
return None, f"{name} does not resolve to a file inside the repository"
if not resolved.is_file():
return None, f"{name} is not a regular file"
return resolved, None
def read_repo_text(path: Path, name: str) -> tuple[str | None, str | None]:
"""Read a validated repository input with no symlink follow and a size cap."""
limit = INPUT_LIMITS[name]
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
try:
descriptor = os.open(path, flags)
except OSError:
return None, f"{name} became unreadable after validation"
try:
chunks: list[bytes] = []
total = 0
while total <= limit:
chunk = os.read(descriptor, min(65_536, limit + 1 - total))
if not chunk:
break
chunks.append(chunk)
total += len(chunk)
except OSError:
return None, f"{name} could not be read completely"
finally:
os.close(descriptor)
if total > limit:
return None, f"{name} exceeds the {limit}-byte release-gate limit"
return b"".join(chunks).decode("utf-8", errors="replace"), None
def parse_semver(value: str) -> tuple[tuple[int, int, int], str | None] | None:
match = SEMVER_RE.fullmatch(value.strip())
if not match:
return None
core = tuple(int(match.group(index)) for index in (1, 2, 3))
prerelease = match.group(4)
if prerelease and any(
part.isdigit() and len(part) > 1 and part.startswith("0")
for part in prerelease.split(".")
):
return None
return core, prerelease
def evidence_value(value: str) -> str:
"""Render repository-controlled text as one bounded evidence line."""
has_controls = any(ord(char) < 32 or ord(char) == 127 for char in value)
rendered = json.dumps(value, ensure_ascii=True) if has_controls else value
return rendered if len(rendered) <= 160 else f"{rendered[:157]}..."
def worktree_state(root: Path) -> None:
code, out = run_git(root, "status", "--porcelain", "-uall")
if code != 0:
block("WORKTREE STATE", [f"git status failed: {out}"], "N/A")
return
staged = modified = untracked = 0
for line in out.splitlines():
if line.startswith("??"):
untracked += 1
continue
if line[:1].strip():
staged += 1
if line[1:2].strip():
modified += 1
lines = [f"staged: {staged}", f"modified: {modified}", f"untracked: {untracked}"]
status = "PASS" if staged == modified == untracked == 0 else "WARN"
if status == "WARN":
lines.append("dirty worktree: account for every file before a release claim")
block("WORKTREE STATE", lines, status)
def remote_sync(root: Path) -> None:
code, out = run_git(
root, "rev-list", "--left-right", "--count", "@{upstream}...HEAD"
)
if code != 0:
block("REMOTE SYNC", ["no upstream configured for current branch"], "N/A")
return
try:
behind, ahead = (int(n) for n in out.split())
except (TypeError, ValueError):
block("REMOTE SYNC", [f"unexpected git rev-list output: {out!r}"], "N/A")
return
lines = [f"ahead of upstream: {ahead}", f"behind upstream: {behind}"]
if behind:
lines.append("branch is behind upstream: sync before releasing")
status = "FAIL"
elif ahead:
lines.append("unpushed commits: remote does not have this state yet")
status = "WARN"
else:
lines.append(
"local tracking ref matches HEAD; fetch or ls-remote evidence is still required"
)
status = "WARN"
block("REMOTE SYNC", lines, status)
def latest_stable_reachable_tag(root: Path) -> str | None:
code, out = run_git(root, "tag", "--merged", "HEAD", "--sort=-version:refname")
if code != 0 or not out:
return None
for line in out.splitlines():
tag = line.strip()
parsed = parse_semver(tag)
if parsed and parsed[1] is None:
return tag
return None
def tag_baseline(root: Path) -> str | None:
tag = latest_stable_reachable_tag(root)
if tag is None:
block("TAG BASELINE", ["no stable tag reachable from HEAD"], "N/A")
return None
code, count = run_git(root, "rev-list", "--count", f"{tag}..HEAD")
lines = [f"latest stable reachable tag: {tag}"]
if code == 0:
lines.append(f"commits since tag: {count}")
if count == "0":
lines.append("HEAD is the tagged commit")
lines.append("local reachability only; confirm this tag is the latest published release")
block("TAG BASELINE", lines, "WARN")
return tag
def toml_version(text: str, sections: tuple[str, ...]) -> str | None:
current = ""
for line in text.splitlines():
header = re.match(r"\s*\[([^\]]+)\]", line)
if header:
current = header.group(1).strip()
continue
if current in sections:
m = re.match(r"\s*version\s*=\s*[\"']([^\"']+)[\"']", line)
if m:
return m.group(1)
return None
def collect_versions(root: Path) -> tuple[dict[str, str], list[str]]:
found: dict[str, str] = {}
errors: list[str] = []
version_file, error = repo_file(root, "VERSION")
if error:
errors.append(error)
if version_file:
text, read_error = read_repo_text(version_file, "VERSION")
if read_error:
errors.append(read_error)
elif text:
first = text.strip()
if first:
found["VERSION"] = first.splitlines()[0].strip()
pkg, error = repo_file(root, "package.json")
if error:
errors.append(error)
if pkg:
text, read_error = read_repo_text(pkg, "package.json")
if read_error:
errors.append(read_error)
else:
try:
data = json.loads(text or "")
except json.JSONDecodeError:
found["package.json"] = "(unparseable)"
else:
if isinstance(data, dict) and isinstance(data.get("version"), str):
found["package.json"] = data["version"]
cargo, error = repo_file(root, "Cargo.toml")
if error:
errors.append(error)
if cargo:
text, read_error = read_repo_text(cargo, "Cargo.toml")
if read_error:
errors.append(read_error)
elif text:
v = toml_version(text, ("package",))
if v:
found["Cargo.toml"] = v
pyproject, error = repo_file(root, "pyproject.toml")
if error:
errors.append(error)
if pyproject:
text, read_error = read_repo_text(pyproject, "pyproject.toml")
if read_error:
errors.append(read_error)
elif text:
v = toml_version(text, ("project", "tool.poetry"))
if v:
found["pyproject.toml"] = v
return found, errors
def version_sync(root: Path, tag: str | None) -> str | None:
found, errors = collect_versions(root)
if not found and not errors:
block(
"VERSION FIELD SYNC",
["no VERSION / package.json / Cargo.toml / pyproject.toml version found"],
"N/A",
)
return None
lines = [f"{name}: {evidence_value(value)}" for name, value in sorted(found.items())]
status = "FAIL" if errors else "PASS"
lines.extend(f"unsafe repository input: {error}" for error in errors)
invalid_evidence = [
name
for name, value in found.items()
if len(value) > 128
or any(ord(char) < 32 or ord(char) == 127 for char in value)
]
if invalid_evidence:
lines.append(
"version fields contain control characters or exceed 128 characters: "
+ ", ".join(sorted(invalid_evidence))
)
status = "FAIL"
if "(unparseable)" in found.values():
lines.append("unparseable manifest: fix it before trusting version sync")
status = "FAIL"
normalized = {
v[1:] if v.startswith(("v", "V")) else v
for v in found.values()
if v != "(unparseable)"
}
if len(normalized) > 1:
lines.append("version fields disagree: align them before releasing")
status = "FAIL"
manifest = next(iter(normalized)) if len(normalized) == 1 else None
raw_manifest = next(iter(found.values())) if len(normalized) == 1 and found else None
parsed_manifest = parse_semver(raw_manifest) if raw_manifest else None
if manifest and parsed_manifest is None:
lines.append("manifest version is not exact SemVer")
status = "FAIL"
if parsed_manifest and parsed_manifest[1] is not None:
lines.append("manifest is a prerelease version; do not treat it as stable-tag parity")
if status == "PASS":
status = "WARN"
if parsed_manifest and tag:
lines.append(f"stable reachable tag: {tag}")
parsed_tag = parse_semver(tag)
if parsed_tag:
manifest_core, manifest_pre = parsed_manifest
tag_core, tag_pre = parsed_tag
if parsed_manifest == parsed_tag:
lines.append("manifest exactly matches stable reachable tag")
elif manifest_core < tag_core or (
manifest_core == tag_core and manifest_pre is not None and tag_pre is None
):
lines.append("manifest BEHIND stable reachable tag: version regressed")
status = "FAIL"
elif manifest_core > tag_core:
lines.append(
"manifest ahead of stable reachable tag (unreleased version in progress)"
)
else:
lines.append("manifest does not exactly match stable reachable tag")
if status == "PASS":
status = "WARN"
block("VERSION FIELD SYNC", lines, status)
return manifest
def changelog_mentions(root: Path, manifest: str | None) -> None:
changelog = None
for name in ("CHANGELOG.md", "CHANGELOG", "CHANGES.md", "HISTORY.md"):
candidate, error = repo_file(root, name)
if error:
block("CHANGELOG VERSION", [f"unsafe repository input: {error}"], "FAIL")
return
if candidate:
changelog = candidate
break
if changelog is None or not manifest:
reason = "no changelog file" if changelog is None else "no single manifest version"
block("CHANGELOG VERSION", [reason], "N/A")
return
text, error = read_repo_text(changelog, changelog.name)
if error:
block("CHANGELOG VERSION", [error], "FAIL")
return
assert text is not None
version_pattern = re.compile(
rf"(?<![0-9A-Za-z.+-])(?:v|V)?{re.escape(manifest)}"
rf"(?![0-9A-Za-z.+-])"
)
if version_pattern.search(text):
block(
"CHANGELOG VERSION",
[f"{changelog.name} mentions {manifest}"],
"PASS",
)
else:
block(
"CHANGELOG VERSION",
[f"{changelog.name} does not mention {manifest}"],
"WARN",
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", default=".", help="project root to inspect")
args = parser.parse_args()
root = Path(args.root).resolve()
if not root.is_dir():
block("RELEASE GATE", [f"root not found: {root}"], "N/A")
return 0
if not is_git_repo(root):
block("WORKTREE STATE", ["not a git repository"], "N/A")
block("REMOTE SYNC", ["not a git repository"], "N/A")
block("TAG BASELINE", ["not a git repository"], "N/A")
manifest = version_sync(root, None)
changelog_mentions(root, manifest)
return 0
worktree_state(root)
remote_sync(root)
tag = tag_baseline(root)
manifest = version_sync(root, tag)
changelog_mentions(root, manifest)
return 0
if __name__ == "__main__":
raise SystemExit(main())
scripts/run-tests.sh
#!/usr/bin/env bash
# Auto-detect and run project verification (lint + typecheck + tests).
# Run from the project root. Exits non-zero on failure.
set -euo pipefail
if [ -f Cargo.toml ]; then
cargo check && cargo test
elif [ -f tsconfig.json ]; then
npx tsc --noEmit && npm test
elif [ -f package.json ] && grep -q '"test"' package.json; then
npm test
elif [ -f Makefile ] && grep -q '^test:' Makefile; then
make test
elif [ -f pytest.ini ] || [ -f pyproject.toml ] || find . -maxdepth 2 -name "test_*.py" | grep -q .; then
pytest
else
echo "(no test command detected - ask the user for the verification command)"
exit 1
fi
SKILL.md
---
name: check
description: "Reviews code diffs, PRs, issue queues, release readiness, commits, pushes, publishing, and project audits. Use when users ask in any language for code review, issue or PR triage, release gates, publishing follow-through, or project audits. Not for debugging root causes or prose review."
when_to_use: "review, 看看代码, 检查一下, 有没有问题, 是否需要优化, 合并前, 继续优化, 优化代码, 看看issue, 看看PR, release, publish, push, release reaction, GitHub reaction, 发布, 提交, 关闭issue, 发布表情, release表情, close issue, issue close, review my code, check changes, before merge, before release, 值得发布, ready to release, code review, code-review, audit, project audit, 项目体检, 项目评分, 给项目打分, 深入分析项目代码, 评估项目质量, 代码质量评分, scorecard, linus review, rate this codebase, score this project"
dispatch_intent: "Code review, before merge, release gates, generated artifacts, safety sinks, publish/push/reaction follow-through, triage issues/PRs, project-wide code-quality audit scorecard"
---
# Check: Review Before You Ship
Prefix your first line with 🥷 inline, not as its own paragraph.
> Note: `/review` is a built-in Anthropic plugin command for PR review. Waza uses `/check` (or the alias `code-review`) instead. Do not re-trigger `/review` from within this skill.
Read the diff and find the problems. Review, audit, triage, and readiness requests are report-only; apply fixes only when the current turn explicitly asks to fix, change, implement, or optimize. Done means the requested review surface is covered and every verification claim comes from this session.
## Outcome Contract
- Outcome: a review, release decision, or maintainer action grounded in the current diff, project context, and live evidence.
- Done when: findings, fixes, shipped state, or blockers are stated with the commands, artifacts, or remote state that prove them.
- Evidence: worktree status, diff, public project docs, manifests, CI, package contents, release or registry state, and current command output.
- Output: concise findings first, then verification and shipped-state summary when applicable. Multi-step or ship-action runs, and any request with several items or screenshots, close with a numbered completion ledger (done / not applicable / remaining), never a narrative that leaves the user asking "is everything done".
- Authorization: read-only intent may inspect the worktree and remote state but may not edit files, apply autofixes, commit, push, publish, comment, close, merge, or change branches. Each write or public action needs current-turn authorization, except when the user explicitly authorizes a named batch that contains it.
## Durable Context Preflight
See [references/durable-context.md](references/durable-context.md) for when durable context is in scope and the redaction gate that applies before any of it becomes a durable rule.
For `/check`: the current diff, CI, and remote state override memory. Durable memory can explain user intent and preferred follow-through, but public project rules still come from README files, manifests, CI workflows, release docs, and explicit instructions in the current thread. Never cite private memory as a public project requirement.
## Worktree Safety Preflight
Before any review, triage, ship, release, or PR operation, read the current worktree with:
```bash
git status --short --branch -uall
```
Treat modified, staged, and untracked files as user work. You may read them and include them in the review surface, but you must not move, hide, overwrite, clean, or discard them without explicit user approval in the current turn.
Do not run these commands as default review or PR setup: `git switch`, `git checkout`, `git reset --hard`, `git clean`, `git stash -u`, `git stash --include-untracked`, `git stash -a`, `git stash --all`, or `gh pr checkout`. If a branch change or cleanup is genuinely required, stop and ask for that exact operation.
Do not "protect" user work by moving untracked files, generated files, screenshots, or local scratch files into `/tmp` or another holding directory. Moving someone else's WIP out of the checkout is the same class of interference as stashing it. If a clean tree is required for generation, packaging, or verification, use a separate worktree from a known commit and copy only the artifact or patch you own back into the current checkout.
For commit or push follow-through in a dirty or multi-agent checkout, record `git rev-parse HEAD` before staging. Re-read `git status --short --branch -uall` and `git rev-parse HEAD` immediately before commit and again before push. If HEAD moved, unknown commits appeared, or the worktree changed outside your intended files, stop and report the mismatch instead of rebasing, recommitting, or pushing.
For PR inspection, prefer commands that do not switch the current working tree: `gh pr view`, `gh pr diff`, `git fetch origin pull/<n>/head:refs/tmp/pr-<n>`, and `git merge-tree`.
## Mode Picker
Pick the mode that matches the user's intent, then read it in full before acting. Modes layer on top of the shared review surface (Scope, Hard Rules, Hard Stops, Autofix, Specialist Review, Verification, Sign-off) further down, which applies in every mode. Load a mode file only when its row matches; the default review path needs none of them.
| User intent | Mode |
|---|---|
| "implement this plan", `/think` output handed off | [Plan Execution](#plan-execution-mode) |
| Diff or PR ready, "review", "看看代码", "合并前" | Default review (start at [Get the Diff](#get-the-diff)) |
| "look at issues", "review PRs", "triage", "批量处理" | load `references/mode-triage.md` |
| "is this worth a release", "值不值得发版" | load `references/mode-ship.md` (Release Worthiness Analysis) |
| "commit", "push", "publish", "release", "close issue", "发布表情" | load `references/mode-ship.md` (Ship / Release Follow-through) |
| "audit", "项目体检", "项目评分", "给项目打分", "深入分析项目代码", "scorecard", "linus review" | load `references/mode-audit.md` |
| Document, PDF, prose review | Delegate to `/write` (see [Document Review](#document-review)) |
Before any mode, run [Project Context Extraction](#project-context-extraction) and (if memory is in scope) [Durable Context Preflight](#durable-context-preflight).
## Project Context Extraction
This is Waza's public, standalone code-review capability. It should not depend on private machine paths or unpublished project instructions.
Before reviewing, extract project constraints from repository context:
1. Read the diff and identify changed languages, frameworks, manifests, generated outputs, release files, and CI workflows.
2. Inspect public project files only as needed: README, AGENTS/CLAUDE instructions when present, package manifests, lockfiles, build configs, test configs, workflow files, and release notes.
3. Compress the findings into review context: verification commands, protected or generated files, release artifacts, domain risks, and public reply rules.
4. Apply the stricter rule when project context and this skill overlap.
5. If project docs or CI name a verification command, prefer that over auto-detection.
For the context shape, see `references/project-context.md`.
For release or maintainer work, also fill the Release Gate 2.0 matrix from `references/project-context.md`. It covers review base, dirty/staged/untracked state, latest tag, origin sync, version fields, generated artifacts, package/archive contents, release assets, registry/appcast/CI, and public issue/PR state. Missing matrix evidence is a blocker for a "ready to release" claim.
## Plan Execution Mode
Activate when the user's message starts with "Implement the following plan", "按计划实施", "按照计划", "整", "可以干", "直接改" followed by a plan body, or links to a `/think` output.
In this mode, do not run a code review. Instead:
1. State which plan is being executed (first heading or summary line).
2. Check for obvious repo drift: run `git status --short --branch -uall` and skim any changed files that contradict the plan. If drift makes the plan unsafe, name the specific conflict and stop.
3. After all items are done, run the project's verification command.
4. Transition automatically into `references/mode-ship.md` if the project context or current thread indicates review-then-ship.
## Default Continuation (review-then-ship)
When the project's `AGENTS.md` or the current thread explicitly asks to "commit after review", "ship if green", or equivalent, load `references/mode-ship.md` and transition directly from review to the ship flow after a clean review. Do not ask again. State "proceeding to ship" before acting.
## Get the Diff
Derive the review baseline from the user's words and current repository state. Do not ask for commits when the scope is already inferable:
- **All local or uncommitted changes**: inventory staged, unstaged, and untracked files, plus local commits ahead of the configured upstream. Being on the base branch does not make this scope ambiguous.
- **PR or branch review**: use the merge base through the reviewed head, then add any dirty files in that checkout as a separate surface.
- **Since the last release**: use the latest published stable tag through `HEAD`, not the local version field, then add dirty files.
- **Recent N days or an explicit ref**: resolve that time/ref boundary through `HEAD`, then add dirty files.
- **Known-good or previous working version**: compare that ref through `HEAD`; route to `/hunt` Bisect Mode only when the regression point itself is unknown.
- **Whole-project audit**: use Audit Mode rather than pretending one diff is the repository.
Freeze the resolved base, `HEAD`, worktree inventory, generated/distribution surfaces, and delegated scopes before review. Ask one narrow question only when two plausible baselines would materially change the verdict. If review fixes are applied or repository state moves, the old verdict expires: re-read `HEAD`, status, and the full resolved diff before signing off.
## Scope
Measure the diff and classify depth:
| Depth | Criteria | Reviewers |
|-------|----------|-----------|
| **Quick** | Under 100 lines, 1-5 files | Base review only |
| **Standard** | 100-500 lines, or 6-10 files | Base + conditional specialists |
| **Deep** | 500+ lines, 10+ files, or touches auth/payments/data mutation | Base + all specialists + adversarial pass |
State the depth before proceeding.
Explicit depth language overrides the size thresholds. "All", "全部", "deep", "深入", or "仔细" means whole-scope coverage of the resolved inventory, even when the textual diff is small; it does not permit skipping untracked files, generated mirrors, required artifacts, or pending reviewers.
Static content diffs can stay quick even when they touch several generated files: version strings, dates, release-copy mirrors, sitemap dates, or one-for-one localization copy changes usually need line-by-line readback plus grep consistency, not a specialist fleet. Escalate only when the diff changes logic, generation rules, public distribution behavior, or user-facing semantics beyond the literal text replacement.
## Did We Build What Was Asked?
Before reading code, check scope drift: do the diff and the stated goal match? Label: **on target** / **drift** / **incomplete**.
Also check surgical traceability: every changed file and every new public surface must trace back to the user's stated goal. If a file, dependency, config knob, abstraction, generated artifact, workflow permission, or release behavior cannot be explained in one sentence from the request, label it drift until proven necessary.
For every new public setting, flag, environment variable, command, or service, ask who will change it and why one correct default cannot serve them. If there is no evidenced user split, treat the knob as scope drift and fix the default path instead.
Drift signals (examples, not exhaustive -- any one is enough to label drift):
- A changed file has no connection to the stated goal
- The diff includes pure refactoring (renames, formatting, restructuring) when the goal was a bug fix or feature
- A new dependency appears that the goal did not mention
- Code unrelated to the goal was deleted or commented out
- A new abstraction or helper was introduced that is not required by the goal
- A maintainability, review, or cleanup change quietly adds user-visible UI, default config, workflow permissions, or release behavior
## Question the Approach, Not Just the Diff
Scope drift checks the diff against the stated goal; this checks the goal against the approach. Skip when the user declares the route settled or the repo's design docs record the decision -- do not re-litigate deliberate trade-offs.
When findings cluster on one root cause -- the same bug class patched repeatedly, permission or state problems that follow from the architecture itself, a simple problem made complex -- stop listing patches and state the route verdict first: keep / adjust / replace / insufficient information. Compare a real alternative only when it eliminates the problem class at an acceptable migration cost; never manufacture one to fill the report. No patch list before the verdict.
## Pattern-Fix Completeness
When the diff fixes one instance of a class-of-bug, run the sibling sweep from hunt's Scope Blast Mode (anti-pattern 19) and confirm the other instances were handled. List any unswept sibling: a hard stop when it carries the same risk, advisory when lower-risk.
When the diff contains a recurring or hard-to-observe bug, captured output or asynchronous completion, simplification or deletion, history-sensitive normalization, non-atomic replacement of user files, broad destructive matchers, duplicated derivations, test-surface fidelity, never-shipped migrations, or unknown identifiers, load the matching section of `references/review-patterns.md`. Do not load that catalog for unrelated diffs.
## CLI Command Surface
When a diff touches a CLI entrypoint, installer, completion, config/env handling, package wrapper, or a mutating command such as cleanup, update, uninstall, migration, or cache removal, load `references/release-surfaces.md` (CLI Command Surface) and work its checklist, then fill the `CLI Command Surface` block of the Recommended Context Shape in `references/project-context.md` before sign-off. The core stance: verify command contract and installed-runtime behavior, not just library tests, and treat every mutating command as a safety sink.
Terminal output is a rendered surface. After changing CLI-facing text, spacing, or layout, re-run the command and read the real output before claiming done; editing the string is not seeing the screen.
## Skill, Plugin, And Packaged Install Surface
When a diff touches a skill, plugin, marketplace entry, installer, package allowlist, package manifest, generated mirror, or published archive, load `references/release-surfaces.md` (Packaged Install Surface) and verify the installed runtime contract through its five steps: real user install path, rebuilt package contents, isolated install smoke, noise filtering, and explicit gaps when the smoke cannot run. Manifest JSON, source tests, or a successful local import never substitute for installed-runtime proof.
## Hard Rules
- **No unverified claims.** Do not write "I verified X", "I ran Y", "tests pass", or "this fixes Z" unless the shell output is in this turn's transcript. If you reason about behavior without running, say "based on reading the code" instead of "I verified". Every verification claim in the sign-off must point to a command that actually ran in this session.
- **Re-read source-of-truth facts.** Refresh line numbers, worktree state, fallback behavior, locale coverage, artifact state, and the identity of any issue, PR, or thread in the current turn before citing or posting to it. Earlier context and reviewer notes are leads, not evidence.
- **Public replies follow `references/public-reply.md`**: short natural paragraphs, one thanks, no bullet structure, in the reporter's language.
## Hard Stops (fix before merging)
Examples, not exhaustive -- flag any diff that could cause irreversible harm if merged unreviewed.
- **Destructive auto-execution**: any task marked "safe" or "auto-run" that modifies user-visible state (history files, config, preferences, installed software) must require explicit confirmation.
- **Source and distribution out of sync**: everything the source change implies downstream must be regenerated, tracked, uploaded, and version-consistent before declaring done: generated or bundled outputs rebuilt and included, every artifact named in release notes or workflows actually uploaded, every new helper module, reference file, or script present in the built archive, and version fields synchronized across manifests, package metadata, changelogs, tags, and lockfiles.
- **Verifier failure layer unclear**: if a verifier fails before assertions or due to missing optional dependencies, bootstrap noise, transient build-service crashes, unavailable simulators, or tool setup, classify setup versus product failure. Retry only with new evidence or a narrower environment. Do not call the repo broken until the intended test body or artifact check actually ran. The inverse is the same stop: a verifier that passes without running the real path -- a skipped optional-dependency job that still prints OK, a function that early-returns leaving output empty so a true-on-empty assertion passes, a render reported fixed but never opened -- is a hollow green. A pass counts only when at least one non-skipped, non-empty case exercised the path and the assertions fail on emptiness.
- **Publishing over your own open findings**: when the same run produced review findings and then reaches a ship action, every finding must be fixed, or restated as "known, shipping anyway" with its user impact and confirmed, before the release proceeds. A standing release authorization does not cover problems discovered after it was given.
- **Injection and validation**: SQL, command, path injection at system entry points. Credentials hardcoded, logged, committed, or copied into public docs.
- **Dependency changes**: unexpected additions or version bumps in package.json, Cargo.toml, go.mod, requirements.txt. Flag any new dependency not obviously required by the diff. The inverse is a finding too: a declared dependency or linked SDK with zero imports across the repo gets flagged to the maintainer, not silently removed (it may be staged for an upcoming feature, and unused analytics/telemetry SDKs still drag app review and privacy manifests). Removal needs the maintainer's go-ahead in the current turn, a grep proving zero references first, and a full build after.
- **Verify lockfile consistency in the project's declared environment.** Check the full manifest, overrides, resolver configuration, and dependency diff, then run the project's frozen/locked verification. Regenerate in an isolated checkout only to investigate a concrete discrepancy, using the complete proposed inputs and declared toolchain. Equal bytes do not prove provenance; differences require explanation, not an assumption of hand-editing. Confirm that the resolved dependency graph implements the requested change.
- Automated security PRs get two extra checks their scanner does not do. First, whole-file reserialization: bots that rewrite a manifest can silently escape non-ASCII (emoji, CJK) or reorder keys, so diff the manifest against base in full rather than only the version line. Second, reachability: confirm the package is actually built into the shipped artifact (`cargo tree -i <pkg> --target all`, or the equivalent import/feature check) before repeating the advisory's severity, since an inert lockfile entry is not a live vulnerability in this project.
- When a version pin exists, find out why before moving anything near it: `git log -S'<pinned-name>' -- <manifest>` usually names the bug it was added for. A bump that satisfies the advisory but leaves a companion pin at its old version can be worse than not bumping at all.
- **Safety sinks**: destructive file operations, shell or AppleScript construction, cwd/path/symlink traversal, approval or sandbox boundary changes, signing/appcast flows, and auth prompts need explicit review of validation, rollback, and user-confirmation behavior.
## Finding Quality Gate
Before writing any finding into the report, run this gate:
**Pre-report self-check (four questions, every finding must pass):**
1. Can I cite the exact file:line?
2. Can I describe the specific input or state that triggers the bad outcome?
3. Have I read the upstream callers / downstream consumers, not just the function in isolation?
4. Is the severity defensible? Would a senior reviewer raise this at this level in a real PR?
If any answer is "no", drop the finding or downgrade it to advisory. Vague findings train the reader to ignore real ones.
**A clean review is a valid review.** Do not manufacture findings to justify the invocation. Zero findings with a stated review surface is a complete output. Padding the report with low-confidence noise is a worse outcome than reporting nothing.
**HIGH and CRITICAL require three pieces of evidence:**
1. The exact file:line where the bug lives.
2. The specific trigger: what input, state, or sequence produces the bad outcome.
3. Why existing guards (validation, type system, upstream catch, framework default) do not already prevent it.
Cannot supply all three? Downgrade to MEDIUM, or drop. "This *might* break under some condition" is not a HIGH.
## Knowledge Sync
After reviewing the diff, check whether it introduces invariants not yet captured in project docs:
- New safety gate or path-guard rule goes to AGENTS.md
- New UI constraint (layout rule, animation, overlay registration) goes to `.claude/rules/*.md`
- New deploy/release step or artifact goes to AGENTS.md or `docs/`
- New cross-file sync requirement (enum and HTML anchors, Swift keys and xcstrings) goes to AGENTS.md
- One-off review reports or diagnostic snapshots should not become durable docs as-is; extract the stable rule into AGENTS/CLAUDE/rules/references and drop the stale report from the commit.
### Snapshot Report Routing
Treat review reports, scorecards, and diagnostic snapshots as evidence, not as source-of-truth docs. Before approving one:
1. Re-read the current diff or repo surface named by the report. If the claim is stale, exclude the report from the commit or rewrite it into a stable rule.
2. Keep project-specific commands, paths, protected areas, release rituals, and safety constraints in that project's public context. Do not promote them into Waza.
3. Promote only transferable review behavior into Waza: e.g. "check untracked files before readiness", "inspect generated package contents", or "turn one-off reports into invariants."
If found, either apply the doc update as `safe_auto` (when the invariant is clear from the diff) or flag it in the sign-off as `doc debt`. When no new invariants exist, sign-off says `doc debt: none`.
## Specialist Review (Standard and Deep only)
Load `references/persona-catalog.md` to determine which specialists activate. When the environment has an agent or sub-agent facility, launch all activated specialists in parallel, each with the full diff and its own persona brief. If no parallel reviewer facility exists, run the specialist passes sequentially in the same session.
Merge findings: when two specialists flag the same code location, keep the higher severity and note cross-reviewer agreement. Findings on different code locations are never duplicates even if they share a theme.
Every specialist finding is a claim to verify, not a fact to act on. For HIGH and CRITICAL claims, when the agent facility allows it, spawn one independent skeptic per finding whose only brief is to refute it against the actual code; a finding the skeptic refutes on direct read is dropped or downgraded regardless of which persona raised it. Without the facility, run the skeptic pass yourself: re-read the cited code this turn and confirm the claim is real and live, not already handled elsewhere, not consistent-by-design, not a latent-only risk labeled as a live bug. Parallel reviewers over-report from name-based inference and partial context; drop what dissolves on direct read, and cite the verification path before routing anything to Autofix or sign-off.
Before a whole-scope verdict, reconcile a completion ledger for every delegated review: assigned scope, returned status, and uncovered remainder. Wait for every active reviewer, or name its scope as unreviewed. Never say "all read", "full audit complete", or "no issues" while any reviewer or required verification is still pending.
## Autofix Routing
| Class | Definition | Action |
|-------|------------|--------|
| `safe_auto` | Unambiguous, risk-free: typos, missing imports, style inconsistencies | Apply only after explicit write authorization; otherwise report it |
| `gated_auto` | Behavior fixes with a clear intended result: null checks, error handling additions | Apply within explicit repair authorization; ask only for scope expansion or an unresolved user choice |
| `manual` | Architecture or security tradeoffs with no settled intended result | Resolve from project context; present any remaining user decision |
| `advisory` | Informational only | Note in sign-off |
Write authorization covers necessary fixes within its scope, including behavior changes needed for the requested result. A routing class does not create another approval step. In report-only mode, do not modify the worktree.
Any fix made during review invalidates the pre-fix verdict. Re-freeze the baseline, re-run the check that exposed the finding, refresh the sibling sweep, and complete the final adversarial pass required by the review depth before declaring ready.
## Adversarial Pass (Deep only)
"If I were trying to break this system through this specific diff, what would I exploit?" Four angles (see `references/persona-catalog.md`): assumption violation, composition failures, cascade construction, abuse cases. When the agent facility exists, run the four angles as parallel agents, each blind to the others' findings: convergence from independent angles raises confidence, and singleton findings face the same per-finding skeptic verification as specialist claims. Suppress findings below 0.60 confidence.
## Platform Operations
Use the platform tool that matches the project. For GitHub projects, prefer `gh` or the available GitHub integration and confirm CI passes before merging. For non-GitHub projects, derive the CLI/API from public project docs or the user's explicit platform context; do not force GitHub commands onto other hosts.
Poll CI as structured state, not streamed text: `gh run view <id> --json status,conclusion` (or the host's equivalent). Piping `gh run watch`, test output, or build output through `tail`/`head` swallows the real exit code and can report a failed or still-running run as green.
## Verification
Use the project's known verification command appropriate to the changed surface. Otherwise, `bash <skill-base-dir>/scripts/run-tests.sh` from the target project root can discover a candidate command. Report the exit status and summary, with relevant failure output rather than full passing logs.
A failed check needs diagnosis; no detected command is a discovery gap, not proof of failure or of no verification surface. Inspect project docs, manifests, and CI for an appropriate check. Complete a read-only review with explicit evidence limits when no check is available. Block a fix or readiness claim only when required evidence is missing or failing, and ask for a command only if it cannot be recovered from project context.
For bug fixes: a regression test that fails on the old code must exist before the fix is done.
In a dirty or multi-agent checkout, a passing local build or test run is not proof your change is sound: unrelated WIP already in the tree can supply missing symbols, mask a break, or fail for reasons unrelated to you. Verify in isolation -- `git worktree add --detach <known-good-commit>`, `git apply` only the diff of the files you own, then build/test there. The clean isolated pass is the real signal; the contaminated local pass is not.
## Document Review
For document, PDF, white paper, or prose review, route to `/write` (Document Review Mode). `/check` handles code diffs and release artifacts only.
## Gotchas
| What happened | Rule |
|---|---|
| New file name duplicated a locale, platform, or suffix convention | Check the target directory's existing naming convention before creating or renaming files |
| Deployed without provider runtime or env checks | Follow the project's public deployment docs and compare provider config with local required env and runtime settings |
## Sign-off
Open the final message with the `status` line as plain prose before any table or detail: exactly where the work stands now, with the hash, tag, or blocker. A verdict buried under verification tables reads as unfinished; the tables support the verdict, they do not replace it.
```
status: [committed and pushed as <hash> / staged, not committed / released vX.Y.Z / blocked on <what>]
files changed: N (+X -Y)
scope: on target / drift: [what]
user-visible delta: none / [entry, UI, copy, behavior added, removed, or changed]
review depth: quick / standard / deep
hard stops: N found, N fixed, N deferred
sibling sweep: N same-shape sites checked, N fixed / none found / not applicable
specialists: [security, architecture] or none
new tests: N
public actions: replied #N, closed #N, reactions done / none pending
doc debt: none / AGENTS.md needs X / rules need Y
verification: [command] -> pass / fail
```
`public actions` lists every outward-facing step the task implied (issue replies, closures, release reactions) with its done or pending state; an external action the user has to ask about was not finished.
For a whole-scope or post-fix verdict, `scope` is backed by the frozen baseline and current inventory, not by the last patch viewed. For a ship action, the status line is incomplete until every currently authorized ledger item is `done`, `not applicable`, or `blocked` with evidence.
A turn that wrote files ends with the actual output of `git status --short --branch` and, when it pushed, the `status,conclusion` of the CI run for that sha; if either command was not run, the first line says which.