-
Notifications
You must be signed in to change notification settings - Fork 5
feat(review): prompt templates, structured review output, opt-in stop review gate #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,3 +4,4 @@ coverage | |
| package-lock.json | ||
| *.ndjson | ||
| .cursor-plugin-cc | ||
| prompts/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,11 @@ | ||
| --- | ||
| description: Health-check Cursor CLI, list models, or guide installation. | ||
| argument-hint: '[--doctor] [--print-models] [--install] [--json]' | ||
| description: Health-check Cursor CLI, list models, guide installation, or toggle the stop-time review gate. | ||
| argument-hint: '[--doctor] [--print-models] [--install] [--json] [--enable-review-gate|--disable-review-gate]' | ||
| allowed-tools: Bash(node:*) | ||
| --- | ||
|
|
||
| !`node "${CLAUDE_PLUGIN_ROOT}/scripts/setup.mjs" -- "$ARGUMENTS"` | ||
|
|
||
| Present the check results as-is. If any check failed, tell the user concretely what to do (install cursor-agent, run `cursor-agent login`, run `npm install` inside the plugin). Never attempt to run the installer yourself. | ||
|
|
||
| `--enable-review-gate` / `--disable-review-gate` toggle the per-repo stop-time review gate: when enabled, a Cursor model reviews each turn's edits before Claude Code is allowed to stop, and a `BLOCK` verdict keeps the turn going. Relay the confirmation message verbatim. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| {{ROLE}} | ||
|
|
||
| **Review target:** {{REVIEW_TARGET}} | ||
|
|
||
| {{FOCUS_BLOCK}} | ||
|
|
||
| Review ONLY the changes below — not the entire codebase. | ||
|
|
||
| {{BODY}} | ||
|
|
||
| --- | ||
|
|
||
| **How to respond:** | ||
|
|
||
| - Group findings by severity: **Blocking**, **Should-fix**, **Nits**. | ||
| - For each finding: `path:line` — what is wrong, why it matters, and a concrete fix (described, not applied). | ||
| - Flag correctness bugs, security holes, missing error handling, broken or missing tests, and deviations from the repo conventions (read `AGENTS.md` / `.cursor/rules` / `CLAUDE.md` if present). | ||
| {{ADVERSARIAL_GUIDANCE}} | ||
| - End with a one-line verdict on its own line: **APPROVE**, **APPROVE WITH NITS**, or **REQUEST CHANGES**. | ||
| - After the verdict, append a fenced ```json code block that restates the review as machine-readable data (no new content), shaped exactly like this: | ||
|
|
||
| ```json | ||
| { | ||
| "verdict": "approve | approve-with-nits | request-changes", | ||
| "summary": "one-sentence overall assessment", | ||
| "findings": [ | ||
| { | ||
| "severity": "blocking | should-fix | nit", | ||
| "title": "short finding title", | ||
| "file": "path/relative/to/repo", | ||
| "line": 42, | ||
| "body": "what is wrong and why it matters", | ||
| "recommendation": "the concrete fix, described" | ||
| } | ||
| ], | ||
| "next_steps": ["optional follow-up actions"] | ||
| } | ||
| ``` | ||
|
|
||
| **Hard constraints:** | ||
|
|
||
| - This is a **READ-ONLY review.** Do NOT modify, create, or delete any files. Do NOT run commands that change state. Do NOT stage or commit anything. If you spot a bug, describe the fix — never apply it. | ||
| - If the diff above was truncated, you MAY read specific files for context (read-only), but never edit them. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| <task> | ||
| Run a stop-gate review of the previous Claude turn. | ||
| Only review the work from the previous Claude turn. | ||
| Only review it if Claude actually made code changes in that turn. | ||
| Pure status, setup, or reporting output does not count as reviewable work. | ||
| For example, the output of /cursor:setup or /cursor:status does not count. | ||
| Only direct edits made in that specific turn count. | ||
| If the previous Claude turn was only a status update, a summary, a setup/login check, a review result, or output from a command that did not itself make direct edits in that turn, return ALLOW immediately and do no further work. | ||
| Challenge whether that specific work and its design choices should ship. | ||
|
|
||
| {{CLAUDE_RESPONSE_BLOCK}} | ||
| </task> | ||
|
|
||
| <compact_output_contract> | ||
| Return a compact final answer. | ||
| Your first line must be exactly one of: | ||
| - ALLOW: <short reason> | ||
| - BLOCK: <short reason> | ||
| Do not put anything before that first line. | ||
| </compact_output_contract> | ||
|
|
||
| <default_follow_through_policy> | ||
| Use ALLOW if the previous turn did not make code changes or if you do not see a blocking issue. | ||
| Use ALLOW immediately, without extra investigation, if the previous turn was not an edit-producing turn. | ||
| Use BLOCK only if the previous turn made code changes and you found something that still needs to be fixed before stopping. | ||
| </default_follow_through_policy> | ||
|
|
||
| <grounding_rules> | ||
| Ground every blocking claim in the repository context or tool outputs you inspected during this run. | ||
| Do not treat the previous Claude response as proof that code changes happened; verify that from the repository state before you block. | ||
| Do not block based on older edits from earlier turns when the immediately previous turn did not itself make direct edits. | ||
| </grounding_rules> | ||
|
|
||
| <dig_deeper_nudge> | ||
| If the previous turn did make code changes, check for second-order failures, empty-state behavior, retries, stale state, rollback risk, and design tradeoffs before you finalize. | ||
| </dig_deeper_nudge> | ||
|
|
||
| <hard_constraints> | ||
| This is a READ-ONLY check. Do NOT modify, create, or delete any files. Do NOT run commands that change state. Do NOT stage or commit anything. | ||
| </hard_constraints> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| { | ||
| "$schema": "https://json-schema.org/draft/2020-12/schema", | ||
| "title": "Cursor review structured output", | ||
| "description": "The fenced ```json block a /cursor:review run appends after its Markdown findings. Parsed by scripts/lib/review-output.mjs and stored on the job record as `review`.", | ||
| "type": "object", | ||
| "additionalProperties": false, | ||
| "required": ["verdict", "summary", "findings"], | ||
| "properties": { | ||
| "verdict": { | ||
| "type": "string", | ||
| "enum": ["approve", "approve-with-nits", "request-changes"] | ||
| }, | ||
| "summary": { | ||
| "type": "string", | ||
| "minLength": 1 | ||
| }, | ||
| "findings": { | ||
| "type": "array", | ||
| "items": { | ||
| "type": "object", | ||
| "additionalProperties": false, | ||
| "required": ["severity", "title", "file", "body"], | ||
| "properties": { | ||
| "severity": { | ||
| "type": "string", | ||
| "enum": ["blocking", "should-fix", "nit"] | ||
| }, | ||
| "title": { | ||
| "type": "string", | ||
| "minLength": 1 | ||
| }, | ||
| "file": { | ||
| "type": "string", | ||
| "minLength": 1 | ||
| }, | ||
| "line": { | ||
| "type": ["integer", "null"], | ||
| "minimum": 1 | ||
| }, | ||
| "body": { | ||
| "type": "string", | ||
| "minLength": 1 | ||
| }, | ||
| "recommendation": { | ||
| "type": "string" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "next_steps": { | ||
| "type": "array", | ||
| "items": { | ||
| "type": "string", | ||
| "minLength": 1 | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // Per-repo plugin config, persisted at `<state-root>/config/<repo-hash>.json` | ||
| // (deliberately outside jobs/<repo-hash>/ so config never mixes with job | ||
| // records). Currently the only key is the stop-review-gate toggle. | ||
|
|
||
| import { readFileSync, writeFileSync } from 'node:fs'; | ||
| import { join } from 'node:path'; | ||
| import { ensureDir, pluginHome, repoHash } from './paths.mjs'; | ||
|
|
||
| const DEFAULTS = Object.freeze({ stopReviewGate: false }); | ||
|
|
||
| /** | ||
| * @param {string} repoPath | ||
| * @returns {string} | ||
| */ | ||
| export function configPath(repoPath) { | ||
| return join(pluginHome(), 'config', `${repoHash(repoPath)}.json`); | ||
| } | ||
|
|
||
| /** | ||
| * @param {string} repoPath | ||
| * @returns {{stopReviewGate: boolean}} | ||
| */ | ||
| export function getConfig(repoPath) { | ||
| try { | ||
| return { ...DEFAULTS, ...JSON.parse(readFileSync(configPath(repoPath), 'utf8')) }; | ||
| } catch { | ||
| return { ...DEFAULTS }; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @param {string} repoPath | ||
| * @param {string} key | ||
| * @param {unknown} value | ||
| * @returns {{stopReviewGate: boolean}} | ||
| */ | ||
| export function setConfigValue(repoPath, key, value) { | ||
| ensureDir(join(pluginHome(), 'config')); | ||
| const next = { ...getConfig(repoPath), [key]: value }; | ||
| writeFileSync(configPath(repoPath), JSON.stringify(next, null, 2) + '\n', 'utf8'); | ||
| return next; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| // Prompt templates live in `<plugin-root>/prompts/*.md` so they can be | ||
| // reviewed and diffed as prose instead of JS string arrays. Placeholders use | ||
| // `{{UPPER_SNAKE}}`; unknown keys interpolate to the empty string. | ||
|
|
||
| import { readFileSync } from 'node:fs'; | ||
| import { dirname, join } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| // lib/ → scripts/ → plugin root | ||
| const PLUGIN_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url)))); | ||
|
|
||
| /** | ||
| * @param {string} name Template basename without extension (e.g. "review"). | ||
| * @returns {string} | ||
| */ | ||
| export function loadPromptTemplate(name) { | ||
| return readFileSync(join(PLUGIN_ROOT, 'prompts', `${name}.md`), 'utf8'); | ||
| } | ||
|
|
||
| /** | ||
| * @param {string} template | ||
| * @param {Record<string, string|undefined>} variables | ||
| * @returns {string} | ||
| */ | ||
| export function interpolateTemplate(template, variables) { | ||
| const filled = template.replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => | ||
| Object.prototype.hasOwnProperty.call(variables, key) ? String(variables[key] ?? '') : '', | ||
| ); | ||
| // Optional blocks interpolate to '' and would leave runs of blank lines. | ||
| return filled.replace(/\n{3,}/g, '\n\n'); | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION]
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[MINOR] The schema declares
additionalProperties: falseat both the top level and per-finding, but the hand-rolled validator inscripts/lib/review-output.mjs(isValidFinding,parseReviewOutput) never rejects objects with unexpected extra keys — it only checks that the required fields exist and have the right type. The schema and the actual runtime contract have quietly diverged.