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.
**CI workflow privilege and trust:** A workflow file is code holding a repository token. Flag a job that declares no `permissions` block where the repository default is write, a third-party action pinned to a branch or tag rather than a commit SHA inside a job holding write scope, and any `${{ github.event.* }}` or `inputs.*` value interpolated into a `run:` body, where it expands before the shell parses it; untrusted values pass through `env:` and are shape-checked first.
**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. Live task instructions and current project rules decide authorization, including explicit authorization still in force for the same unfinished goal. 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.
references/incident-ledger.md
# Incident Ledger And Review Evals
Loaded from `check` when a fix targets a defect that reached users or a published build, when a review prompt, skill, reviewer persona, or model changes, or when the user asks whether review is catching what it should. The project's own ledger and layer names win over this shape.
## The ledger
A project that ships to users keeps one table, conventionally `docs/incidents.md`, with one row per defect that reached a user or a published build. The row lands in the same change as the fix; a fix without its row is incomplete in a project that keeps the ledger.
```markdown
## Incidents
<!-- layers: write-time, unit, integration, review, e2e, release, field -->
| ID | Reported | Symptom | Introduced | Fixed | Layer | Caught by |
|---|---|---|---|---|---|---|
| I-01 | 2026-01-02 | What the user saw, in their words | unknown | 1a2b3c4d | unit | `test_name_that_fails_on_the_old_code` |
```
- `Symptom` is the observed failure, not the mechanism.
- `Introduced` is the commit that brought the defect in, or `unknown`; it measures how long defects stay in.
- `Layer` is the earliest layer that should have caught it, from the project's ordered list (earliest first). The retro question is which layer missed it, never who wrote it.
- `Caught by` names, in backticks, the test or gate that now fails on the pre-fix code: a test function or test title, or a gate script's repository path. A row with no live catcher is a note, not coverage.
Validate it with `python3 <skill-base-dir>/scripts/incident_ledger.py --root <project>`: unique IDs, dates, commits that resolve (shape-checked only in a shallow clone, which it says), layers from the `<!-- layers: ... -->` declaration or `--layers`, and catchers that are existing workspace files or recognized declarations in Python, shell/Bats, or JS/TS test files, including non-ignored new files. Other languages and declaration forms need an explicit gate path; finding a declaration does not prove that it executes or catches the defect. Wire it into the project's fast check so a renamed or deleted test fails its row instead of silently orphaning it. Projects may keep their own checker with a stricter catcher resolver; the contract is the same.
## Replay eval
The ledger is the regression suite for review itself. Rerun it when a review prompt, skill, persona, or model changes, and before trusting a new reviewer:
1. `incident_ledger.py --plan` prints each row's pre-fix revision (`<Fixed>^`) and the files the fix touched.
2. Run the changed reviewer on those files as they were at that revision, from a detached worktree, without telling it the symptom.
3. Count recovered rows: a row is recovered when a finding names the defect at the right place with a trigger that matches the symptom. Report recovered over total, per layer, beside the previous run's count.
4. Run the same reviewer on a few known-clean changes (merged changes with no later fix touching them). Every finding there is noise; report the count beside the recovered rows. A reviewer that recovers more rows by also doubling noise is not better.
A drop in recovered rows blocks adopting the change until explained. Record the run's counts where the project keeps review history, not in the ledger rows.
## Recurring classes
When two incidents, or two review rounds, share a bug class, the next fix belongs at the source: a lint rule, a source gate that enumerates the call sites, a type, or a routed project rule, with the earlier rows pointing at that one catcher. Another per-site test beside the last one is not the fix.
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. Select every delivery surface the authorized request covers before running gates, and apply the union of their steps:
- **Commit/push:** inspect the intended diff, run its required checks, synchronize tracked generated outputs, and follow steps 4-5. Verify the remote SHA and relevant CI after push. Do not require a tag, release notes, registry publication, or uploaded assets unless the change or project rules make them part of this task.
- **Release readiness, packaging, or publication:** apply steps 1-3, then the authorized delivery steps.
- **Public replies, issue closure, or reactions:** apply the matching steps 6-7 against the actual delivery channel. Commit/push authorization alone does not authorize these actions.
Apply only the steps of the selected surfaces; step 8 applies whenever a network or API operation fails:
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`, and serialize git operations so index locks or overlapping adds do not corrupt the workflow. In a dirty or multi-agent checkout, record `git rev-parse HEAD` before staging, then 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. A commit in a risk tier (`project-context.md`, Risk tiers by path) states in its body how it was verified and how to revert it.
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 posting, editing, read-back, and closure criteria; the reply body follows `/write` Public Reply Mode.
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. For an at-most-once operation (publish, tag, release create, payment), a verification timeout is unknown, not failed: a registry can gate a successful upload behind a scan that delays visibility for tens of minutes. Use bounded read-only checks within the project's retry policy; if visibility remains unknown, report the exact unresolved state and stop. Never re-run the publish to make the check pass.
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. Answer existing affordances with the current path and assess each remaining ask against the maintainer's accepted scope. Classification does not cancel an explicitly requested cosmetic fix or authorize removing an accepted feature; record a separate disposition for every ask.
**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. Missing logs, inability to reproduce, or inability to fix now do not establish invalidity or a not-planned decision. State the missing evidence and the smallest next diagnostic step; send that request only within the user's authorization.
**Several repositories at once:** When the request covers more than one repository ("all my repos", "其他仓库的 issue 和 PR 也看看"), list them first from the host (for GitHub, `gh repo list <owner> --no-archived` plus each repository's open issue and PR counts), state which ones are in scope and which are skipped and why, and freeze the IDs per repository before touching any. For every item, read the repository's history before judging it: `git log` for the topic, linked and closed PRs, and the guide or decision record, because an item may already be fixed under another title or be a recorded, deliberate decision; a deliberate decision gets a short, friendly reply that names the boundary and closes, not a re-opened debate. Then run the flow above per repository: fix what is in scope, ship within the authorization in force, close what will not be done. Report per repository and in total with four counts: fixed, already done, closed as not planned, and still pending with its blocker; a repository that was in scope and is not in the report is unfinished.
**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.
Keep PRs with unresolved evidence in a pending state. Once the evidence supports a final decision, name one of three dispositions in the analysis output: 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 replies:** load `references/public-reply.md` for login resolution, edit-or-new-comment, read-back, and closure criteria; the reply body follows `/write` Public Reply Mode.
**Sign-off line (append to standard sign-off):**
```
triage: N reviewed, N closed, N deferred
repositories: N in scope; per repository and total: N fixed, N already done, N not planned, N pending
```
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.
## Review Coordination
Use the activation signals below. 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.
## 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 posting, edit-or-new-comment, read-back, and closure criteria; the reply body follows `/write` Public Reply Mode. 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.
- Changing CI workflows, release or publish scripts, installers, or anything that runs with elevated privilege.
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.
### Risk tiers by path
The tier follows the paths a change touches, not its size or author. A one-line change to a workflow, updater, license check, installer, privilege helper, or deletion path gets the project's heaviest gate (its full or native test run, a security pass, and a human maintainer's review), and an agent's, bot's, or automated reviewer's approval never stands in for that human. Extract the project's tiers from its docs; when it keeps a table-driven script that prints the gates for the changed paths, run it and paste its lines. Such a script is advice, not a pass: the gates it names may run where this session cannot prove they ran. Commits and PRs in a tier also state how they were verified and how to revert them (`mode-ship.md` step 4).
references/public-reply.md
# Public Reply Mechanics (maintainer, issue or PR)
Used by Triage Mode and Ship / Release Follow-through for the actions around a reply. The reply body belongs to `/write` Public Reply Mode; without `/write`, open with `@<login>` and at most one short thanks, match the opener's language, and give one or two sentences naming the exact ship state and the reporter's next step, each true at the moment of posting. `AGENTS.md` or `CLAUDE.md` in the target repo overrides this shape.
1. Resolve `@<login>` from `gh issue view` / `gh pr view --json author` before posting, and re-read the live item there rather than replying from memory. When requesting logs, crash dumps, screenshots, or local-state archives, ask only for the minimum non-sensitive public facts; use a private support channel only when the project's public context provides one. Choose a next step the reporter can perform with the reported failure or accessibility constraint still present; do not require disabling the assistive capability they need to follow it. This applies even when `/write` is unavailable.
2. Edit your own comment in place (`PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}`) only while nobody has replied after it. Once the reporter or anyone else has replied, post a new comment instead of rewriting history; never delete and repost unless the old text must disappear.
3. 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.
4. 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 it 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. When a build or test pass is cached, trace newly bundled resources and test fixtures into the cache key too. In a disposable copy, change one new input and one known-covered input separately as controls: a changed package or test input must invalidate the reused pass, even if the verifier and key share the same incomplete list.
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. Then ask the two questions a red-green run cannot answer. What can the assertion not see: a matcher scoped to tagged instances reads an untagged one as exempt rather than as a blind spot, an input set built before the producer's own transformation never contains what production emits, and a parity check comparing two copies to each other is blind to what both are missing. What benign change turns it red: an anchor on exact wording, on a positional slice, or on one spelling of a rule usually written another way survives the removal it exists to catch and fails on a rewrite. Anchor on the fact, the identifier, or the enclosing structure.
## 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.
## Dependency changes
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.
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/incident_ledger.py
#!/usr/bin/env python3
"""Validate an incident ledger and print the replay plan for review evals.
An incident ledger is a Markdown table, one row per defect that reached users
or a published build. Each row names the fix commit, the introducing commit
when known, the earliest layer that should have caught it, and the test or
gate that catches it now. This helper checks that every row still holds:
- the ID is unique and the Reported cell, when present, is a YYYY-MM-DD date;
- Fixed is a commit that resolves, and Introduced is `unknown` or a commit;
- Layer is one of the ledger's declared layers;
- every backticked catcher in "Caught by" is an existing workspace file path
or a recognized declaration in a Python, shell/Bats, or JS/TS test file.
Unsupported declaration forms and languages require an explicit gate path.
Declaration recognition does not prove that a test runs or catches a defect.
In a shallow clone commits cannot be resolved, so they are shape-checked and
the summary says so. `--plan` prints, per row, the pre-fix revision and the
files the fix touched, which is what a review replay runs against.
Layers come from `--layers a,b,c`, else from a `<!-- layers: a, b, c -->`
comment in the ledger, else DEFAULT_LAYERS. Prose that explains the layers
is not parsed; the comment is the declaration.
Pure stdlib. Read-only. Exit 0 when every row holds, 1 when any row fails,
2 on usage errors or a missing ledger.
Run as: python3 <skill-base-dir>/scripts/incident_ledger.py --root <project>
"""
from __future__ import annotations
import argparse
import ast
import shlex
import re
import subprocess
import sys
from pathlib import Path
DEFAULT_LEDGER = "docs/incidents.md"
DEFAULT_LAYERS = ("write-time", "unit", "integration", "review", "e2e", "release", "field")
REQUIRED_COLUMNS = ("ID", "Fixed", "Layer", "Caught by")
SHA = re.compile(r"^[0-9a-f]{7,40}$")
DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
TEST_DIR_PARTS = {"test", "tests", "spec", "specs", "__tests__", "testing"}
TEST_NAME = re.compile(
r"(^test_.*|.*_test\.[^.]+$|.*\.(test|spec)\.[^.]+$|.*Tests?\.[^.]+$|.*\.bats$)"
)
SKIP_PARTS = {".git", "node_modules", "vendor", "dist", "build", "target", ".venv"}
MAX_TEST_BYTES = 2_000_000
def git(root: Path, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(["git", "-C", str(root), *args], capture_output=True, text=True)
def split_row(line: str) -> list[str]:
return [cell.strip() for cell in line.strip().strip("|").split("|")]
def table(text: str):
"""Header cells and (line number, cells) rows of the first table under
`## Incidents`, or of the first table in the file when no such section."""
lines = text.splitlines()
start = next((i for i, line in enumerate(lines) if line.strip() == "## Incidents"), -1) + 1
header: list[str] = []
body: list[tuple[int, list[str]]] = []
for index in range(start, len(lines)):
line = lines[index]
if header and not line.startswith("|"):
break
if start and line.startswith("## "):
break
if not line.startswith("|"):
continue
cells = split_row(line)
if not header:
header = cells
elif set("".join(cells)) <= set("-: "):
continue
else:
body.append((index + 1, cells))
return header, body
def declared_layers(text: str) -> tuple[str, ...]:
match = re.search(r"<!--\s*layers:\s*(.*?)\s*-->", text, re.IGNORECASE | re.DOTALL)
if match:
names = tuple(name.strip() for name in match.group(1).split(",") if name.strip())
if names:
return names
return DEFAULT_LAYERS
def tracked_files(root: Path) -> list[str]:
"""Existing, contained worktree files, including non-ignored new files."""
root = root.resolve()
out = git(root, "ls-files", "--cached", "--others", "--exclude-standard", "-z")
candidates = out.stdout.split("\0") if out.returncode == 0 else (
p.relative_to(root).as_posix() for p in root.rglob("*")
)
files = set()
for rel in candidates:
if not rel or SKIP_PARTS.intersection(Path(rel).parts):
continue
path = root / rel
try:
path.resolve().relative_to(root)
if path.is_file() and not path.is_symlink():
files.add(rel)
except (OSError, ValueError, RuntimeError):
continue
return sorted(files)
def is_test_file(path: str) -> bool:
parts = path.split("/")
if SKIP_PARTS.intersection(parts):
return False
if TEST_DIR_PARTS.intersection(part.lower() for part in parts[:-1]):
return True
return bool(TEST_NAME.match(parts[-1]))
def test_names(source: str, suffix: str) -> set[str]:
"""Recognize supported declarations, not arbitrary identifier mentions.
Python uses its AST. JS/TS and shell use deliberately narrow declaration
forms, not a syntax-validity proof. Unsupported forms need a gate path.
"""
if suffix == ".py":
try:
return {node.name for node in ast.walk(ast.parse(source))
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))}
except (SyntaxError, ValueError):
return set()
names = set()
if suffix in {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}:
# Replace comments and literals before matching declaration shapes.
# Keep literal positions separately for direct test registrations.
literals = {}
token = re.compile(r"//[^\n]*|/\*[\s\S]*?\*/|(['\"`])(?:\\[\s\S]|(?!\1)[^\\])*?\1")
def mask(match):
value = match.group()
if value[0] in "'\"`":
literals[match.start()] = (match.end(), value[1:-1])
return "".join("\n" if char == "\n" else " " for char in value)
clean = token.sub(mask, source)
names.update(re.findall(r"(?m)^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(", clean))
names.update(re.findall(r"(?m)^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\s*\(|\([^()]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)", clean))
for pos, (end, title) in literals.items():
# Only a literal first argument to a direct test registration.
if re.search(r"(?:^|[;{}\n])\s*(?:it|test)(?:\.(?:only))?\s*\(\s*$", clean[:pos]) and re.match(r"\s*,", clean[end:]):
if "\\" not in title and "${" not in title:
names.add(title)
elif suffix in {".sh", ".bash", ".bats"}:
# Heredoc bodies are shell data, not declarations. Prefer an explicit
# gate path for these files rather than interpreting their contents.
if "<<" in source:
return set()
try:
tokens = shlex.shlex(source, posix=False)
tokens.whitespace = " \t\r"
tokens.whitespace_split = False
tokens.commenters = "#"
words = list(tokens)
except ValueError:
return set()
for i, word in enumerate(words):
boundary = i == 0 or words[i - 1] in {"\n", ";", "}"}
if boundary and IDENTIFIER.fullmatch(word) and words[i + 1:i + 4] == ["(", ")", "{"]:
names.add(word)
if boundary and word == "function" and i + 2 < len(words):
if IDENTIFIER.fullmatch(words[i + 1]) and words[i + 2] == "{":
names.add(words[i + 1])
if boundary and words[i:i + 2] == ["@", "test"] and i + 3 < len(words) and words[i + 3] == "{":
title = words[i + 2]
if title.startswith(("'", '"')):
names.add(title[1:-1])
return names
def test_corpus(root: Path, files: list[str]) -> set[str]:
names = set()
for rel in files:
if not is_test_file(rel):
continue
path = root / rel
try:
path.resolve().relative_to(root.resolve())
if path.is_symlink() or path.stat().st_size > MAX_TEST_BYTES:
continue
names.update(test_names(path.read_text("utf-8", errors="replace"), path.suffix))
except (OSError, ValueError, RuntimeError):
continue
return names
def catcher_defined(name: str, corpus: set[str], files: set[str]) -> bool:
return name in files or name in corpus
def problems(text: str, label: str, layers: tuple[str, ...], corpus: set[str],
files: set[str], resolve) -> list[str]:
header, body = table(text)
if not header:
return [f"{label}: no incident table found"]
missing = [column for column in REQUIRED_COLUMNS if column not in header]
if missing:
return [f"{label}: table is missing column(s): {', '.join(missing)}"]
found: list[str] = []
seen: set[str] = set()
for number, cells in body:
where = f"{label}:{number}"
if len(cells) != len(header):
found.append(f"{where}: expected {len(header)} cells, found {len(cells)}")
continue
row = dict(zip(header, cells))
if row["ID"] in seen:
found.append(f"{where}: {row['ID']} repeats")
seen.add(row["ID"])
if "Reported" in row and not DATE.match(row["Reported"]):
found.append(f"{where}: Reported is not a date: {row['Reported']}")
for column in ("Fixed", "Introduced"):
if column not in row or (column == "Introduced" and row[column] == "unknown"):
continue
value = row[column]
if not SHA.match(value):
found.append(f"{where}: {column} is not a commit: {value}")
elif resolve is not None and not resolve(value):
found.append(f"{where}: {column} {value} does not resolve")
if row["Layer"] not in layers:
found.append(f"{where}: layer {row['Layer']} is not one of {', '.join(layers)}")
catchers = re.findall(r"`([^`]+)`", row["Caught by"])
if not catchers:
found.append(f"{where}: no catcher named")
found.extend(
f"{where}: catcher `{name}` is not an existing workspace file or a recognized test declaration (use an explicit gate path for unsupported forms)"
for name in catchers
if not catcher_defined(name, corpus, files)
)
return found
def plan(root: Path, text: str) -> list[str]:
header, body = table(text)
if "Fixed" not in header:
return []
lines = []
for _, cells in body:
if len(cells) != len(header):
continue
row = dict(zip(header, cells))
out = git(root, "show", "--name-only", "--format=", row["Fixed"])
touched = " ".join(p for p in out.stdout.split() if p) if out.returncode == 0 else "(unresolved)"
lines.append(f"{row['ID']}: review {row['Fixed']}^ files: {touched}")
return lines
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(description="Validate an incident ledger.")
parser.add_argument("--root", default=".", help="project root (default: .)")
parser.add_argument("--ledger", default=DEFAULT_LEDGER, help=f"ledger path (default: {DEFAULT_LEDGER})")
parser.add_argument("--layers", help="comma-separated allowed layers; overrides the ledger's Layers line")
parser.add_argument("--plan", action="store_true", help="print the replay plan after validating")
args = parser.parse_args(argv)
root = Path(args.root).resolve()
ledger = (root / args.ledger) if not Path(args.ledger).is_absolute() else Path(args.ledger)
if not ledger.is_file():
print(f"incident ledger not found: {args.ledger}", file=sys.stderr)
return 2
text = ledger.read_text("utf-8", errors="replace")
layers = tuple(x.strip() for x in args.layers.split(",") if x.strip()) if args.layers else declared_layers(text)
in_git = git(root, "rev-parse", "--git-dir").returncode == 0
shallow = in_git and git(root, "rev-parse", "--is-shallow-repository").stdout.strip() == "true"
resolve = None
if in_git and not shallow:
def resolve(sha: str) -> bool:
return git(root, "cat-file", "-e", sha + "^{commit}").returncode == 0
files = tracked_files(root)
try:
label = ledger.resolve().relative_to(root).as_posix()
except ValueError:
label = str(ledger)
found = problems(text, label, layers, test_corpus(root, files), set(files), resolve)
for line in found:
print(line)
if args.plan and not found:
for line in plan(root, text):
print(line)
count = len(table(text)[1])
note = "" if resolve else (", commits shape-checked only (shallow clone)" if shallow else ", commits shape-checked only (not a git repository)")
print(f"incident-ledger: {count} rows, {len(found)} problems{note}", file=sys.stderr)
return 1 if found else 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
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 diffs, PRs, release readiness, and publishing follow-through. Use when asked to review, triage issues or PRs, or ship. Not for debugging root causes or prose."
when_to_use: "review, 看看代码, 检查一下, 合并前, 看看issue, 看看PR, release, publish, push, 发布, 提交, close issue, 关闭issue, release reaction, 发布表情, before merge, 值得发布, code review, code-review, audit, 项目体检, 项目评分, scorecard"
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 under explicit repair authorization still in force for the same task. 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 authorization from the current request, or from an authorization still in force for the same unfinished task: one that named this action and goal and has not been withdrawn. A later turn that asks for progress or says "continue" does not revoke it and does not have to repeat it. A new action, a wider scope, a changed goal, or a risk the original authorization did not cover needs its own. Approval of a draft is not approval to commit, push, publish, or delete.
## 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 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`. Commit and push follow-through adds the HEAD re-read in `references/mode-ship.md`.
## 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", "批量处理", "all repos' issues", "其他仓库也扫一遍" | 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` |
| A review prompt, skill, persona, or model changed; "is review still catching bugs" | load `references/incident-ledger.md` (Replay eval) |
| Document, PDF, prose review | Delegate to `/write` Document Review Mode, keeping it in the same completion ledger; without `/write`, use the available prose capability and name the limitation |
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 never depends on private machine paths or unpublished instructions. Learn the project from the diff and its public context (manifests, CI workflows, AGENTS/CLAUDE files, release notes) until you know its verification commands, generated or protected files, release artifacts, and domain risks. The shape is in `references/project-context.md`.
- Where project context and this skill overlap and both can be met, satisfy the stricter one. An explicit project exception wins and the report names it; stricter is not a licence to overrule a written decision or reorder the host's instruction precedence.
- Prefer a verification command the project names, and read the workflow rather than the sentence describing it. A doc claiming CI enforces something is a claim; a check that runs on only one side (local or CI) means a green run there predicts nothing about the other.
For release readiness or publication, 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. A commit/push-only task uses the source-delivery checks in `references/mode-ship.md`; unrelated release assets and registry state are out of scope.
## Plan Execution Mode
Activate when the message starts with "Implement the following plan", "按计划实施", "按照计划", "整", "可以干", or "直接改" followed by a plan body, or links to a `/think` output. Skip the code review: name the plan, check `git status --short --branch -uall` for drift that makes it unsafe (name the conflict and stop), execute, then run the project's verification command.
Review-then-ship: when `AGENTS.md` or the current thread asks to "commit after review", "ship if green", or equivalent, load `references/mode-ship.md` after a clean review or finished plan, state "proceeding to ship", and do not ask again.
## 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**: resolve the latest stable from the delivery channel the product actually uses (feed, registry, store, or release host), map it to a source ref, and review through `HEAD`, then add dirty files. A stale release-host listing is not the baseline when another channel ships the product; a missing artifact-to-ref mapping stays an explicit gap.
- **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. For a large inventory, reconcile the union of reviewed paths against that frozen set, including root files and cross-domain consumers; assignment alone is not coverage. Repeated locale or generated files may share a semantic check only when it accounts for every member, otherwise name the sampled and unread remainder separately. 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.
Depth measures size; risk tier follows paths. A diff touching CI workflows, release or signing, the updater, auth, billing or license, installers, privilege, or deletion paths gets the project's heaviest gate and a human maintainer's review whatever its size or author; an agent's, bot's, or automated reviewer's approval never replaces that review. The method is the Risk tiers section of `references/project-context.md`.
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**.
Every changed file and new public surface must trace to the stated goal in one sentence; a file, dependency, config knob, abstraction, generated artifact, workflow permission, or release behavior that cannot is drift until proven necessary. Any one of these is enough: a file unrelated to the goal, pure refactoring or unrelated deletion inside a fix or feature, an unmentioned dependency or helper, or a cleanup that quietly adds user-visible UI, default config, workflow permissions, or release behavior. For every new public setting, flag, environment variable, command, or service, ask who will change it and why one correct default cannot serve them; with no evidenced user split, treat the knob as drift and fix the default path instead.
Every review, unasked, runs an over-design pass: for each new file, option, fallback, layer, or abstraction, ask whether the change would still hold with it removed, and report each one that would as removable.
## 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, grep the repo for the same shape (hunt's Scope Blast Mode) 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.
## Command And Install Surfaces
When a diff touches a CLI entrypoint, installer, config/env handling, or a mutating command (cleanup, update, uninstall, migration, cache removal), work the CLI Command Surface section of `references/release-surfaces.md`; when it touches a skill, plugin, marketplace entry, package manifest, generated mirror, or published archive, work its Packaged Install Surface section. The lesson behind both: library tests and manifest JSON never prove installed-runtime behavior, and every mutating command is a safety sink. Terminal output is a rendered surface too; after changing CLI-facing text or layout, re-run the command and read the real output.
## 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, or be labelled as reused evidence naming the commit and inputs it came from and what was re-checked to confirm it still applies. Reuse the project allows is fine; presenting it as this session's run is not.
- **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**: the body follows `/write` Public Reply Mode; posting, editing, read-back, and closure follow `references/public-reply.md`.
## 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. For lockfiles, automated security PRs, or version pins, load `references/review-patterns.md` (Dependency changes).
- **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
Every finding needs the exact file:line, the specific input or state that triggers the bad outcome, a read of upstream callers and downstream consumers rather than the function in isolation, and a severity a senior reviewer would raise at that level in a real PR; missing any of these, drop it or downgrade it to advisory. Before calling a change a regression, read its commit message and author intent: a deliberate maintainer change is a decision, not a finding. 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** also need why existing guards (validation, type system, upstream catch, framework default) do not already prevent it. Cannot show that? Downgrade to MEDIUM, or drop. "This *might* break under some condition" is not a HIGH.
**Report shape.** Group findings by bug class, highest severity first, each class listing every same-class site with its file:line, then give the total; with none, write `no issues found` against the stated surface. Issues the diff did not introduce go in a separate `pre-existing` list and do not count toward the verdict.
**Rounds converge.** One task gets at most three review/fix rounds. From round two, review only the new diff plus the re-checks Autofix Routing requires, and never re-report a finding already reported. A third round that still finds new problems stops the patch loop: return to [Question the Approach](#question-the-approach-not-just-the-diff). A class that comes back in a second round or a second review is fixed at its source (lint, source gate, type, routed rule), not site by site.
## Knowledge Sync
If the diff introduces an invariant the project docs do not hold yet (a safety gate, a UI constraint, a release step, a cross-file sync requirement), it belongs in the narrowest tracked guidance every intended runtime reads, usually AGENTS.md. A review report or scorecard in the diff is evidence, not a durable doc: extract its stable rule instead of committing the snapshot. Apply a clear doc update as `safe_auto` or flag it as `doc debt`; with nothing new, sign-off says `doc debt: none`.
## Specialist Review (Standard and Deep only)
Load `references/persona-catalog.md` for activation, parallel or sequential review, and finding verification.
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)
Run the four attack angles in `references/persona-catalog.md`. Convergence from independent angles raises confidence; singleton findings face the same per-finding skeptic verification as specialist claims. Suppress findings below 0.60 confidence.
## Platform Operations
Derive the host CLI/API from public project docs; do not force GitHub commands onto other hosts, and confirm CI passes before merging. 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, run red on that code (hunt's Regression guard rule). When the defect reached users or a published build and the project keeps an incident ledger, the same change adds its row; load `references/incident-ledger.md`. Establish expected behavior independently of the implementation: updating a snapshot does not prove it is correct. For agent instructions, keyword checks prove text retention only; behavior checks inspect tool actions and resulting files or artifacts, including forbidden side effects. Exercise a known-good and a known-bad case before relying on a new checker or a changed reviewer, and compare baseline and candidate under the same runtime and inputs; for a reviewer, the incident ledger's pre-fix code is the known-bad set and known-clean changes measure its noise.
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.
## 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] / removable: [what]
user-visible delta: none / [entry, UI, copy, behavior added, removed, or changed]
review depth: quick / standard / deep
risk tier: [tier]: [gates], human review done / pending (only when a tier applies)
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.