Skip to content

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

Closed
freema wants to merge 1 commit into
feat/session-lifecyclefrom
feat/review-chain
Closed

feat(review): prompt templates, structured review output, opt-in stop review gate#19
freema wants to merge 1 commit into
feat/session-lifecyclefrom
feat/review-chain

Conversation

@freema

@freema freema commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

Third of four PRs porting the high-value patterns from openai/codex-plugin-cc. Stacked on #18 (extends its hooks/hooks.json); merge #18 first, then retarget this to main.

  • Prompt templates — the review prompt moves from a JS string array to prompts/review.md; new lib/prompts.mjs ({{UPPER_SNAKE}} interpolation, blank-line collapse for empty optional blocks). Prompts are now reviewable as prose. prompts/ is excluded from prettier — markdown reflow was rewriting model-facing whitespace (it even turned a --- separator into a setext heading).
  • Structured review output — reviews append a fenced ```json verdict block per schemas/review-output.schema.json (verdict, findings with severity/file/line, next steps). Hand-rolled validation in `lib/review-output.mjs` (zero-deps). On success the data lands on the job record (`review`, visible via `/cursor:result --json`) and the JSON fence is stripped from the human-facing summary. Models that ignore the instruction just produce an unstructured review — parsing never fails a run.
  • Stop review gate (opt-in, per repo, off by default) — /cursor:setup --enable-review-gate. A Stop hook has a Cursor model review the turn's edits (prompts/stop-review-gate.md, first-line ALLOW:/BLOCK: contract); a BLOCK emits {"decision":"block","reason":…} so Claude keeps working. Hardening beyond the codex original: stop_hook_active short-circuit (a block can never loop), and missing/logged-out Cursor degrades to a stderr note instead of trapping the session. The hook also warns about still-running jobs at stop time.
  • Per-repo config — new lib/config.mjs at <state-root>/config/<repo-hash>.json (outside jobs/), gate status surfaced in --doctor / --json.

Test plan

  • New tests/prompts.test.mjs, tests/config.test.mjs, tests/review-output.test.mjs (valid/invalid blocks, last-block wins, strip), tests/gate.test.mjs (ALLOW silent, BLOCK decision JSON, loop guard, disabled no-op, logged-out degrade; stub extended with a status auth mode).
  • npm test: 119/119 green. npm run lint: clean.

… 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 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 feature (stop review gate, structured review output, externalized prompt templates) with solid unit test coverage for the new lib modules, but the gate's core context-passing likely doesn't work against Claude Code's real Stop hook payload, and there are a few test-coverage and duplication gaps in the new wiring.

General Issues

  • [MINOR] The Stop hook now runs unconditionally on every turn for every plugin user, even when the review gate is disabled (the previous default), adding new per-turn subprocess/git overhead that didn't exist before.

    Suggestion: Short-circuit earlier when no config exists for the repo, or document the added per-turn cost.

  • [SUGGESTION] Schema declares additionalProperties: false but the hand-rolled validator never enforces it, so the documented contract is stricter than what's actually validated.

    Suggestion: Either relax the schema annotation or add an unknown-key check to the validator.


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] Reads input.last_assistant_message, a field Claude Code's Stop hook JSON payload does not appear to provide (the documented schema is session_id/transcript_path/cwd/hook_event_name/stop_hook_active). In production this is likely always empty, so the reviewer never gets the previous turn's response text.

Suggestion: Parse the transcript at input.transcript_path to extract the last assistant message instead of reading a non-existent field.


const sessionId =
(typeof input.session_id === 'string' && input.session_id) || process.env[SESSION_ID_ENV];
const running = listJobs(root).filter(

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] Reimplements session-scoped running-job filtering inline instead of reusing filterJobsForSession/findRunningJobs from lib/jobs.mjs, risking drift between the two implementations.

Suggestion: Use filterJobsForSession(findRunningJobs(root), sessionId).

return { bin, checks, mcps, allOk };
}

/**

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] toggleReviewGate and the new doctor check have no test coverage anywhere in the diff.

Suggestion: Add a setup.test.mjs case exercising --enable-review-gate/--disable-review-gate through main().

result.exitCode === 0 && summary.success && warnings.length === 0 ? 'done' : 'failed';
// The prompt asks the reviewer to append a fenced ```json verdict block
// (schemas/review-output.schema.json). When it parses, store the data on
// the record and hide the raw fence from the human-facing summary.

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 end-to-end test verifies that a fixture with a trailing json block actually gets stored as review on the job record and stripped from the summary via the real runReview/updateJob path.

Suggestion: Add a structured-output fixture and assert on the stored job record.

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