Skip to content

feat(review): prompt templates, structured review output, opt-in stop review gate - #21

Merged
freema merged 3 commits into
mainfrom
feat/review-chain
Jul 28, 2026
Merged

feat(review): prompt templates, structured review output, opt-in stop review gate#21
freema merged 3 commits into
mainfrom
feat/review-chain

Conversation

@freema

@freema freema commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces #19, which was auto-closed when its stacked base branch (feat/session-lifecycle, #18) was deleted on merge. Content is identical, now rebased on main; see #19 for the original description and discussion.

  • Prompt templates in prompts/*.md + lib/prompts.mjs interpolator (prompts/ excluded from prettier — reflow was rewriting model-facing whitespace).
  • Structured review output per schemas/review-output.schema.json, hand-rolled validation in lib/review-output.mjs, stored on the job record as review.
  • Opt-in per-repo stop review gate (/cursor:setup --enable-review-gate, Stop hook, ALLOW:/BLOCK: contract, loop guard via stop_hook_active, degrade-to-note when Cursor is missing/logged out).
  • Per-repo config in lib/config.mjs.

Test plan

  • npm test: 123/123 green after merging main in. npm run lint + npm run typecheck: clean.

freema and others added 3 commits July 28, 2026 15:30
…, --json output

Modelled on openai/codex-plugin-cc's session tracking, adapted to the
zero-deps runtime:

- hooks/hooks.json + scripts/session-hook.mjs: SessionStart exports the
  Claude session id (and CLAUDE_PLUGIN_DATA) into CLAUDE_ENV_FILE;
  SessionEnd cancels the session's still-running jobs so a closed session
  no longer leaves detached workers and cursor-agent running unattended.
  Other sessions' jobs and unattributed jobs are never touched.
- createJob stamps sessionId from CURSOR_PLUGIN_CC_SESSION_ID
- /cursor:status default view scoped to the current session (plus
  unattributed jobs) with a hidden-rows hint; --all lifts scope and cap;
  new lib/jobs.mjs#filterJobsForSession
- --json on status/result/setup for scripting (raw job records / doctor
  report); setup's doctor split into gather + render
- pluginHome(): CURSOR_PLUGIN_CC_HOME > existing ~/.cursor-plugin-cc >
  CLAUDE_PLUGIN_DATA/state > ~/.cursor-plugin-cc; jobs/<repo-hash>/
  layout unchanged
- tests: session stamping, session filter, hook handlers (env-file write,
  selective cancel), state-root precedence
- docs: README session-lifecycle section, AGENTS.md state-root rule,
  CHANGELOG

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… review gate

Ported from openai/codex-plugin-cc's review chain, adapted to the
zero-deps runtime:

- prompts/*.md: review prompt moved out of JS string arrays into
  reviewable templates ({{UPPER_SNAKE}} placeholders, new lib/prompts.mjs
  loader/interpolator with blank-line collapse); prompts/ excluded from
  prettier so template whitespace stays intentional
- schemas/review-output.schema.json + lib/review-output.mjs: reviews
  append a fenced json verdict block (verdict/summary/findings/next_steps,
  hand-rolled validation); parsed data stored on the job record as
  `review`, raw fence stripped from the human-facing summary; parsing
  never fails a review
- stop review gate (opt-in, per repo): Stop hook runs a Cursor review of
  the turn with an ALLOW:/BLOCK: first-line contract; BLOCK surfaces
  {"decision":"block"} so the turn continues. stop_hook_active
  short-circuits (no loops); missing/logged-out Cursor degrades to a
  stderr note; also warns about still-running jobs on stop
- lib/config.mjs: per-repo config at <state-root>/config/<repo-hash>.json;
  /cursor:setup --enable-review-gate / --disable-review-gate toggle, gate
  status shown in doctor/--json
- test stub understands `cursor-agent status` (CURSOR_AGENT_STUB_AUTH)
- tests: prompts, config, review-output parser, gate hook
  (allow/block/loop-guard/logged-out)
@freema
freema merged commit 9c8168a into main Jul 28, 2026
6 checks passed
@freema
freema deleted the feat/review-chain branch July 28, 2026 13:59

@freema freema left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeForge Review

Verdict: request_changes | Score: 6/10

Well-structured PR (prompt templates externalized, structured review output, opt-in stop-review gate) with good test coverage for the pure-logic pieces. However, the stop-review-gate hook reads a last_assistant_message field from the Claude Code Stop hook payload that the harness does not actually provide (the real payload exposes transcript_path, not the message text), so the 'Previous Claude response' context the design and CHANGELOG describe will silently never populate in production. A few smaller consistency/coverage gaps round out the review.


Reviewed by CodeForge

*/
async function runGateReview(input, root) {
const template = loadPromptTemplate('stop-review-gate');
const last = String(input.last_assistant_message ?? '').trim();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] runGateReview reads input.last_assistant_message to build the CLAUDE_RESPONSE_BLOCK context for the reviewer prompt, but Claude Code's documented Stop hook JSON payload only includes session_id, transcript_path, cwd, hook_event_name, and stop_hook_active — there is no last_assistant_message field. In real usage this will always be undefined, so last is always '' and CLAUDE_RESPONSE_BLOCK interpolates to an empty string every time, even though the CHANGELOG/README describe the gate as reviewing 'that turn's edits' with 'Previous Claude response: …' context. The gate will still run (grounded only in whatever the reviewer inspects on its own via tools), but the intended context-passing never actually works.

Suggestion: Read input.transcript_path (the JSONL transcript Claude Code writes) and extract the last assistant message from it, the way other hook-consuming tools do, instead of assuming the message is inlined in the hook's stdin JSON. Add a gate.test.mjs case that constructs input the way the real harness does (transcript_path pointing at a fixture JSONL) to catch this kind of payload-shape drift.

"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,

Copy link
Copy Markdown
Owner Author

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: false at both the top level and per-finding, but the hand-rolled validator in scripts/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.

Suggestion: Either drop additionalProperties: false from the schema (since it's documentation-only and not enforced) or add an explicit extra-keys check in the validator so the two stay in sync.

* @param {boolean} enable
* @returns {Promise<number>}
*/
async function toggleReviewGate(enable) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] No test exercises toggleReviewGate/the --enable-review-gate/--disable-review-gate CLI paths in setup.mjs; config.test.mjs only covers the underlying lib/config.mjs functions directly, not the command wiring (flag parsing, stdout messaging, or the new 'stop review gate' doctor check).

Suggestion: Add a setup.mjs test (or extend an existing one) that runs main(['--enable-review-gate']) / main(['--disable-review-gate']) and asserts both the persisted config and the printed confirmation text.

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');

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] interpolateTemplate's blank-line collapse (/\n{3,}/g\n\n) runs over the fully-assembled template, including the interpolated BODY (the raw diff/file content passed into the review prompt). Any diff hunk that legitimately contains 3+ consecutive blank lines gets silently collapsed to 2, so the text handed to the reviewer is not byte-for-byte the diff that was collected.

Suggestion: Collapse blank-line runs only around known optional placeholders (e.g. run the collapse before substituting BODY, or scope it to lines adjacent to a placeholder) rather than over the final interpolated string, so arbitrary diff content is never mutated.

freema added a commit that referenced this pull request Jul 28, 2026
…roup cancel (#22)

Cut the 0.5.0 changelog section covering #17/#18/#20/#21 and bump the
version in package.json, package-lock.json, and plugin.json.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant