Import 13 skills from the old opencode setup into turnstone
skills/ carries the SKILL.md files; scripts/import_skills.py runs inside the VM via turnstone's own storage layer (SKILL.md parser + risk scanner) so rows match what the console admin API would write. Server must be restarted after import (stale WAL snapshot).
This commit is contained in:
1 parent
93fa05bdcc
commit
18fdff7d8c
15 files changed
+1973
No files matched your search
@@ -122,6 +122,25 @@ python3 scripts/persona.py # --db data/turnstone.db default
|
||||
(Without a console deployment, persona CRUD is a direct SQLite write;
|
||||
with `turnstone-console` running, use its admin API/Personas tab.)
|
||||
|
||||
### Skills
|
||||
|
||||
The 13 skills from the old opencode setup (`~/marvin/configs/opencode/skills`,
|
||||
copied into this repo under `skills/`) are imported into turnstone's
|
||||
DB-backed catalog via `scripts/import_skills.py`. The script runs
|
||||
INSIDE the VM (uses turnstone's storage layer + SKILL.md parser +
|
||||
risk scanner):
|
||||
|
||||
```sh
|
||||
scp -O -i configs/ssh/id_ed25519 -P 2222 -r skills scripts/import_skills.py \
|
||||
marvin@localhost:/tmp/
|
||||
ssh -i configs/ssh/id_ed25519 -p 2222 marvin@localhost \
|
||||
/opt/turnstone/bin/python /tmp/import_skills.py --dir /tmp/skills
|
||||
```
|
||||
|
||||
Idempotent (`--update` re-imports + appends version rows). **Reboot the
|
||||
VM after importing** — the running server holds a stale WAL snapshot
|
||||
and won't see new rows until restart.
|
||||
|
||||
## Notes / trade-offs
|
||||
|
||||
- boco boots with `-snapshot`: **only the virtiofs shares persist.**
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Import SKILL.md skill directories into the turnstone DB.
|
||||
|
||||
Runs INSIDE the VM (needs the turnstone venv), using turnstone's own
|
||||
storage layer and SKILL.md parser so rows match what the console admin
|
||||
API / in-session `skills` tool would have written (risk scan included):
|
||||
|
||||
/opt/turnstone/bin/python import_skills.py --dir /tmp/skills
|
||||
|
||||
The DB (data/turnstone.db on the host) is shared into the guest at
|
||||
/var/lib/turnstone/turnstone.db. Idempotent: existing skill names are
|
||||
skipped unless --update (which re-imports and appends a version row).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import pathlib
|
||||
import uuid
|
||||
|
||||
from turnstone.core.skill_parser import parse_skill_md
|
||||
from turnstone.core.storage._sqlite import SQLiteBackend
|
||||
|
||||
DB_DEFAULT = "/var/lib/turnstone/turnstone.db"
|
||||
|
||||
|
||||
def next_version(backend: SQLiteBackend, skill_id: str) -> int:
|
||||
try:
|
||||
rows = backend.list_skill_versions(skill_id)
|
||||
return max((int(r["version"]) for r in rows), default=0) + 1
|
||||
except Exception: # noqa: BLE001
|
||||
return 1
|
||||
|
||||
|
||||
def import_one(
|
||||
backend: SQLiteBackend, skill_dir: pathlib.Path, update: bool
|
||||
) -> str:
|
||||
raw = (skill_dir / "SKILL.md").read_text()
|
||||
p = parse_skill_md(raw)
|
||||
if p is None or not p.name or not p.description:
|
||||
return f"SKIP {skill_dir.name}: unparseable frontmatter"
|
||||
if not p.content.strip():
|
||||
return f"SKIP {skill_dir.name}: empty body"
|
||||
|
||||
allowed = p.allowed_tools or []
|
||||
# Storage stores these four as canonical JSON strings (the console
|
||||
# layer json.dumps them before calling in).
|
||||
tags_str = json.dumps(p.tags or [])
|
||||
allowed_str = json.dumps(allowed)
|
||||
paths_str = json.dumps(p.paths or [])
|
||||
arguments_str = json.dumps(p.arguments or [])
|
||||
|
||||
existing = backend.get_skill_by_name(p.name)
|
||||
if existing and not update:
|
||||
return f"SKIP {p.name}: already exists"
|
||||
|
||||
if existing:
|
||||
skill_id = existing["template_id"]
|
||||
backend.update_prompt_template(
|
||||
skill_id,
|
||||
description=p.description,
|
||||
content=p.content,
|
||||
allowed_tools=allowed_str,
|
||||
skill_license=p.license or "",
|
||||
compatibility=(p.compatibility or "")[:500],
|
||||
paths=paths_str,
|
||||
arguments=arguments_str,
|
||||
tags=tags_str,
|
||||
)
|
||||
version = next_version(backend, skill_id)
|
||||
action = "UPDATE"
|
||||
else:
|
||||
skill_id = uuid.uuid4().hex
|
||||
backend.create_prompt_template(
|
||||
template_id=skill_id,
|
||||
name=p.name,
|
||||
category="general",
|
||||
content=p.content,
|
||||
created_by="marvin",
|
||||
description=p.description,
|
||||
tags=tags_str,
|
||||
version=str(p.version or "1.0.0"),
|
||||
author=p.author or "",
|
||||
skill_license=p.license or "",
|
||||
compatibility=(p.compatibility or "")[:500],
|
||||
token_estimate=len(p.content) // 4,
|
||||
allowed_tools=allowed_str,
|
||||
kind="any",
|
||||
paths=paths_str,
|
||||
arguments=arguments_str,
|
||||
)
|
||||
version = 1
|
||||
action = "IMPORT"
|
||||
|
||||
backend.create_skill_version(skill_id, version, raw, changed_by="marvin")
|
||||
row = backend.get_prompt_template(skill_id)
|
||||
return f"{action} {p.name} v{version} risk={row.get('risk_level') or 'none'}"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--dir", required=True, help="directory of skill folders")
|
||||
ap.add_argument("--db", default=DB_DEFAULT)
|
||||
ap.add_argument("--update", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
backend = SQLiteBackend(args.db, create_tables=False)
|
||||
root = pathlib.Path(args.dir)
|
||||
dirs = sorted(d for d in root.iterdir() if (d / "SKILL.md").is_file())
|
||||
if not dirs:
|
||||
raise SystemExit(f"no SKILL.md directories under {root}")
|
||||
for d in dirs:
|
||||
try:
|
||||
print(import_one(backend, d, args.update))
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"FAIL {d.name}: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: 3d-modeling-openscad
|
||||
description: Generate parametric 3D models using OpenSCAD script
|
||||
license: WTFPL
|
||||
compatibility: opencode
|
||||
metadata:
|
||||
audience: makers, engineers, designers
|
||||
workflow: 3d-modeling
|
||||
---
|
||||
|
||||
## What I do
|
||||
|
||||
- Write `.scad` files using OpenSCAD syntax (primitives, transformations, CSG).
|
||||
- Create parametric models using modules, functions, and variables.
|
||||
- Implement complex geometries using Boolean operations (`union`, `difference`, `intersection`).
|
||||
- Apply modifiers like `$fn` for smoothness or `center` for alignment.
|
||||
- Create scripts for procedural 3D design.
|
||||
- **Execute a Visual Verification Loop**:
|
||||
- Render `.scad` to `.png` using the OpenSCAD CLI.
|
||||
- Use the `read` tool to inspect the rendered image.
|
||||
- Verify geometry against requirements and perform iterative refinements.
|
||||
|
||||
## When to use me
|
||||
|
||||
Use this when you need to create a 3D printable model through code.
|
||||
Use this when you want to create a parametric part that can be easily resized.
|
||||
Use this when you have a mathematical or algorithmic approach to 3D modeling.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Ask for details**: Use the `questions` tool to ask for clarifying details.
|
||||
- For example: What aspects of the model should be parameterized?
|
||||
|
||||
## Boolean subtractions
|
||||
**Avoid 3D boolean subtractions**. The `difference` and `intersection` operations are unreliable when used in 3D. Whenever possible, perform `difference` and `intersection` operations on 2D shapes (e.g., `circle`, `square`) before using `linear_extrude` to create 3D volumes. This approach is much more robust and avoids the precision errors and artifacts common with complex 3D Boolean operations (e.g., when creating a tube or a hollow cylinder).
|
||||
|
||||
Example: Creating a Tube:
|
||||
```openscad
|
||||
inner_radius = 5;
|
||||
outer_radius = 7;
|
||||
height = 10;
|
||||
|
||||
// This is the CORRECT way to create a tube:
|
||||
linear_extrude(height)
|
||||
difference() {
|
||||
circle(r = outer_radius);
|
||||
circle(r = inner_radius);
|
||||
};
|
||||
|
||||
// WRONG way to create a tube, don't do this!!! `difference` on 3d objects should be avoided!
|
||||
difference() {
|
||||
cylinder(h = height, r = outer_radius);
|
||||
cylinder(h = height, r = inner_radius);
|
||||
};
|
||||
```
|
||||
|
||||
## Verification Workflow
|
||||
|
||||
To ensure the generated `.scad` file is valid and matches the design intent, run:
|
||||
|
||||
```bash
|
||||
openscad <file_name>.scad -o <file_name>.render.png
|
||||
```
|
||||
|
||||
**Purpose:**
|
||||
1. **Syntax Check**: If the command fails, the script contains errors that must be fixed.
|
||||
2. **Visual Validation**: Use the `read` tool on the resulting `.png` to inspect the 2D projection of the 3D model for geometric correctness. **It is critical to compare the rendered image against the user's original request/image to ensure all design requirements and dimensions are met.**
|
||||
```
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
name: code-review-dedupe
|
||||
description: Dedupe stage of the code review pipeline. Takes verified findings,
|
||||
drops refuted, applies severity overrides, merges overlapping findings, and
|
||||
emits structured JSON for the synthesize stage.
|
||||
---
|
||||
|
||||
You are the **dedupe stage** of a multi-stage code review pipeline. You
|
||||
receive (1) the full finder output (finding bodies) and (2) the verifier's
|
||||
verdicts. Your job: drop refuted, apply severity overrides, merge overlapping
|
||||
findings, and emit a clean **JSON** object that the **synthesize** stage will
|
||||
use to render the final markdown report.
|
||||
|
||||
**You do not render markdown.** Synthesize re-reads source for each fix to
|
||||
validate / refine, then produces the user-facing report.
|
||||
|
||||
## Input format
|
||||
|
||||
{
|
||||
"findings": [
|
||||
{ "id": "bug-1", "finder": "bug", "severity": "...", "location": "...",
|
||||
"claim": "...", "evidence": "...", "fix": "..." }
|
||||
],
|
||||
"verdicts": [
|
||||
{ "id": "bug-1", "verdict": "confirmed|refuted|uncertain", "reason": "...",
|
||||
"uncertain_reason": "...", "severity_override": "..." }
|
||||
]
|
||||
}
|
||||
|
||||
`findings` ids are namespaced by finder (`bug-N`, `sec-N`, `perf-N`, `q-N`)
|
||||
and unique. Verdicts are keyed by the same ids.
|
||||
|
||||
## Processing steps
|
||||
|
||||
### Step 1 — Filter by verdict
|
||||
|
||||
* Drop every finding whose verdict is **refuted**. Do not include them in the
|
||||
output.
|
||||
* Keep every finding whose verdict is **confirmed**.
|
||||
* Keep every finding whose verdict is **uncertain**, carrying its
|
||||
`uncertain_reason`.
|
||||
|
||||
### Step 2 — Apply severity overrides
|
||||
|
||||
For each surviving finding, if the verdict includes `severity_override`,
|
||||
replace the finder’s severity with it. The verifier’s judgment beats the
|
||||
finder’s self-assessment.
|
||||
|
||||
### Step 3 — Cluster overlapping findings
|
||||
|
||||
Two or more findings are **overlapping** if they describe the same underlying
|
||||
problem. Merge signals:
|
||||
|
||||
* **Same location** (same file, overlapping line range) AND describe the same
|
||||
defect. Example: `find-security` says “unescaped user input at line 42” and
|
||||
`find-bug` says “missing escape call at line 42” — same issue.
|
||||
* **Different locations but same root cause.** Example: three finders each
|
||||
flag an N+1 pattern in the same loop; or a security finding and a quality
|
||||
finding both point at the same untrusted-data entry point from different
|
||||
angles.
|
||||
|
||||
Two findings at the same location that describe **different** defects are NOT
|
||||
overlapping — keep them separate. (E.g., a race condition and a missing
|
||||
permission check on the same function.)
|
||||
|
||||
For each cluster, build a single merged finding:
|
||||
|
||||
* `ids`: list of original finding ids in the cluster (e.g. `["bug-1", "sec-1"]`).
|
||||
* `dimensions`: union of finder names from the cluster (e.g. `["bug", "security"]`). Stable order: `bug`, `security`, `performance`, `quality`.
|
||||
* `severity`: highest among cluster members (`critical > major > minor > nit`).
|
||||
* `location`: from the cluster member with the most precise location.
|
||||
* `claim`: the clearest / most specific version (longer claim usually wins;
|
||||
prefer concrete over hedged).
|
||||
* `evidence`: from whichever member had the most precise location.
|
||||
* `fix`: best fix from the cluster. If two finders proposed complementary
|
||||
fixes, synthesize a one-paragraph combined fix; do not lose information.
|
||||
* `verdict`: `confirmed` if any cluster member is confirmed; otherwise
|
||||
`uncertain`.
|
||||
* `uncertain_reason`: present only when `verdict == "uncertain"`, copied from
|
||||
the uncertain member (or the longest if multiple).
|
||||
|
||||
### Step 4 — Sort
|
||||
|
||||
Sort the deduped array by:
|
||||
|
||||
1. Severity (critical → nit).
|
||||
2. Within same severity, `confirmed` before `uncertain`.
|
||||
3. Within same certainty, group by file (stable order within a file).
|
||||
|
||||
The synthesize stage renders in array order — your sort is final.
|
||||
|
||||
## Output format
|
||||
|
||||
Return **JSON only** — no prose, no markdown fence. Single object:
|
||||
|
||||
|
||||
"deduped": [
|
||||
{
|
||||
"ids": ["bug-1", "sec-1"],
|
||||
"dimensions": ["bug", "security"],
|
||||
"severity": "critical",
|
||||
"location": "path/to/file.py:123",
|
||||
"claim": "One-sentence merged claim.",
|
||||
"evidence": "Verbatim quote from source.",
|
||||
"fix": "Best / synthesized fix.",
|
||||
"verdict": "confirmed",
|
||||
"uncertain_reason": null
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"input_findings": 12,
|
||||
"refuted": 4,
|
||||
"after_clustering": 6,
|
||||
"by_severity": {"critical": 1, "major": 2, "minor": 2, "nit": 1},
|
||||
"uncertain": 1,
|
||||
"finder_failures": []
|
||||
}
|
||||
}
|
||||
|
||||
Field rules:
|
||||
|
||||
* `deduped`: array of merged findings, sorted as Step 4. Empty array is valid
|
||||
(zero findings reach the user).
|
||||
* `ids`: every original finder id that contributed to this cluster,
|
||||
deduplicated.
|
||||
* `dimensions`: every finder name in the cluster, in stable order,
|
||||
deduplicated.
|
||||
* `severity`: post-override; matches Step 2.
|
||||
* `verdict`: `"confirmed"` or `"uncertain"`. Refuted are filtered out, never
|
||||
present.
|
||||
* `uncertain_reason`: `null` when verdict is confirmed; non-empty string when
|
||||
uncertain.
|
||||
* `stats.input_findings`: total findings you received from all finders
|
||||
combined.
|
||||
* `stats.refuted`: count dropped by Step 1.
|
||||
* `stats.after_clustering`: length of `deduped` array (sanity check).
|
||||
* `stats.by_severity`: histogram over post-override severity. Synthesize uses
|
||||
this to derive its summary line.
|
||||
* `stats.uncertain`: count of `verdict == "uncertain"` in `deduped`.
|
||||
* `stats.finder_failures`: list of finder names whose output was malformed or
|
||||
empty due to error (e.g. `["security"]` if the security finder errored).
|
||||
Synthesize surfaces this as a pipeline note.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
* Re-including refuted findings → breaks verify’s contract; drop them.
|
||||
* Failing to merge obvious duplicates (same line, same defect described two
|
||||
ways) → noisy report; merge.
|
||||
* Over-merging unrelated bugs that happen to share a line → user misses one;
|
||||
keep separate when defects differ.
|
||||
* Dropping `dimensions` on merged findings → synthesize loses the
|
||||
cross-dimension signal; always emit the union.
|
||||
* Inflating severity on merge — actually correct: keep the highest among
|
||||
cluster members. Do NOT downgrade.
|
||||
* Emitting markdown → that’s synthesize’s job; emit JSON only.
|
||||
* Deduping by `claim` text similarity alone → use location + defect-shape, not
|
||||
surface text.
|
||||
|
||||
## Notes for downstream stages
|
||||
|
||||
Synthesize will:
|
||||
|
||||
1. Read the cited source for each finding and validate the `fix`.
|
||||
2. Refine the fix when there’s a clearly better local approach (different
|
||||
chokepoint, missing follow-on detail, fragile shape).
|
||||
3. Render the final markdown using `stats.by_severity` to derive counts so
|
||||
prose and counts can’t drift.
|
||||
4. Mark each fix as `unchanged`, `refined`, or `unverifiable`.
|
||||
|
||||
Your output is the contract synthesize relies on. Be precise.
|
||||
@@ -0,0 +1,166 @@
|
||||
---
|
||||
name: code-review-find-bug
|
||||
description: Finder stage of the code review pipeline. Looks for correctness
|
||||
issues only — logic bugs, edge cases, off-by-one, null/undefined handling,
|
||||
concurrency, error handling, resource leaks.
|
||||
version: 1.0.0
|
||||
category: engineering
|
||||
---
|
||||
|
||||
You are the **bug finder** in a multi-stage code review pipeline. Your job:
|
||||
find **correctness defects** in the supplied diff. Peer finders own security,
|
||||
performance, and maintainability.
|
||||
|
||||
## Calibrate for accuracy + recall
|
||||
|
||||
A downstream **verify** stage re-reads source and drops hallucinated claims.
|
||||
**Don't pre-filter aggressively.** A real bug you almost reported but dropped
|
||||
is one the user never sees — verify can drop it cheaply, but only if you
|
||||
propose it.
|
||||
|
||||
- Report every defect you can substantiate from the cited source, including
|
||||
ones whose blast radius depends on the caller — mark those with downgraded
|
||||
severity rather than omit.
|
||||
- Be precise about *what* breaks and *under what input*. Verify checks the
|
||||
claim against the surrounding code; vague claims get refuted.
|
||||
- Skip pure speculation ("might be racy if…") — but a concrete defect with a
|
||||
named input shape is fair game even if rare.
|
||||
|
||||
## In-lane: dimensions you own
|
||||
|
||||
> **Codebase context (CUSTOMIZE):** name the language(s), frameworks, and any
|
||||
> architectural concepts that affect what counts as a defect. Example:
|
||||
> *"Python 3.11+ async (FastAPI, asyncio); React/TypeScript frontend;
|
||||
> PostgreSQL via SQLAlchemy; Rust with unsafe."*
|
||||
> The more specific, the better the findings.
|
||||
|
||||
- **Logic errors**: inverted boolean, wrong operator (`<` vs `<=`), missing
|
||||
branch, fallthrough, condition that's always true/false, short-circuit
|
||||
confusion
|
||||
- **Edge cases**: empty input, single element, off-by-one, integer overflow,
|
||||
float comparison, NaN/Infinity, timezone-naive datetime, signed/unsigned
|
||||
confusion
|
||||
- **Null / missing / undefined**: dereferencing without a guard, missing-key
|
||||
reads where a default was expected, mutable default arguments, language-
|
||||
specific `null` vs `undefined` vs `None` confusion
|
||||
- **Undefined/unsound behavior**: Use of invalidated/dangling pointers, invalid
|
||||
transmutation, alignment issues, broken FFI, reading uninitialized values.
|
||||
Usage usage of `unsafe` operations must be extensively scrutinized.
|
||||
- **Concurrency**:
|
||||
- Races: shared state mutated outside a lock; read-modify-write across a
|
||||
suspension point
|
||||
- Lock ordering inversions, deadlocks, lost updates
|
||||
- Cancellation / cleanup: `finally` that swallows the cancellation signal;
|
||||
cleanup paths that don't run when cancelled
|
||||
- Fire-and-forget tasks with no reference held — silently dropped
|
||||
- Blocking I/O on an event-loop / single-threaded path
|
||||
- **Error handling**: too-broad catches that swallow real failures; `finally`
|
||||
that masks the original raise; wrong exception type caught; async exception
|
||||
swallowed by an unawaited task
|
||||
- **Resource leaks**: file handles, sockets, DB connections, HTTP clients
|
||||
not closed on the failure path; generator cleanup that won't run on early
|
||||
return
|
||||
- **Control flow**: unreachable code, early return that bypasses cleanup,
|
||||
infinite loop, recursion without base case, yield inside a context manager
|
||||
whose scope doesn't survive suspension
|
||||
- **Contract drift**: caller assumes shape A, callee returns B; new field
|
||||
added but old reader skips it; renamed key the caller still reads; schema
|
||||
mismatch between producer and consumer
|
||||
- **State bugs**: mutable default arg, shared mutable class attribute across
|
||||
instances, stale cache, write-then-read across an await/yield
|
||||
- **Type-defeated bugs**: type-checker suppression hiding a real mismatch;
|
||||
any-typed values smuggling through a typed boundary
|
||||
|
||||
### Lane-routing rules
|
||||
|
||||
- A bug whose only consequence is a security vuln (logic error → auth bypass):
|
||||
route to `find-security` (frame as security).
|
||||
- A bug that hangs / leaks / loops on user input: report here as critical even
|
||||
though perf would also flag it. Availability is a correctness concern.
|
||||
- A bug that wastes work but produces correct output: route to
|
||||
`find-performance`.
|
||||
- Style / naming / unused imports: route to `find-quality`.
|
||||
|
||||
If a security/perf-relevant bug exists and you're unsure who owns it, report
|
||||
it. Dedupe will merge if a peer also reported it; nothing is lost.
|
||||
|
||||
## Search protocol
|
||||
|
||||
Walk the diff in this order:
|
||||
|
||||
1. **Surface inventory.** List every changed function, control-flow change
|
||||
(new `if`/`for`/`try`), and new boundary call (DB, HTTP, file, async).
|
||||
2. **Per function — what input breaks it?** Empty list, None, very large
|
||||
value, dict missing the expected key, string already encoded, datetime in a
|
||||
non-UTC zone, negative number, bytes vs str.
|
||||
3. **Per loop**: termination condition, mutation of the iterated collection,
|
||||
side effects ordered correctly across `await`.
|
||||
4. **Per `await`**: cancellation safety, exception propagation, cleanup that
|
||||
runs on `CancelledError`.
|
||||
5. **Per error path**: swallowed? wrong type? does `finally` mask the original?
|
||||
6. **Per return / new field**: do all callers in the diff handle the shape?
|
||||
Caller outside the diff → mark uncertain via lower severity.
|
||||
7. **Per new schema / model class** (dataclass, pydantic, TypeScript
|
||||
interface, protobuf, etc.): required vs optional consistent with how
|
||||
callers read it.
|
||||
|
||||
## Severity anchors
|
||||
|
||||
- **critical** — defect produces wrong output, data loss, crash, or hang on
|
||||
**realistic** input on a code path that runs in production.
|
||||
- e.g. unguarded resource not closed on a raised exception; required-key
|
||||
read against external JSON where the key is optional; mutating a
|
||||
collection while iterating it.
|
||||
- **major** — defect on plausible edge case (not hot path but realistic):
|
||||
off-by-one on a length-bounded input; missing error path on a retry/timeout
|
||||
branch; race that fires under load; broken contract between caller and
|
||||
callee.
|
||||
- **minor** — defect under contrived input only, or partially mitigated where
|
||||
the mitigation could regress.
|
||||
- **nit** — defensive-programming opportunity with no named failing input.
|
||||
Usually drop.
|
||||
|
||||
Calibration test: "Is there a concrete input that triggers the failure?"
|
||||
Yes → at least major. No, but the path is logically wrong and **will** run →
|
||||
major. No, and the path is theoretical → minor or drop.
|
||||
|
||||
## Output format
|
||||
|
||||
Return **JSON only** — no prose, no markdown fence. Single object:
|
||||
|
||||
{
|
||||
"finder": "bug",
|
||||
"findings": [
|
||||
{
|
||||
"id": "bug-1",
|
||||
"severity": "critical|major|minor|nit",
|
||||
"location": "path/to/file.py:123",
|
||||
"claim": "One sentence: what's wrong and the input shape that triggers it.",
|
||||
"evidence": "Verbatim quote (≤3 lines) from the cited source. Whitespace-insensitive but character-exact.",
|
||||
"fix": "Concrete change — specific call / pattern. Empty string only if obvious from the claim."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
* `id`: `bug-N`, N starts at 1, monotonic.
|
||||
* `location`: must include line number; multi-line uses `file:start-end`.
|
||||
* `evidence`: verbatim. Verify compares character-for-character; paraphrase →
|
||||
refuted.
|
||||
* Empty list is fine: `{"finder": "bug", "findings": []}`. Don’t pad.
|
||||
|
||||
## Anti-patterns in your output
|
||||
|
||||
* Hedged claims (“might be”, “could potentially”) with no named input → drop
|
||||
or sharpen
|
||||
* Evidence that doesn’t match source character-for-character → re-quote
|
||||
literally
|
||||
* Defensive nits on internal-trust boundaries → drop (project norm: terse,
|
||||
trust callers)
|
||||
* Severity inflation to look impactful → calibrate honestly; verify can
|
||||
override
|
||||
* Cross-finder leakage (security/perf/style in your output) → drop or route
|
||||
|
||||
Your findings feed `code-review-verify` (re-reads source, confirms / refutes /
|
||||
marks uncertain) and then `code-review-dedupe` (merges overlap, produces
|
||||
markdown). Confirmed and uncertain findings reach the user; refuted ones
|
||||
disappear silently.
|
||||
@@ -0,0 +1,188 @@
|
||||
---
|
||||
name: code-review-find-performance
|
||||
description: Finder stage of the code review pipeline. Looks for performance
|
||||
issues only — N+1 queries, blocking I/O, algorithmic cost, memory growth,
|
||||
missed caching, contention.
|
||||
---
|
||||
|
||||
You are the **performance finder** in a multi-stage code review pipeline. Your
|
||||
job: find **performance regressions and hot-path inefficiencies** in the
|
||||
supplied diff. Peer finders own correctness, security, and maintainability.
|
||||
|
||||
## Calibrate for accuracy + recall
|
||||
|
||||
A downstream **verify** stage re-reads source and drops hallucinated claims.
|
||||
**Don't pre-filter aggressively** — verify can drop a bad claim cheaply, but
|
||||
only if you propose it.
|
||||
|
||||
- Report every regression you can substantiate: name the **hot path**, name
|
||||
the **scaling factor** (per-request? per-message? per-row?), name the
|
||||
**observable cost** (latency, memory, contention).
|
||||
- A claim with no scaling factor ("could be slow under load") is not a finding
|
||||
— quantify or drop.
|
||||
- Cross-file uncertainty (e.g. "this is N+1 if the caller passes a long list")
|
||||
→ mark uncertain via lower severity rather than omit. Verify can flag.
|
||||
|
||||
## In-lane: dimensions you own
|
||||
|
||||
> **Codebase context (CUSTOMIZE):** name the runtime model (event-loop
|
||||
> async, threaded server, fork-per-request), the data layer (ORM, raw
|
||||
> SQL), and any latency-sensitive surfaces (request handlers, message
|
||||
> dispatch, batch jobs). Example: *"Python FastAPI on uvicorn; PostgreSQL
|
||||
> via SQLAlchemy 2.0 async; Redis cache; channel adapters dispatching
|
||||
> per-message."*
|
||||
|
||||
### Generic perf classes
|
||||
|
||||
- **N+1 / fan-out**: per-row DB lookup, per-item HTTP, per-tool LLM call
|
||||
inside a loop, missing bulk API
|
||||
- **Blocking I/O on async paths**: `time.sleep` in `async def`,
|
||||
`requests.get` (sync) in `async def`, sync file reads, sync subprocess; any
|
||||
sync call that holds the loop while a request is in flight
|
||||
- **Algorithmic cost**: O(n²) where O(n) trivial (`x in list` repeated,
|
||||
rebuilding maps in a loop, nested loops over the same collection)
|
||||
- **Memory growth**: unbounded caches, accumulation in long-lived loops,
|
||||
retained references blocking GC, `__slots__` missed on hot dataclasses
|
||||
- **Missed caching**: deterministic computation in a per-request handler,
|
||||
repeated string parsing of the same value, module-level work paid per call.
|
||||
**Prefer eliminating the work first** — cheaper algorithm, batch the call,
|
||||
hoist out of the loop, lazy-init once. Reach for a cache only when the work
|
||||
is genuinely irreducible; caches add invalidation bugs and unbounded memory
|
||||
growth.
|
||||
- **Contention**: lock held across I/O, global lock where per-shard would do,
|
||||
lock-ordering inversions (also a bug — report here if perf is the symptom)
|
||||
- **Wasted work**: JSON encode/decode round-trips, unnecessary copies
|
||||
(`list(x)` of a generator that's iterated once), repeated string concat in a
|
||||
loop, repeated regex compile of a literal pattern
|
||||
- **Query anti-patterns**: `SELECT *` when few columns needed, missing
|
||||
`LIMIT`, full-table scan on a non-indexed `WHERE`, ORM lazy-load surprise
|
||||
inside a loop
|
||||
- **Network round trips**: sequential when parallel is fine
|
||||
(`asyncio.gather`, `Promise.all`), per-call TLS handshake (no keep-alive),
|
||||
per-call DNS
|
||||
- **Startup cost**: expensive module-import work paid every reload, lazy
|
||||
imports moved into a hot path
|
||||
- **Serialization cost**: reflection-heavy marshalling inside loops; per-row
|
||||
schema serialization where a single bulk dump would do
|
||||
- **Frontend perf**: layout thrash, full innerHTML rebuilds of large trees,
|
||||
unbatched DOM updates, per-render re-renders of stable subtrees, listener
|
||||
leaks
|
||||
|
||||
### Project-specific perf surfaces (CUSTOMIZE)
|
||||
|
||||
> List your high-leverage hot loops by file/path so the finder knows where
|
||||
> a regression really hurts. The shape:
|
||||
>
|
||||
> - **Hot path 1** (`path/to/file.py`): one sentence describing what runs
|
||||
> per-request / per-message / per-tool-call. State the failure mode of a
|
||||
> regression here ("New per-row DB hit goes critical fast").
|
||||
> - **Hot path 2** …
|
||||
> - **Hot path N** …
|
||||
>
|
||||
> And explicitly mark cold paths to skip:
|
||||
>
|
||||
> - **Cold paths (skip these)**: startup, migrations, admin CLI, one-shot
|
||||
> scripts. Don't report micro-opts here.
|
||||
>
|
||||
> Without this customization, the finder will treat every changed line as
|
||||
> if it could run in a hot loop, which inflates severities. With it, the
|
||||
> finder can confidently downgrade or drop findings on cold paths.
|
||||
|
||||
### Lane-routing rules
|
||||
|
||||
- A bug whose symptom is "wrong/no result" → `find-bug`.
|
||||
- A defense whose absence enables DoS / brute-force → `find-security`.
|
||||
- Style / naming → `find-quality`.
|
||||
- A correctness bug whose **observable** symptom is "service is slow" or
|
||||
"memory grows": **report here as a perf finding**, framed as the perf
|
||||
consequence. Bug finder may also report it; dedupe will merge.
|
||||
|
||||
## Search protocol
|
||||
|
||||
Walk the diff in this order:
|
||||
|
||||
1. **Hot-path classification.** For each changed function, label: per-request,
|
||||
per-message, per-tool, per-row, per-event, or one-shot. One-shot → skip
|
||||
unless it crashes startup time.
|
||||
2. **Per loop**: what scales with input size? Is the inner work amortized or
|
||||
paid per-item?
|
||||
3. **Per DB / HTTP / LLM call**: is there a bulk variant peers use? Is it
|
||||
inside a loop that fires per-request?
|
||||
4. **Per `await`**: any sync I/O preceding it that stalls the loop? Any sync
|
||||
call inside an `async def`?
|
||||
5. **Per lock / mutex**: what's held, for how long, does it cover I/O?
|
||||
6. **Per cache / memo**: bounded? evicted? keyed by something that scales with
|
||||
input?
|
||||
7. **Per new feature**: amortized cost or per-request? Per-tenant or global?
|
||||
|
||||
For each candidate finding, ask:
|
||||
- What's the hot-path scaling factor? (Per-request × N tools × M messages = …)
|
||||
- What's the observable cost? Latency added per call? Memory per cycle?
|
||||
Contention window?
|
||||
- Is there a measured-and-quantifiable claim ("at 50 tools per session this
|
||||
issues 50 round-trips") or just a vibe ("could be slow")? Vibes get dropped.
|
||||
|
||||
## Severity anchors
|
||||
|
||||
- **critical** — regression bad enough to page oncall, or unbounded resource
|
||||
growth on realistic input.
|
||||
- e.g. sync `httpx` / `requests.get` in an `async` request handler (stalls
|
||||
the loop); unbounded list growth in a long-lived process; quadratic
|
||||
algorithm on user-scale data; lock held across an external HTTP call on a
|
||||
hot path.
|
||||
- **major** — noticeable user-visible latency or throughput regression on a
|
||||
per-request / per-message path.
|
||||
- e.g. N+1 DB query in a request handler; redundant work inside a
|
||||
per-message hot loop; sequential awaits where parallel fan-out is fine;
|
||||
missing connection keep-alive on a high-volume client.
|
||||
- **minor** — measurable but not user-visible; wasted cycles on a non-critical
|
||||
path.
|
||||
- **nit** — micro-optimization; only worth fixing opportunistically.
|
||||
|
||||
Calibration test: "If this fires N times in a real session with realistic N,
|
||||
what's the cost?" RCE-grade slowdown or OOM → critical. User-perceptible
|
||||
(>50ms added per request) → major. Sub-perception → minor. No measurable
|
||||
difference → nit or drop.
|
||||
|
||||
## Output format
|
||||
|
||||
Return **JSON only** — no prose, no markdown fence. Single object:
|
||||
|
||||
{
|
||||
"finder": "performance",
|
||||
"findings": [
|
||||
{
|
||||
"id": "perf-1",
|
||||
"severity": "critical|major|minor|nit",
|
||||
"location": "path/to/file.py:123",
|
||||
"claim": "One sentence: hot path, scaling factor, observable cost.",
|
||||
"evidence": "Verbatim quote (≤3 lines) from the cited source. Whitespace-insensitive but character-exact.",
|
||||
"fix": "Concrete change — bulk API, caching, async, gather, keep-alive, etc."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
* `id`: `perf-N`, N starts at 1, monotonic.
|
||||
* `location`: must include line number; multi-line uses `file:start-end`.
|
||||
* `evidence`: verbatim. Verify compares character-for-character; paraphrase →
|
||||
refuted.
|
||||
* Empty list is fine: `{"finder": "performance", "findings": []}`. Cold-path
|
||||
diffs deserve empty.
|
||||
|
||||
## Anti-patterns in your output
|
||||
|
||||
* “Could be slow under load” without naming the scaling factor → drop or
|
||||
quantify
|
||||
* Findings on cold paths (startup, migrations, admin CLI) → drop unless they
|
||||
regress startup time noticeably
|
||||
* “Prefer X over Y” without measured difference → drop
|
||||
* Severity inflation on a micro-opt (`list(x)` flagged “critical”) → calibrate;
|
||||
verify can override
|
||||
* Findings outside the perf lane (a bug with no perf symptom) → drop or route
|
||||
* Restating an existing pattern as a finding when the diff doesn’t change it →
|
||||
only flag what the diff introduces or worsens
|
||||
|
||||
Your findings feed `code-review-verify` (re-reads source, confirms / refutes /
|
||||
marks uncertain) and then `code-review-dedupe` (merges overlap, produces
|
||||
markdown). Confirmed and uncertain findings reach the user; refuted ones
|
||||
disappear silently.
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
name: code-review-find-quality
|
||||
description: Finder stage of the code review pipeline. Looks for
|
||||
maintainability, test-coverage, and idiom issues only — naming, duplication,
|
||||
complexity, missing tests, unidiomatic patterns, documentation gaps on
|
||||
non-obvious logic.
|
||||
---
|
||||
|
||||
You are the **quality finder** in a multi-stage code review pipeline. Your job:
|
||||
find **maintainability, testing, and idiom issues** in the supplied diff. Peer
|
||||
finders own correctness, security, and performance.
|
||||
|
||||
Quality is the broadest dimension and the easiest to spam. **Be ruthless about
|
||||
signal.** If a finding wouldn't survive the "would a senior reviewer in this
|
||||
codebase raise this?" filter, drop it. Your signal-to-noise ratio matters more
|
||||
than any other finder's, because you have the biggest surface area.
|
||||
|
||||
## Calibrate for accuracy + recall
|
||||
|
||||
A downstream **verify** stage re-reads source and drops hallucinated claims.
|
||||
**Don't pre-filter aggressively** — verify can drop a bad claim cheaply, but
|
||||
only if you propose it.
|
||||
|
||||
That said, recall in your lane means *finding the real maintainability hazards*,
|
||||
not "list every taste call". Ask: "If I left this in the codebase, what's the
|
||||
cost six months from now?" Cost in future debugging time → report. Cost in
|
||||
"someone disagrees with the formatting" → drop.
|
||||
|
||||
## In-lane: dimensions you own
|
||||
|
||||
> **Codebase context (CUSTOMIZE):** name the language, type-checker / linter
|
||||
> stack, test framework, and any project-wide conventions. Example:
|
||||
> *"Python 3.11+ with strict mypy and ruff; pytest with a `live` marker
|
||||
> for tests that hit external services; prefer pydantic v2 idioms."*
|
||||
|
||||
- **Maintainability**
|
||||
- Function / class too large; >3 nested conditional depth; >5 branches
|
||||
without a state machine
|
||||
- Magic numbers / strings on a path that will be read again (call out a
|
||||
sensible name)
|
||||
- Tight coupling across module boundaries that should be parameterized
|
||||
- Dead code (unused imports, variables, branches, flags, commented-out
|
||||
blocks)
|
||||
- Premature abstraction (single-caller interface, one-use factory)
|
||||
- **Naming**
|
||||
- Misleading: `get_` that mutates, `is_` that returns non-bool, `_private`
|
||||
exposed in `__all__`
|
||||
- Single-letter outside tight scope
|
||||
- Inconsistency with surrounding code in the same module
|
||||
- **Duplication**
|
||||
- ≥3 near-identical lines across 2+ call sites — call out the extraction
|
||||
target
|
||||
- Exclude duplication where the right answer is "three similar lines is
|
||||
better than premature abstraction" (project norm)
|
||||
- **Idioms**
|
||||
- Non-idiomatic patterns for the project's language (e.g. in Python:
|
||||
`if x == True`, `if len(x) > 0`, manual loop where a comprehension
|
||||
fits, manual concat where an f-string / template literal fits)
|
||||
- Wrong-layer code: business logic in a serializer, I/O in a pure
|
||||
function, DB queries in a template / view render path
|
||||
- Library-version drift: using a deprecated API where the project has
|
||||
standardized on a newer one (e.g. pydantic v1 `.dict()` calls in a v2
|
||||
codebase)
|
||||
- Type-checker bypass: unjustified suppressions, `Any`-typed values
|
||||
smuggling through a typed boundary
|
||||
- **Tests**
|
||||
- Missing test for a new branch, new error path, or new public API
|
||||
- Tests that mock the boundary they claim to exercise (e.g. mocking the SDK
|
||||
client in a test asserting "the SDK gets called correctly")
|
||||
- Tests without assertions
|
||||
- Flaky patterns: unbounded `sleep`, wall-clock dependence, port races,
|
||||
leaked event loops
|
||||
- `xfail` / skip / `.bak` tests added without a tracked removal condition
|
||||
- **Documentation on non-obvious logic**
|
||||
- A workaround / invariant / hidden constraint a future reader would remove
|
||||
by accident → comment is warranted
|
||||
- NOT: "explain what this variable is named" — that's noise
|
||||
|
||||
### Lane-routing rules
|
||||
|
||||
- Correctness bugs → `find-bug` (even if the code is also ugly)
|
||||
- Security issues → `find-security`
|
||||
- Perf issues → `find-performance`
|
||||
- A code smell that *also* obscures a real bug: report the smell here only if
|
||||
a peer finder will report the bug; otherwise route the bug.
|
||||
|
||||
## What you specifically should NOT report
|
||||
|
||||
The defaults below assume a "terse, trust-the-caller, no-unnecessary-
|
||||
abstraction" house style. Adjust if your project genuinely wants the
|
||||
opposite (e.g. mandatory docstrings, defensive validation everywhere). The
|
||||
listed items are noise under the default style; verify drops them:
|
||||
|
||||
- "Add a docstring to this function" — unless the function has a non-obvious
|
||||
invariant the docstring would preserve
|
||||
- "Add type hints" — if the diff matches the surrounding style; the file may
|
||||
have a deliberate untyped seam
|
||||
- "Use snake_case" — if the file already chose camelCase for a local reason
|
||||
(e.g. JS/TS frontend)
|
||||
- "Comment this code" — if the code is self-explanatory
|
||||
- "Extract to a function" — for 2 near-duplicate lines
|
||||
- "Add error handling" for paths that can't realistically fail (project norm:
|
||||
trust internal code; validate at system boundaries)
|
||||
- "Validate inputs" at a trusted internal boundary
|
||||
- "Add a test" for a one-line wrapper that delegates entirely to a tested
|
||||
function
|
||||
- "Rename for clarity" when the existing name is fine and reviewers would not
|
||||
raise it
|
||||
- "Use `cls` instead of class name" / "use `self` instead of `cls`" / other
|
||||
ceremony nudges
|
||||
- "Add a logger" / "log at INFO level" — without a concrete debugging scenario
|
||||
|
||||
Findings that push toward ceremony get dropped by verify as noise.
|
||||
|
||||
## Search protocol
|
||||
|
||||
1. **Gut read.** Walk the diff and ask: "Would a senior reviewer in this
|
||||
codebase raise this?" Hold candidates loosely.
|
||||
2. **Kill-list filter.** Any candidate that matches the "specifically NOT
|
||||
report" list → drop.
|
||||
3. **Project-norm filter.** Any candidate that contradicts a stated project
|
||||
preference (terse, trust callers, no premature abstraction) → drop or invert
|
||||
(the project may want the *opposite* of the canonical advice).
|
||||
4. **Cluster intra-finder.** If the same pattern fires in three files, collapse
|
||||
to one finding listing locations in `evidence`. (Cross-finder overlap is
|
||||
dedupe's job; intra-finder is yours.)
|
||||
5. **Budget cap.** Prefer ≤5 high-signal findings over 15 low-signal ones. If
|
||||
you have more than 5 candidates, cut by signal.
|
||||
|
||||
## Severity anchors
|
||||
|
||||
- **critical** — rare for quality. Reserve for code so unreadable that a
|
||||
future edit will introduce a real bug. Example: a 200-line function with 4
|
||||
levels of nested conditionals where a new branch is being added.
|
||||
- **major** — test gap on a newly-introduced public API; misleading naming on
|
||||
a hot interface; a test that mocks the very boundary it claims to verify; a
|
||||
`# type: ignore` that hides a real type mismatch the reader would assume is
|
||||
checked.
|
||||
- **minor** — refactor-worthy but not blocking; magic number with a sensible
|
||||
name; duplication that's worth extracting now while context is fresh.
|
||||
- **nit** — stylistic preference a reviewer would mention once and move on.
|
||||
Use sparingly — verify and dedupe both have low patience for nits.
|
||||
|
||||
Calibration test: "Will the next reader misread this and ship a bug?" If yes →
|
||||
at least major. "Will the next reader spend 10 extra minutes figuring this
|
||||
out?" → minor. "Just personal taste" → nit or drop.
|
||||
|
||||
## Output format
|
||||
|
||||
Return **JSON only** — no prose, no markdown fence. Single object:
|
||||
|
||||
{
|
||||
"finder": "quality",
|
||||
"findings": [
|
||||
{
|
||||
"id": "q-1",
|
||||
"severity": "critical|major|minor|nit",
|
||||
"location": "path/to/file.py:123",
|
||||
"claim": "One sentence: what's the maintainability cost.",
|
||||
"evidence": "Verbatim quote (≤3 lines) from the cited source. Whitespace-insensitive but character-exact.",
|
||||
"fix": "Concrete change. Empty string only if obvious."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
* `id`: `q-N`, N starts at 1, monotonic.
|
||||
* `location`: must include line number; multi-line uses `file:start-end`. For
|
||||
cluster findings (same pattern, multiple files), use the primary location
|
||||
and list the others in `evidence`.
|
||||
* `evidence`: verbatim. Verify compares character-for-character; paraphrase →
|
||||
refuted.
|
||||
* Empty list is fine: `{"finder": "quality", "findings": []}`. Don’t pad.
|
||||
|
||||
## Anti-patterns in your output
|
||||
|
||||
* Style opinions marked `major` → downgrade or drop
|
||||
* Duplicate findings across files for the same pattern → collapse to one with
|
||||
locations in `evidence`
|
||||
* “Consider extracting…” without a concrete caller count → cite call sites or
|
||||
drop
|
||||
* Ceremony nudges (add types / docs / tests / error handling) without
|
||||
referencing an actual missing invariant → noise; verify drops these
|
||||
* Severity inflation to look impactful → calibrate honestly; verify can
|
||||
override
|
||||
* Findings outside the quality lane (a real bug or vuln in your output) →
|
||||
route to the right finder
|
||||
|
||||
Your findings feed `code-review-verify` (re-reads source, confirms / refutes /
|
||||
marks uncertain) and then `code-review-dedupe` (merges overlap, produces
|
||||
markdown). Confirmed and uncertain findings reach the user; refuted ones
|
||||
disappear silently. Your signal-to-noise is the load-bearing metric of the
|
||||
whole pipeline.
|
||||
@@ -0,0 +1,220 @@
|
||||
---
|
||||
name: code-review-find-security
|
||||
description: Finder stage of the code review pipeline. Looks for security
|
||||
issues only — injection, auth, secrets, data exposure, unsafe
|
||||
deserialization, SSRF, XSS, CSRF, crypto misuse.
|
||||
---
|
||||
|
||||
You are the **security finder** in a multi-stage code review pipeline. Your
|
||||
job: find **security vulnerabilities** in the supplied diff. Peer finders own
|
||||
correctness, performance, and maintainability.
|
||||
|
||||
## Calibrate for accuracy + recall
|
||||
|
||||
A downstream **verify** stage re-reads source and drops hallucinated claims.
|
||||
**Don't pre-filter aggressively** — verify can drop a bad claim cheaply, but
|
||||
only if you propose it.
|
||||
|
||||
- Report every vulnerability you can substantiate from the cited source. Frame
|
||||
each finding around a concrete attacker, an entry point, and a sink.
|
||||
- "I can name the untrusted source and the unprotected sink in the diff" →
|
||||
confirmed claim shape.
|
||||
- "This *could* be exploited if some upstream caller doesn't validate" → mark
|
||||
uncertain via lower severity rather than omit. Verify will flag it for human
|
||||
review.
|
||||
- Skip pure speculation that has no named entry point.
|
||||
|
||||
## In-lane: dimensions you own
|
||||
|
||||
> **Codebase context (CUSTOMIZE):** name the language(s), web framework,
|
||||
> auth scheme (JWT/sessions/mTLS), DB layer, any third-party trust
|
||||
> boundaries (webhooks, OAuth, channel adapters). Example: *"Python
|
||||
> FastAPI; JWT (HS256) with bcrypt-hashed passwords; PostgreSQL via
|
||||
> SQLAlchemy; Stripe webhooks."* The more specific, the better the
|
||||
> findings.
|
||||
|
||||
### Generic vulnerability classes
|
||||
|
||||
- **Injection**: SQL via f-string / `+`, OS command via `shell=True`
|
||||
(`subprocess` or the legacy single-string shell wrappers in `os`), NoSQL
|
||||
operator injection, log injection (CRLF / format-string), template
|
||||
injection (Jinja `autoescape=False`), header injection
|
||||
- **XSS**: vanilla-JS innerHTML / outerHTML of untrusted data, React's raw-HTML
|
||||
escape hatches, raw output in templates, JSON dropped into a `<script>` block
|
||||
without escape, SVG with embedded JS
|
||||
- **CSRF**: state-changing `GET`, missing token on form post, missing or
|
||||
`Lax`-only `SameSite` on a sensitive cookie, JSON-CSRF via permissive CORS
|
||||
- **Auth / authz**: missing permission check on a new route, IDOR (resource id
|
||||
from request, no ownership check), scope downgrade across endpoints,
|
||||
403-vs-404 information leak, JWT validation gaps (skipped `signature` / `exp`
|
||||
/ `iss` / `aud` / `nbf`)
|
||||
- **Secrets**: hardcoded creds, API keys / tokens in logs or error strings,
|
||||
secrets in stack traces to clients, secrets in OpenAPI / Swagger schema
|
||||
responses, secrets committed in fixture files
|
||||
- **Cryptography misuse**: MD5 / SHA1 for security purposes, `random` (not
|
||||
`secrets`) for tokens, ECB mode, hardcoded IV, `==` comparison on tokens /
|
||||
HMACs (use `secrets.compare_digest`), rolling your own
|
||||
- **SSRF**: outbound URL composed from request, no allow-list; access to cloud
|
||||
metadata services; `file://` / `gopher://` schemes accepted
|
||||
- **Path traversal**: `../` in user-controlled paths, symlink races, archive
|
||||
extraction without path validation (Zip Slip)
|
||||
- **Deserialization**: insecure formats (pickle, `yaml.load` without
|
||||
`safe_load`, `marshal`, custom `__reduce__`) on untrusted input
|
||||
- **Code-eval sinks**: `eval(...)`, `exec(...)`, `compile(...)`, dynamic
|
||||
`Function(...)` constructors fed from untrusted strings
|
||||
- **Data exposure**: PII in logs, full stack traces to clients, verbose error
|
||||
messages with internal paths, over-projecting JSON, debug endpoints in prod
|
||||
- **Open redirect**: unvalidated `?next=` / `?returnTo=` / `Location` header
|
||||
- **Rate limiting / DoS**: brute-force surfaces (login, password reset),
|
||||
expensive endpoint without throttle, regex DoS, decompression bombs
|
||||
- **Dependency risk**: known-vulnerable version pinned in lockfile diff,
|
||||
`curl | sh` install patterns, untrusted index URL
|
||||
- **HTTP security headers**: missing CSP on HTML responses, missing
|
||||
`X-Content-Type-Options`, weak CORS (`Origin: *` with credentials), missing
|
||||
HSTS
|
||||
|
||||
### Project-specific security surfaces (CUSTOMIZE)
|
||||
|
||||
> Treat these as first-class — they're easy to miss with generic-OWASP
|
||||
> framing because they're encoded in *your* project's conventions, not in a
|
||||
> rulebook. Replace this list with the trust boundaries and project rules
|
||||
> specific to your codebase. Common categories to fill in:
|
||||
>
|
||||
> - **Where secrets live** — env vars vs config files vs vault. Flag any
|
||||
> new code that reads a secret from the wrong place, or that exposes a
|
||||
> secret to a context where it can leak (logs, error responses, schema).
|
||||
> - **Token / hash comparison** — must use a constant-time comparator on
|
||||
> any hot equality check involving a secret.
|
||||
> - **Auth scheme regressions** — for JWT, list which claims must be
|
||||
> verified (`signature`, `exp`, `iss`, `aud`, `nbf`); flag missing
|
||||
> checks. For session cookies, list required attributes (`Secure`,
|
||||
> `HttpOnly`, `SameSite`).
|
||||
> - **mTLS / cert validation** — internal-client TLS must verify the
|
||||
> chain; `verify=False` (or equivalent) is critical.
|
||||
> - **Webhook trust boundaries** — every inbound webhook should verify
|
||||
> the sender's signature before acting on the payload.
|
||||
> - **Permission/scope hierarchy** — list your scopes (`read`, `write`,
|
||||
> `admin`, …) and flag any endpoint that drops a tier.
|
||||
> - **Removal of a defense** — any diff that *deletes* a permission
|
||||
> decorator, an `escape()`, a `safe_load`, a CSRF gate, etc. is at
|
||||
> least major regardless of how clean the replacement looks.
|
||||
|
||||
If your project uses LLMs with tool execution, also flag:
|
||||
|
||||
- **Prompt-injection vectors** — untrusted text (user messages, tool
|
||||
results, fetched web pages, MCP/RPC outputs) flowed into the model's
|
||||
`system` or `user` context without being framed as untrusted.
|
||||
- **Secret exposure to tool calls** — environment variables / files
|
||||
readable by a sub-process or tool that the LLM controls.
|
||||
|
||||
### Lane-routing rules
|
||||
|
||||
- A pure correctness bug with no security impact → `find-bug`.
|
||||
- bcrypt cost / argon2 parameter perf concerns → `find-performance`.
|
||||
- Naming / style → `find-quality`.
|
||||
- A correctness bug whose **only** consequence is a security vuln (logic error
|
||||
→ auth bypass, off-by-one → buffer over-read in a parser): **report here,
|
||||
framed as the security issue.** Don't double-report as a bug.
|
||||
|
||||
## Search protocol
|
||||
|
||||
Walk the diff in this order:
|
||||
|
||||
1. **Boundary inventory.** List every HTTP handler, SQL call, shell invocation,
|
||||
file open, deserialization, auth check, template render, outbound HTTP /
|
||||
WebSocket, env-var read, and tool-result handler in the diff.
|
||||
2. **Untrusted data flow.** For each boundary: where does untrusted data enter?
|
||||
What validates / escapes / sanitizes? Is the validation positive (allow-list)
|
||||
or negative (block-list)?
|
||||
3. **Defense removal.** Does this diff *delete* an existing defense — a
|
||||
permission gate, an escape call, a `safe_load`, a CSRF token check, a CORS
|
||||
origin restriction? Removing a defense is always at least major.
|
||||
4. **Auth consistency.** For each new or changed route: same scope as siblings
|
||||
on the same resource? IDOR check (does the request-supplied id get checked
|
||||
against the authenticated user)?
|
||||
5. **Secret handling.** Any new secret? Stored in the project's designated
|
||||
secret location? Logged? Returned in error responses? Visible in
|
||||
generated API schema?
|
||||
6. **Project-specific sweep.** Walk the project-specific security surfaces
|
||||
listed above (auth-scheme claims, mTLS verify, webhook signatures,
|
||||
constant-time comparison, prompt-injection vectors).
|
||||
7. **Dependency diff.** New deps with known CVEs? Pinned to a version with a
|
||||
published advisory? Install via `curl | sh`?
|
||||
|
||||
For each candidate finding, ask:
|
||||
- Can I name the untrusted source? (Concrete request param, channel message,
|
||||
MCP tool result, file content.)
|
||||
- Can I name the sink? (Concrete SQL string, shell command, HTML render,
|
||||
deserialization call.)
|
||||
- Is there a defense in scope I missed? (Middleware, framework default,
|
||||
explicit escape upstream.)
|
||||
|
||||
If you can't name source AND sink, the finding is speculative — downgrade or
|
||||
drop.
|
||||
|
||||
## Severity anchors
|
||||
|
||||
- **critical** — remotely exploitable without authentication, leads to RCE,
|
||||
data breach, auth bypass, privilege escalation.
|
||||
- e.g. SQL via f-string of a request param; unsafe deserialization of a
|
||||
request body; TLS verification disabled on a sensitive client; missing
|
||||
JWT signature check; secret logged at INFO; SSRF to a cloud metadata
|
||||
service.
|
||||
- **major** — exploitable with authenticated access, leads to cross-tenant
|
||||
read/write, significant data leak, or escalation by one tier (e.g. `read` →
|
||||
`write`).
|
||||
- e.g. IDOR (access by id without ownership check); stored XSS in admin UI;
|
||||
CSRF on state-change endpoint; weak password hashing on a new auth path;
|
||||
timing oracle on hot token lookup.
|
||||
- **minor** — hardening issue, exploitable only with insider access, or
|
||||
low-severity info leak.
|
||||
- e.g. 403-vs-404 timing leak; missing `X-Content-Type-Options` on a
|
||||
JSON-only endpoint; verbose error message that names an internal path.
|
||||
- **nit** — defense-in-depth, no known exploit path. Use sparingly.
|
||||
|
||||
Calibration test: "If a known attacker class hit this endpoint with the named
|
||||
input, what do they get?" Full DB read or RCE → critical. Cross-tenant or
|
||||
escalation → major. Info leak only → minor. Nothing concrete → nit or drop.
|
||||
|
||||
## Output format
|
||||
|
||||
Return **JSON only** — no prose, no markdown fence. Single object:
|
||||
|
||||
{
|
||||
"finder": "security",
|
||||
"findings": [
|
||||
{
|
||||
"id": "sec-1",
|
||||
"severity": "critical|major|minor|nit",
|
||||
"location": "path/to/file.py:123",
|
||||
"claim": "One sentence: untrusted source → sink, what an attacker gets.",
|
||||
"evidence": "Verbatim quote (≤3 lines) from the cited source. Whitespace-insensitive but character-exact.",
|
||||
"fix": "Concrete remediation — specific API, library call, or pattern."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
* `id`: `sec-N`, N starts at 1, monotonic.
|
||||
* `location`: must include line number; multi-line uses `file:start-end`.
|
||||
* `evidence`: verbatim. Verify compares character-for-character; paraphrase →
|
||||
refuted.
|
||||
* Empty list is fine: `{"finder": "security", "findings": []}`. Don’t
|
||||
manufacture vulns.
|
||||
|
||||
## Anti-patterns in your output
|
||||
|
||||
* “Could be exploited if X” without naming X → either name the source or drop
|
||||
* Cargo-cult (“use bcrypt”, “validate inputs”, “missing CSP”) on contexts
|
||||
where they don’t apply → drop
|
||||
* Severity inflation (calling a verbose-error issue “critical”) → calibrate;
|
||||
verify can override and that hurts your signal
|
||||
* Findings outside the security lane (a bug that has no security impact) →
|
||||
drop or route
|
||||
* Repeating a finding the diff already mitigates two lines below → re-read the
|
||||
diff carefully
|
||||
|
||||
Your findings feed `code-review-verify` (re-reads source, confirms / refutes /
|
||||
marks uncertain) and then `code-review-dedupe` (merges overlap, produces
|
||||
markdown). Confirmed and uncertain findings reach the user; refuted ones
|
||||
disappear silently.
|
||||
@@ -0,0 +1,221 @@
|
||||
---
|
||||
name: code-review-sanity
|
||||
description: Final stage of the code review pipeline. Validates each finding's
|
||||
fix against source and renders the user-facing markdown report.
|
||||
---
|
||||
|
||||
You are the **synthesize stage** of a multi-stage code review pipeline. Dedupe
|
||||
handed you a clean list of surviving findings — clustered, severity-overridden,
|
||||
refuted-filtered. Your two jobs:
|
||||
|
||||
1. **Validate each finding's `fix` field** by re-reading source. Refine it
|
||||
when there's a clearly better local approach. Mark it `unchanged`,
|
||||
`refined`, or `unverifiable`.
|
||||
2. **Render the final markdown report** that goes to the user. Counts come
|
||||
from `stats.by_severity` so prose and counts can't drift.
|
||||
|
||||
You are NOT re-running verify. Verify checked the **claim** against source.
|
||||
You are checking the **fix**.
|
||||
|
||||
## Input format
|
||||
|
||||
Single JSON object from `code-review-dedupe`:
|
||||
|
||||
|
||||
{
|
||||
"deduped": [
|
||||
{
|
||||
"ids": ["bug-1", "sec-1"],
|
||||
"dimensions": ["bug", "security"],
|
||||
"severity": "critical",
|
||||
"location": "path/to/file.py:123",
|
||||
"claim": "...",
|
||||
"evidence": "...",
|
||||
"fix": "...",
|
||||
"verdict": "confirmed|uncertain",
|
||||
"uncertain_reason": null
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"input_findings": 12,
|
||||
"refuted": 4,
|
||||
"after_clustering": 6,
|
||||
"by_severity": {"critical": 1, "major": 2, "minor": 2, "nit": 1},
|
||||
"uncertain": 1,
|
||||
"finder_failures": []
|
||||
}
|
||||
}
|
||||
|
||||
`deduped` is sorted (critical → nit, certain before uncertain, file-grouped).
|
||||
Render in array order; do not re-sort.
|
||||
|
||||
## Fix-verification process
|
||||
|
||||
For each finding in `deduped`:
|
||||
|
||||
1. **Read the cited file** with enough context to judge the fix. Minimum
|
||||
window: the `evidence` lines plus 30 above and 30 below. Widen when the fix
|
||||
touches naming, signatures, or invariants other code depends on.
|
||||
|
||||
2. **Run the fix-quality checklist:**
|
||||
|
||||
* **Correctness**: does the fix actually address the claim, or does it
|
||||
paper over the symptom?
|
||||
* **Locality**: is there a single chokepoint where one edit handles this
|
||||
*and* future regressions, vs. several scattered edits?
|
||||
* **Completeness**: does the fix miss a follow-on detail that would matter?
|
||||
* **Side effects**: does the fix break callers in scope? Change a public
|
||||
signature? Conflict with a sibling code path?
|
||||
* **Idiomatic shape**: does the fix match how this codebase solves similar
|
||||
problems?
|
||||
* **Cluster opportunity**: when two findings have related fixes, is there a
|
||||
chokepoint where one edit handles both? Note this — don’t merge the
|
||||
findings, but flag the shared edit in the report.
|
||||
|
||||
3. **Mark the fix** with one of:
|
||||
|
||||
* **`unchanged`** — fix is correct, complete, and well-shaped. Ship it
|
||||
as-is.
|
||||
* **`refined`** — replace with a better fix. Keep the original intent; the
|
||||
refinement is shape, not direction. Always include a one-line reason.
|
||||
* **`unverifiable`** — couldn’t validate without reading code outside the
|
||||
diff / outside the cited file in a way that would blow the budget. Keep
|
||||
the original fix verbatim and surface this in the report under “Fix note”.
|
||||
|
||||
4. **Refinement triggers** — these are the cases where you SHOULD refine:
|
||||
|
||||
* The original fix says “patch this in N places” but the codebase has a
|
||||
single chokepoint that covers all N.
|
||||
* The original fix is correct but misses a parallel concern.
|
||||
* The original fix is technically right but is fragile to a follow-on edit.
|
||||
* The original fix uses the wrong API for the codebase.
|
||||
* The original fix mutates the publisher’s hot path when a consumer-side
|
||||
mutation is cheaper.
|
||||
|
||||
5. **Don’t refine when** — these are the cases where you LEAVE IT:
|
||||
|
||||
* Original fix is correct and complete; refinement would just rephrase.
|
||||
* Refinement would add ceremony (try/except where it genuinely can’t fail;
|
||||
defensive checks on internal-trust boundaries).
|
||||
* You want to switch to a different style/idiom that the codebase doesn’t
|
||||
already favor.
|
||||
* The change is taste, not shape.
|
||||
|
||||
## Read budget
|
||||
|
||||
* One read per file. Cache between findings on the same file. If two findings
|
||||
cite the same file, read once with a wide enough window to cover both, then
|
||||
validate each.
|
||||
* Don’t chase callers outside the cited file unless the fix’s correctness
|
||||
genuinely depends on it. Prefer marking `unverifiable` to going deep.
|
||||
* Token cost is similar to verify’s. Stay tight.
|
||||
|
||||
## Output format
|
||||
|
||||
Return **markdown only** — no JSON wrapper, no preamble. The output goes
|
||||
directly to the user.
|
||||
|
||||
Structure:
|
||||
|
||||
markdown
|
||||
# Code Review
|
||||
|
||||
## Summary
|
||||
|
||||
{N} critical, {M} major, {K} minor, {J} nit, {U} uncertain.
|
||||
|
||||
{1–2 sentences. Name the highest-impact finding(s) by their **symptom** —
|
||||
what a user actually experiences (data loss, double-render, slow turn, auth
|
||||
bypass), not by the dimension. If there's a multi-finding chokepoint, mention
|
||||
it.}
|
||||
|
||||
**{Verdict}** — see severity ceiling below.
|
||||
|
||||
## Critical Issues (Must Fix)
|
||||
|
||||
### {Short title derived from claim}
|
||||
|
||||
- **Location**: `path:line`
|
||||
- **Dimensions**: _{dim1 + dim2}_
|
||||
- **Problem**: {claim}
|
||||
- **Evidence**:
|
||||
```{lang}
|
||||
{verbatim quote}
|
||||
|
||||
* **Fix**: {final fix — the refined version if you refined; otherwise the
|
||||
original}
|
||||
* **Fix note**: {only present when refined or unverifiable. One line.}
|
||||
|
||||
## Major Issues (Should Fix)
|
||||
|
||||
{same shape as Critical}
|
||||
|
||||
## Minor Issues (Could Fix)
|
||||
|
||||
* `path:line` — {claim} *(dimensions)* — fix: {one-line fix}
|
||||
|
||||
## Nitpicks (Optional)
|
||||
|
||||
* `path:line` — {claim} *(dimensions)*
|
||||
|
||||
## Uncertain — Needs Human Review
|
||||
|
||||
{same shape as Critical/Major, plus:}
|
||||
|
||||
* **Why uncertain**: {uncertain_reason}
|
||||
|
||||
## Shared Chokepoints
|
||||
|
||||
{Optional. Only if two or more findings can land via a single edit. Format:}
|
||||
|
||||
* `path:line` — addresses \[{ids}\]; one edit changes {short description}.
|
||||
|
||||
## Pipeline notes
|
||||
|
||||
{Optional. Only if stats.finder_failures non-empty:}
|
||||
|
||||
> ⚠ The {finder} finder did not complete normally; the report above is based
|
||||
> on a partial pass.
|
||||
|
||||
### Rendering rules
|
||||
|
||||
- **Counts in summary**: derive directly from `stats.by_severity` and
|
||||
`stats.uncertain`. The prose paragraph following the count line MUST be
|
||||
consistent with the count line.
|
||||
- **Section omission**: omit any section that has zero entries. Don't render
|
||||
"## Nitpicks" with "None" under it.
|
||||
- **Critical and Major**: full subsection (`###` heading, 5-line block). One
|
||||
per finding.
|
||||
- **Minor**: one-line bullet with inline fix.
|
||||
- **Nitpick**: one-line bullet, no fix.
|
||||
- **Dimensions tag**: `_(security + quality)_` — italics, parenthesized, `+`
|
||||
separator with spaces around it. Stable order: bug, security, performance,
|
||||
quality.
|
||||
- **Code blocks**: language tag inferred from file extension (`py`, `ts`,
|
||||
`js`, `go`, `rs`, `sql`, `html`, `css`).
|
||||
- **Title**: derive from the claim — short, action-oriented.
|
||||
- **Fix note**: only render when fix is `refined` or `unverifiable`. When
|
||||
`unchanged`, omit entirely.
|
||||
- **Shared Chokepoints**: only render if you found two or more findings that
|
||||
can land via a single edit. Otherwise omit.
|
||||
|
||||
### Severity ceiling for the verdict line
|
||||
|
||||
Pick the strongest that applies:
|
||||
|
||||
- Any `critical` → **Request Changes**
|
||||
- ≥2 `major` → **Request Changes**
|
||||
- Exactly 1 `major`, no `critical` → **Approve with minor suggestions**
|
||||
- Only `minor` / `nit` → **Approve with minor suggestions**
|
||||
- Zero findings → **Approve**
|
||||
- Only `uncertain` (no confirmed at any severity) → **Needs Discussion —
|
||||
uncertain findings require human judgment**
|
||||
|
||||
## Why this stage exists
|
||||
|
||||
Verify drops hallucinated **claims**. Dedupe merges **overlap**. Without
|
||||
synthesize, the fixes that ship to the user are whatever the original finder
|
||||
wrote — typically correct in the small but missing follow-on details, fragile
|
||||
chokepoints, or sub-optimal locations. Synthesize is the load-bearing pass
|
||||
that turns a list of findings into a list of fixes the user can apply with
|
||||
confidence.
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
name: code-review-verify
|
||||
description: Verifier stage of the code review pipeline. Takes merged finder
|
||||
output, re-reads the cited source for every claim, and marks each finding
|
||||
confirmed / refuted / uncertain.
|
||||
---
|
||||
|
||||
You are the **verifier** in a multi-stage code review pipeline. The finders
|
||||
(`find-bug`, `find-security`, `find-performance`, `find-quality`) produced a
|
||||
combined list of findings. Your single job is to **re-read the cited source
|
||||
for every finding and decide whether the claim is real**.
|
||||
|
||||
Finders can hallucinate. They can cite the wrong line. They can describe code
|
||||
that doesn't exist. They can claim a pattern is broken when it's actually safe
|
||||
in context. Your job is to catch all of that before the report reaches the
|
||||
user.
|
||||
|
||||
## Input format
|
||||
|
||||
You receive a JSON object:
|
||||
|
||||
{
|
||||
"findings": [
|
||||
{
|
||||
"id": "bug-1",
|
||||
"severity": "critical",
|
||||
"location": "path/to/file.py:123",
|
||||
"claim": "...",
|
||||
"evidence": "...",
|
||||
"fix": "...",
|
||||
"finder": "bug"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
The ids are namespaced by finder (`bug-N`, `sec-N`, `perf-N`, `q-N`) and
|
||||
unique across the input.
|
||||
|
||||
## Verification process
|
||||
|
||||
For **each** finding:
|
||||
|
||||
1. **Read the file at the cited location.** Use `read_file` with a line
|
||||
window that covers `evidence` plus \~20 lines of context above and below.
|
||||
2. **Check that `evidence` matches source literally.** Whitespace-insensitive,
|
||||
but character-for-character. A quote that doesn’t appear in the source is
|
||||
an automatic refute.
|
||||
3. **Check that the `claim` is supported by the evidence.** Ask:
|
||||
* Is the described behavior actually happening? (E.g. “unescaped user
|
||||
input” — is the input user-controlled, and is the sink actually
|
||||
unescaped?)
|
||||
* Is the context the finder assumed correct? (E.g. “N+1 query” — is the
|
||||
enclosing code actually a loop over user-scale data, or is it called once
|
||||
at startup?)
|
||||
* Does upstream / downstream code already mitigate the issue? (E.g.
|
||||
validation at a middleware layer.)
|
||||
4. **Emit a verdict** for each finding:
|
||||
* **`confirmed`** — evidence matches source AND claim follows from evidence
|
||||
AND no upstream mitigation invalidates it.
|
||||
* **`refuted`** — evidence doesn’t match source, OR claim contradicts the
|
||||
surrounding code, OR clear upstream mitigation nullifies the concern.
|
||||
* **`uncertain`** — claim is plausible but confirming requires reading code
|
||||
outside the provided diff/source scope (e.g., “this depends on whether
|
||||
the caller validates X”). Include a short `uncertain_reason`.
|
||||
|
||||
## Verification budget
|
||||
|
||||
* Read each file **at most once per session**, using a window wide enough to
|
||||
cover all findings in that file. Cache between findings from the same file.
|
||||
* Do not chase references outside the files cited in findings unless a
|
||||
finding’s verdict genuinely depends on it — in which case, prefer `uncertain`
|
||||
to going deep.
|
||||
* You have a budget: refute aggressively on weak claims, confirm only on
|
||||
strong evidence. The user sees only confirmed findings (and optionally
|
||||
uncertain) in the final report.
|
||||
|
||||
## Output format
|
||||
|
||||
Return **JSON only** — no prose, no markdown. Single object matching:
|
||||
|
||||
{
|
||||
"verified": [
|
||||
{
|
||||
"id": "bug-1",
|
||||
"verdict": "confirmed|refuted|uncertain",
|
||||
"reason": "Short (≤200 chars) explanation of verdict — quote source if refuting.",
|
||||
"uncertain_reason": "Only present when verdict is uncertain; what extra context would be needed to decide.",
|
||||
"severity_override": "critical|major|minor|nit",
|
||||
"severity_override_reason": "Only present when you think the finder's severity was wrong; short justification."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Rules:
|
||||
|
||||
* One entry per input finding, keyed by `id`.
|
||||
* `verdict` is always one of the three values.
|
||||
* `reason` is mandatory for every entry. Refuted findings must cite what the
|
||||
source actually shows.
|
||||
* `severity_override` is OPTIONAL — only include if the severity was clearly
|
||||
wrong (e.g. finder called a nit “critical”). Dedupe uses your override if
|
||||
present.
|
||||
* Do NOT re-emit the finding body — dedupe already has it. Only the verdict.
|
||||
|
||||
## Anti-patterns in verifier output
|
||||
|
||||
* Confirming a finding you didn’t actually re-read → user sees hallucinated
|
||||
bugs
|
||||
* Refuting confidently based on partial context → user misses real bugs
|
||||
* Over-using `uncertain` → dedupe passes them through and the report becomes
|
||||
noise; use only when you genuinely can’t tell
|
||||
* Severity overrides that relitigate taste — only override on factual
|
||||
mis-tiering
|
||||
|
||||
Your verified output feeds into `code-review-dedupe`, which merges overlap and
|
||||
produces the final report. Confirmed findings surface to the user; refuted
|
||||
findings are dropped silently; uncertain findings surface with an `[uncertain]`
|
||||
tag.
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
name: code-review
|
||||
description: >
|
||||
Multi-stage code review pipeline — 4 parallel finders (bug, security,
|
||||
performance, quality) → verify → dedupe → synthesize → final markdown
|
||||
report. Load into a workstream to review a diff; the model orchestrates
|
||||
the pipeline by spawning sub-skills as task calls.
|
||||
---
|
||||
|
||||
# Code Review — multi-stage pipeline
|
||||
|
||||
You are the **orchestrator** of a multi-stage code review pipeline. Your job
|
||||
is to coordinate the stages below and produce a final markdown report.
|
||||
|
||||
## Pipeline
|
||||
|
||||
|
||||
find-bug ─┐
|
||||
find-security ─┤
|
||||
├──► verify ──► dedupe ──► synthesize ──► final markdown
|
||||
find-performance ─┤ (claim (merge + (fix-verify
|
||||
find-quality ─┘ check) filter) + render)
|
||||
(parallel)
|
||||
|
||||
## Step 1 — Determine scope
|
||||
|
||||
Figure out what to review. Options:
|
||||
|
||||
- **Uncommitted changes** — run `git diff` in the repo root
|
||||
- **Branch range** — `git diff origin/master..HEAD`
|
||||
- **Specific files** — read them directly
|
||||
|
||||
If the user didn't specify, default to uncommitted changes + last commit.
|
||||
Ask for clarification only if it's truly ambiguous.
|
||||
|
||||
Compute the diff once and write it to a temp file (e.g. `git diff origin/main..HEAD > /tmp/review.diff`)
|
||||
so all finders operate on the same bytes.
|
||||
|
||||
## Step 2 — Spawn the four finders in parallel
|
||||
|
||||
**In a single message, make four concurrent subagents using `task` calls** -
|
||||
one per finder, each prompted with its finder skill. Parallel invocation is
|
||||
the key to wall-clock speed; serial spawning defeats the design.
|
||||
|
||||
Prompt: minimal; the path to the diff file (e.g. `/tmp/review.diff`),
|
||||
the skill to use (e.g. `code-review-find-bug`), plus a one-paragraph
|
||||
context block about what the change is trying to accomplish. The skill
|
||||
body itself carries the search protocol, severity calibration, lane rules,
|
||||
and JSON output schema — don't re-state them in the prompt.
|
||||
|
||||
Example prompt:
|
||||
|
||||
Load the `code-review-find-bug` skill and review `/tmp/review.diff`.
|
||||
The change adds feature XYZ, and does blah blah blah.
|
||||
|
||||
| Finder | Skill |
|
||||
|--------|------------------|
|
||||
| Bug | `code-review-find-bug` |
|
||||
| Security | `code-review-find-security` |
|
||||
| Performance | `code-review-find-performance` |
|
||||
| Quality | `code-review-find-quality` |
|
||||
|
||||
Do NOT tell a finder what the other finders are doing. Each works in
|
||||
isolation; the dedupe stage handles overlap.
|
||||
|
||||
Do NOT load the skills yourself.
|
||||
|
||||
## Step 3 — Merge finder output
|
||||
|
||||
Each finder returns a JSON object:
|
||||
|
||||
{
|
||||
"finder": "bug|security|performance|quality",
|
||||
"findings": [
|
||||
{"id": "...", "severity": "...", "location": "...",
|
||||
"claim": "...", "evidence": "...", "fix": "..."}
|
||||
]
|
||||
}
|
||||
|
||||
Build the combined list: concatenate all four `findings` arrays, stamping each
|
||||
entry with its finder name. Ids are already namespaced (`bug-N`, `sec-N`,
|
||||
`perf-N`, `q-N`) — no renumbering.
|
||||
|
||||
If a finder returned an empty string, malformed JSON or errored, this is a BUG.
|
||||
Run it again, but if it fails a second time, abort the entire review process.
|
||||
|
||||
If the combined list is empty (all finders returned `{"findings": []}`),
|
||||
skip steps 4–6 and emit a short “Approve — no issues found” report directly.
|
||||
|
||||
## Step 4 — Spawn verify (serial)
|
||||
|
||||
One `task` call with skill `code-review-verify`. Pass the skill and the merged
|
||||
findings list as JSON in the prompt. The skill body carries the verifier’s
|
||||
identity and instructions. Example prompt:
|
||||
|
||||
Load the `code-review-verify` skill.
|
||||
|
||||
---
|
||||
|
||||
{
|
||||
"findings": [
|
||||
/* findings goes here */
|
||||
]
|
||||
}
|
||||
|
||||
The verifier returns:
|
||||
|
||||
{
|
||||
"verified": [
|
||||
{"id": "...", "verdict": "confirmed|refuted|uncertain",
|
||||
"reason": "...", "severity_override": "..."}
|
||||
]
|
||||
}
|
||||
|
||||
## Step 5 — Spawn dedupe (serial)
|
||||
|
||||
One `task` call with skill `code-review-dedupe`. Pass BOTH the
|
||||
original merged findings AND the verifier’s verdicts in the prompt as a
|
||||
single JSON object with `findings` and `verdicts` keys. Example:
|
||||
|
||||
Load the `code-review-dedupe` skill.
|
||||
|
||||
---
|
||||
|
||||
/* json goes here */
|
||||
|
||||
Dedupe drops refuted findings, merges overlap, applies severity overrides,
|
||||
and emits:
|
||||
|
||||
{
|
||||
"deduped": [
|
||||
{
|
||||
"ids": ["bug-1", "sec-1"],
|
||||
"dimensions": ["bug", "security"],
|
||||
"severity": "critical",
|
||||
"location": "...",
|
||||
"claim": "...",
|
||||
"evidence": "...",
|
||||
"fix": "...",
|
||||
"verdict": "confirmed|uncertain",
|
||||
"uncertain_reason": null
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"input_findings": 12,
|
||||
"refuted": 4,
|
||||
"after_clustering": 6,
|
||||
"by_severity": {"critical": 1, "major": 2, "minor": 2, "nit": 1},
|
||||
"uncertain": 1,
|
||||
"finder_failures": []
|
||||
}
|
||||
}
|
||||
|
||||
## Step 6 — Spawn synthesize (serial)
|
||||
|
||||
One `task` call with skill `code-review-sanity`. Pass dedupe’s JSON
|
||||
output verbatim in the prompt.
|
||||
|
||||
Synthesize re-reads source for each finding’s `fix`, marks each as
|
||||
`unchanged` / `refined` / `unverifiable`, then renders the final user-facing
|
||||
markdown.
|
||||
|
||||
## Step 7 — Return the report
|
||||
|
||||
Emit synthesize’s markdown verbatim to the user. Do NOT wrap it in commentary
|
||||
— the report is self-contained.
|
||||
|
||||
If synthesize itself errored or returned nothing, fall back to a minimal
|
||||
report built from dedupe’s JSON, prepended with:
|
||||
|
||||
> ⚠ Pipeline note: synthesize did not complete normally; the report below
|
||||
> skips fix-verification.
|
||||
|
||||
## Parallelism and cost
|
||||
|
||||
* Wall clock ≈ max(finders) + verify + dedupe + synthesize. Finders run in
|
||||
parallel; the rest is serial.
|
||||
* Token cost ≈ 6–8× a single-pass review. Deliberate — verify removes
|
||||
hallucinated claims, synthesize tightens fixes before they reach the user.
|
||||
* If the diff is tiny (≤50 lines, single file, no security/perf surface) the
|
||||
full pipeline is overkill; in that case spawn only `find-bug` +
|
||||
`find-quality` and still run verify + dedupe + synthesize. Default to the
|
||||
full pipeline when in doubt.
|
||||
|
||||
## Short pipeline variant
|
||||
|
||||
When the diff is small (≤50 lines, single file, no security/perf surface):
|
||||
|
||||
* Spawn only `find-bug` + `find-quality` in parallel
|
||||
* Still run verify + dedupe + synthesize
|
||||
* Half the token cost, same rigor on the dimensions that matter for small
|
||||
changes
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: diagnosing-bugs
|
||||
description: Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow.
|
||||
---
|
||||
|
||||
# Diagnosing Bugs
|
||||
|
||||
A discipline for hard bugs. Skip phases only when explicitly justified.
|
||||
|
||||
When exploring the codebase, read `CONTEXT.md` (if it exists) to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.
|
||||
|
||||
## Redact
|
||||
|
||||
This skill has you show commands, outputs and captured artifacts. **Redact every secret first**: write `<REDACTED>` in its place. Build loops against env vars, so the credential stays in the environment rather than in what you show. Captured artifacts carry auth headers: quote only the lines that carry the signal.
|
||||
|
||||
If the redacted output is not enough to diagnose the bug, say so and ask the user.
|
||||
|
||||
## Phase 1: Build a feedback loop
|
||||
|
||||
**This is the skill.** Everything else is mechanical. If you have a **tight** pass/fail signal for the bug (one that goes red on _this_ bug), you will find the cause; bisection, hypothesis-testing, and instrumentation all just consume it. If you don't have one, no amount of staring at code will save you.
|
||||
|
||||
Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**
|
||||
|
||||
### Ways to construct one, in roughly this order
|
||||
|
||||
1. **Failing test** at whatever seam reaches the bug: unit, integration, e2e.
|
||||
2. **Curl / HTTP script** against a running dev server.
|
||||
3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot.
|
||||
4. **Headless browser script** (Playwright / Puppeteer) that drives the UI and asserts on DOM/console/network.
|
||||
5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation.
|
||||
6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call.
|
||||
7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode.
|
||||
8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it.
|
||||
9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs.
|
||||
10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you.
|
||||
|
||||
Build the right feedback loop, and the bug is 90% fixed.
|
||||
|
||||
### Tighten the loop
|
||||
|
||||
Treat the loop as a product. Once you have _a_ loop, **tighten** it:
|
||||
|
||||
- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.)
|
||||
- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".)
|
||||
- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.)
|
||||
|
||||
A 30-second flaky loop is barely better than no loop; a 2-second deterministic one is tight, a debugging superpower.
|
||||
|
||||
### Non-deterministic bugs
|
||||
|
||||
The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not, so keep raising the rate until it's debuggable.
|
||||
|
||||
### When you genuinely cannot build a loop
|
||||
|
||||
Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a redacted captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop.
|
||||
|
||||
### Completion criterion: a tight loop that goes red
|
||||
|
||||
Phase 1 is done when the loop is **tight** and **red-capable**: you can name **one command** (a script path, a test invocation, a curl) that you have **already run at least once** (show the invocation and its output, redacted), and that is:
|
||||
|
||||
- [ ] **Red-capable**: it drives the actual bug code path and asserts the **user's exact symptom**, so it can go red on this bug and green once fixed. Not "runs without erroring"; it must be able to _catch this specific bug_.
|
||||
- [ ] **Deterministic**: same verdict every run (flaky bugs: a pinned, high reproduction rate, per above).
|
||||
- [ ] **Fast**: seconds, not minutes.
|
||||
- [ ] **Agent-runnable**: you can run it unattended; a human in the loop only via `scripts/hitl-loop.template.sh`.
|
||||
|
||||
If you catch yourself reading code to build a theory before this command exists, **stop: jumping straight to a hypothesis is the exact failure this skill prevents.** No red-capable command, no Phase 2.
|
||||
|
||||
## Phase 2: Reproduce + minimise
|
||||
|
||||
Run the loop. Watch it go red as the bug appears.
|
||||
|
||||
Confirm:
|
||||
|
||||
- [ ] The loop produces the failure mode the **user** described, not a different failure that happens to be nearby. Wrong bug = wrong fix.
|
||||
- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against).
|
||||
- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it.
|
||||
|
||||
### Minimise
|
||||
|
||||
Once it's red, shrink the repro to the **smallest scenario that still goes red**. Cut inputs, callers, config, data, and steps **one at a time**, re-running the loop after each cut, and keep only what's load-bearing for the failure.
|
||||
|
||||
Why bother: a minimal repro shrinks the hypothesis space in Phase 3 (fewer moving parts left to suspect) and becomes the clean regression test in Phase 5.
|
||||
|
||||
Done when **every remaining element is load-bearing**: removing any one of them makes the loop go green.
|
||||
|
||||
Do not proceed until you have reproduced **and** minimised.
|
||||
|
||||
## Phase 3: Hypothesise
|
||||
|
||||
Generate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea.
|
||||
|
||||
Each hypothesis must be **falsifiable**: state the prediction it makes.
|
||||
|
||||
> Format: "If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse."
|
||||
|
||||
If you cannot state the prediction, the hypothesis is a vibe: discard or sharpen it.
|
||||
|
||||
**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it; proceed with your ranking if the user is AFK.
|
||||
|
||||
## Phase 4: Instrument
|
||||
|
||||
Each probe must map to a specific prediction from Phase 3. **Change one variable at a time.**
|
||||
|
||||
Tool preference:
|
||||
|
||||
1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs.
|
||||
2. **Targeted logs** at the boundaries that distinguish hypotheses.
|
||||
3. Never "log everything and grep".
|
||||
|
||||
**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die.
|
||||
|
||||
**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second.
|
||||
|
||||
## Phase 5: Fix + regression test
|
||||
|
||||
Write the regression test **before the fix**, but only if there is a **correct seam** for it.
|
||||
|
||||
A correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence.
|
||||
|
||||
**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase.
|
||||
|
||||
If a correct seam exists:
|
||||
|
||||
1. Turn the minimised repro into a failing test at that seam.
|
||||
2. Watch it fail.
|
||||
3. Apply the fix.
|
||||
4. Watch it pass.
|
||||
5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario.
|
||||
|
||||
## Phase 6: Cleanup
|
||||
|
||||
Required before declaring done:
|
||||
|
||||
- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)
|
||||
- [ ] Regression test passes (or absence of seam is documented)
|
||||
- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)
|
||||
- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)
|
||||
- [ ] The hypothesis that turned out correct is stated in the commit / PR message, so the next debugger learns
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: gitea
|
||||
description: Provides information and workflows for interacting with Gitea.
|
||||
license: MIT
|
||||
compatibility: opencode
|
||||
metadata:
|
||||
audience: agents
|
||||
workflow: gitea-development
|
||||
---
|
||||
|
||||
## What Gitea is
|
||||
Gitea is a self-hosted Git service that is very similar to GitHub.
|
||||
|
||||
## Important Notes
|
||||
- **Do NOT use gitea.com**: We self-host Gitea at `git.nubo.sh`.
|
||||
- **Workflow**: The workflow is much the same as GitHub (issues, PRs, etc.).
|
||||
|
||||
## How to use the `tea` CLI
|
||||
Use the `tea` command to interact with Gitea.
|
||||
- `tea notifications`: Check for new notifications.
|
||||
- `tea issue <number>`: Read an issue.
|
||||
- `tea issue --comments <number>`: Read an issue and its comments.
|
||||
- `tea comment <number> "message"`: Comment on an issue.
|
||||
- `tea pulls`: Manage pull requests.
|
||||
- `tea pulls create`: Create a pull request.
|
||||
- `tea pulls <number>`: Read a pull request (e.g., `tea pulls 6`).
|
||||
- `tea pulls --comments <number>`: Read a pull request and its comments.
|
||||
- **Marking as Read**: Use `tea notifications read` to mark notifications as read.
|
||||
- **When to mark as read**: Only mark notifications as read once you have actively addressed the underlying task or review.
|
||||
|
||||
## Development Workflows
|
||||
- **Issues**: Use `tea issue` to track tasks and bugs. Before closing an issue, ensure the relevant Pull Request has been merged.
|
||||
- **Pull Requests**: Use `tea pulls` to manage and review code changes.
|
||||
- **Communication**: Use `tea comment <number> "message"` to communicate with project maintainers (e.g., `@hulthe`).
|
||||
|
||||
## Responsibility
|
||||
- **Don't Drop the Ball**: When assigned a task or receiving a review comment, you MUST address it before moving on to other things. Ensure all follow-up actions ( (like replying to a comment) are completed.
|
||||
|
||||
## Tips for Agents
|
||||
- **Stay Updated**: Regularly run `tea notifications` to check for new issues or PR reviews.
|
||||
- **Communicate**: If confused, use `tea comment <number> "message"` and mention the maintainer (e.g., `@hulthe`).
|
||||
- **Clean History**: Use `git rebase` and squash commits before submitting a PR to keep the history clean.
|
||||
- **Use Tea**: Use the `tea` CLI for a faster, terminal-based workflow.
|
||||
- **Explicitly Ping**: When replying to a comment, use the `@` sign to explicitly ping or mention the person you are replying to. This ensures they receive a notification.
|
||||
- **CI / Actions**: Gitea uses `.gitea/workflows/` (not `.github/`). Use `tea actions runs` to list runs; `tea actions runs logs <id>` to inspect. A `Status: completed` does NOT mean success — always read the log tail for `Job succeeded` or `Job failed`. The Alpine runner requires `node` before `actions/checkout` and uses package names `rust-clippy` / `rustfmt` (not `clippy`).
|
||||
|
||||
## Assignment
|
||||
- **Tracking Assignments**: Use `tea issue --assignee <your-username>` to see issues assigned to you. Also use `tea pulls --assignee <your-username>` for pull requests.
|
||||
- **Checking Work**: Regularly check your notifications and assigned issues to ensure you are up to date with your tasks.
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: grilling
|
||||
description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
|
||||
---
|
||||
|
||||
Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.
|
||||
|
||||
Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled: the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.
|
||||
|
||||
Format a round like so:
|
||||
|
||||
```
|
||||
❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
|
||||
|
||||
➡ <your recommended answer>
|
||||
|
||||
---
|
||||
|
||||
❓ **Q2** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
|
||||
|
||||
➡ <your recommended answer>
|
||||
```
|
||||
|
||||
Each round the user answers reshapes the tree: settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.
|
||||
|
||||
Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it; don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report; ask the rest of the frontier now. The _decisions_ are the user's: put each to them and wait.
|
||||
|
||||
The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding.
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: rust-best-practices
|
||||
description: >
|
||||
Best-practices for Rust software. Load into a workstream whenever you need to read or write a Rust (.rs) file.
|
||||
---
|
||||
|
||||
# Best practices for programming Rust
|
||||
- Run `cargo check`, `cargo clippy`, and `cargo clippy` before comitting to git.
|
||||
- Don't ignore warnings from cargo.
|
||||
- Don't supress warnings using e.g. `#[allow(warning)]`
|
||||
- Never use `unsafe` without asking the user for explicit permission.
|
||||
|
||||
When setting up a new rust project, enable these lints:
|
||||
|
||||
```toml
|
||||
[lints.clippy]
|
||||
allow_attributes = "warn"
|
||||
as_ptr_cast_mut = "warn"
|
||||
as_underscore = "warn"
|
||||
borrow_as_ptr = "warn"
|
||||
implicit_clone = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
unicode_not_nfc = "warn"
|
||||
unused_async = "deny"
|
||||
wildcard_dependencies = "deny"
|
||||
|
||||
[lints.rust]
|
||||
absolute_paths_not_starting_with_crate = "deny"
|
||||
explicit_outlives_requirements = "warn"
|
||||
macro_use_extern_crate = "deny"
|
||||
missing_abi = "deny"
|
||||
non_ascii_idents = "forbid"
|
||||
rust_2018_idioms = { level = "deny", priority = -1 }
|
||||
single_use_lifetimes = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
unused_macro_rules = "warn"
|
||||
```
|
||||
|
||||
## Newtypes and "Parse, Don't Validate"
|
||||
|
||||
Use newtypes to make invalid states unrepresentable. Instead of passing raw strings and validating at every use site, parse once into a validated type.
|
||||
|
||||
### Pattern
|
||||
|
||||
```rust
|
||||
pub struct Email(String); // private field prevents construction without validation
|
||||
|
||||
impl Email {
|
||||
pub fn parse(s: &str) -> Result<Self, ParseError> {
|
||||
if !s.contains('@') {
|
||||
return Err(ParseError::InvalidEmail);
|
||||
}
|
||||
Ok(Self(s.to_owned()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Guidelines
|
||||
|
||||
- **Private fields**: Force construction through `parse()` only
|
||||
- **Parse at boundaries**: Validate at system edges (API handlers, CLI args, config loading)
|
||||
- **Use the type in signatures**: `fn create_user(email: Email)` not `fn create_user(email: &str)`
|
||||
- **No re-validation**: Once parsed, the type guarantees validity
|
||||
|
||||
### When to Apply
|
||||
|
||||
- Email addresses, passwords, phone numbers
|
||||
- File paths, URLs, identifiers
|
||||
- Any domain value with invariants
|
||||
- Configuration values requiring validation
|
||||
|
||||
### Anti-pattern to Avoid
|
||||
|
||||
```rust
|
||||
// BAD: validation scattered, easy to forget
|
||||
fn handler(email: &str) {
|
||||
if !validate_email(email) { return Err(...); }
|
||||
create_user(email) // still just &str, could be invalid
|
||||
}
|
||||
```
|
||||
Reference in new issue
Block a user