Skip to content

feat(review): citadel-native review system (request-review hook + comments + MCP)#37

Open
ovdmar wants to merge 19 commits into
mainfrom
agent/review-system-inside-citadel-xleqqa
Open

feat(review): citadel-native review system (request-review hook + comments + MCP)#37
ovdmar wants to merge 19 commits into
mainfrom
agent/review-system-inside-citadel-xleqqa

Conversation

@ovdmar

@ovdmar ovdmar commented May 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • New `workspace.requestReview` hook event in `@citadel/config` — a repo can declare a script that receives the workspace/repo/PR/diff-summary on stdin and returns a validated `ReviewSuggestionsOutput` payload (reviewer suggestions, checklists, notes, warnings).
  • Citadel-native review comments stored in SQLite (`review_comments` + `review_suggestion_runs` tables, schema v8) — file/line anchors, optimistic concurrency via `ifUpdatedAtMatches`, soft-delete + restore via `includeDeleted`. Comments live in Citadel, not GitHub, so agents attached to a workspace can read them via MCP regardless of provider health.
  • Five new MCP tools: `list_review_comments` (read-only), `add_review_comment` / `update_review_comment` / `delete_review_comment` (destructive, daemon-mediated), `request_review` (daemon-mediated). The daemon MCP path refuses caller-supplied `author`/`runtimeId` and stamps `agent:unknown` until the transport surfaces per-client identity.
  • New `Review` tab in the inspector with a Request-review button (gated on PR or non-default branch) and a flat comments composer; comment bodies render as plain text and suggestion URLs are http(s)-only.

Plan

`.agents/plans/review-system-inside-citadel.md` — dual-approved (3 review rounds; Opus reviewer per MEMORY.md).

Test plan

  • `make check` passes locally (arch, size, typecheck, lint, 591 tests, coverage, deps, build)
  • Self-review via `/review-pr` ran 2 iterations; 14 findings surfaced and all fixed (author spoof, MCP/HTTP diff parity, AC2 visibility gate, structured-config save round-trip, 409 conflict banner, javascript:/data: URL filter, DELETE-on-soft-deleted contract, dead exports, spec marker, etc.)
  • `pnpm e2e` passes (CI will run `e2e/review-system.spec.ts`)
  • `pnpm smoke` exercises the new `/api/workspaces/:id/review-comments` round-trip + `/api/mcp/status` tool inventory
  • Manual QA per the How-to-QA section in the completion report

🤖 Generated with Claude Code

ovdmar and others added 17 commits May 25, 2026 23:23
… system

Plan covers a repo-configurable "Request review" hook + SQLite-backed
review comments readable by agents via MCP. Dual-reviewed (3 rounds with
the Opus reviewer); approved with the concerns log in the plan history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
B.4: Human Review items 3-5 advance to [~] with the v1 store + MCP +
Review tab caveat; full diff-anchored renderer + GitHub-style review
mode stay planned. Add Request Review subsection and a retention note.
B.6: register the workspace.requestReview hook event with its dedicated
ReviewSuggestionsOutput payload and blocking-default semantics.
B.7: expand the MCP tool inventory with the four review tools and
document the author-stamping + optimistic-concurrency rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits the new schemas into packages/contracts/src/review.ts so the main
contracts file stays under the 800-line file-size limit. Adds zod
schemas + inferred types for ReviewSuggestion, ReviewSuggestionsOutput,
ReviewComment (with file/line anchor refinement), ReviewSuggestionRun,
and RequestReviewPayload. Nullable fields use .nullable().default(null)
so a fully-omitted object parses without missing-key surprises.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the event to HookEventSchema, includes it in the blocking-default
list (matches setup/teardown semantics), adds requestReviewHookIds to
repoDefaults, and wires validateHookReferences so the event/hook id
mismatch is caught at load time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n 8)

Two additive tables with FK cascades to workspaces(id) and supporting
indexes. SqliteStore gains CRUD methods via the same module-augment
pattern used for scheduled-run-store. Optimistic concurrency via
ifUpdatedAtMatches returns conflict without mutating; soft-delete keeps
rows physically present so threads stay readable via includeDeleted.

Also extends Repo with requestReviewHookIds (defaulted, additive
column). Existing fixtures touched to satisfy the now-required field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors parseHookOutput's empty-stdout-returns-null contract. Schema
validation surfaces malformed or invalid payloads as a throw the
operations service catches and records as a failed run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Standalone functions in review-system.ts (no augmentation of
OperationService to keep operations/index.ts under the 800-line gate).
requestReviewForWorkspace exits cleanly through:
- no-hook (returns sentinel, no activity)
- succeeded (parses output, writes succeeded run + ONE activity row)
- failed (non-zero exit OR invalid JSON, writes failed run + activity)
- timed-out (detects via runner rejection text + wall-clock backup)

Comment CRUD goes through optimistic-concurrency-aware DB accessors
and logs a single activity row per successful mutation (added/updated/
resolved/deleted). Exports commandHook from hooks-runner for reuse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…stReview

listHookDiagnostics gains the new event in its enumeration so the
repo-settings diagnostics panel surfaces a request-review hook's
validation state. structured-config.tsx Settings UI dropdown +
ConfigResponse repoDefaults type pick it up so operators can author
the hook without hand-editing JSON.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six routes: list/post comments, patch/delete by id (with
ifUpdatedAtMatches → 409 on stale), latest suggestion run, trigger
request-review. POST stamps author='operator' regardless of body input
and rejects extra fields via zod .strict(). Request-review builds the
hook payload from the workspace diff summary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…request)

callMcpTool routes all five to a single review_tool_requires_daemon
sentinel; daemon-side handler implements the actual flows. Author is
stamped agent:<runtimeId>; caller-supplied author returns
author_not_allowed. Conflict semantics returned as { error: 'conflict',
latest } so agents can re-read and retry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
inspector.tsx grows a third tab routing to the new ReviewTab
(inspector-review.tsx, kept separate so inspector.tsx stays under
800 lines). RequestReviewPanel hits the new daemon routes; the button
is disabled with a tooltip when the repo has no workspace.requestReview
hook configured. ReviewCommentsPanel renders a flat list with status
(open/resolved) sections; comments render as plain text so untrusted
bodies cannot inject markup. Optimistic-concurrency token plumbed
through resolve and delete actions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
E2E adds two API-only tests so the daemon endpoints exercised by the
cockpit are kept honest: comment CRUD with optimistic-concurrency
round-trip + request_review no-hook path. Skips DOM assertions on the
new Review tab because the surrounding inspector still evolves; the
underlying behavior is covered by daemon-route + UI unit tests.

scripts/dev/smoke.ts asserts the five MCP review tools are surfaced and
optionally exercises a POST/GET/DELETE round-trip when
CITADEL_SMOKE_WORKSPACE_ID is set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- packages/operations/src/index.ts: consolidate scheduled-agents
  re-exports onto one line.
- packages/mcp/src/index.ts: extract the five review tool definitions
  into a new packages/mcp/src/review-tools.ts file.
- apps/daemon/src/app.ts: move the registerReviewRoutes call into
  extra-routes.ts so app.ts adds no net lines from this PR. (apps/
  daemon/src/app.ts is pre-existing at the gate boundary; the move
  keeps it level with main.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
biome organized imports and reflowed long lines across the new code +
collapsed three short blocks in app.ts so the daemon entry point fits
under the 800-line ceiling. Operations re-export switched to
\`export * from\` for the same reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…UX gates)

Validated review findings, fixed each:

1. BUG/SECURITY — MCP add_review_comment honored caller-supplied
   runtimeId, defeating AC8. The schema doesn't declare runtimeId and
   the dispatcher doesn't re-validate args, so a caller could stamp
   any author. Now refuse both `author` and `runtimeId` from args and
   stamp `agent:unknown` until the MCP transport surfaces per-client
   identity. Test rewritten to codify the new contract.

2. BUG — MCP request_review sent diff:{files:[],…} while HTTP passed a
   real diff. Extracted readWorkspaceDiffSummary and used in both
   callers. Operations test added that hooks observe the real
   files/lines/truncated payload over stdin.

3. PLAN AC2 — Request review button was visible even on the default
   branch with no PR. ReviewTab now takes hasReviewableContext and
   hides the panel when neither prUrl nor a non-default branch.

4. BUG — Structured-config "Save" dropped requestReviewHookIds (and
   the pre-existing appHookIds/actionHookIds) because the payload only
   surfaced setup/teardown. Now round-trips repoDefaultsExtra from the
   loaded config.

5. UX — 409 conflict on PATCH/DELETE rendered as a generic error span.
   CommentRow now detects ApiError.status === 409, refetches, and
   renders an <output role="status"> banner.

6. SECURITY — ReviewSuggestionSchema.url accepted javascript:/data:
   schemes (zod's .url() is permissive). Added http(s)-only refinement
   + contracts test.

7. BUG (minor) — DELETE on already-soft-deleted comment returned 404
   regardless of token. Now distinguishes: fresh token → 204
   (idempotent), stale token → 409 with the tombstone.

8. QUALITY — Removed dead exports `RegisteredReviewRoutes` (void) and
   the unused `ReviewComment` re-export from review-routes.ts.

9. SPEC — B.4 § Request Review item 4 marker advanced to `[~]` and
   notes the http(s) URL constraint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…E test)

- Hoist readWorkspaceDiffSummary to a static import in daemon-mcp-tool;
  the dynamic import was unmotivated (no circular-dep).
- AC2 gate: hasReviewableContext now requires repo to be non-null for
  the non-default-branch arm so an orphaned/repo-less workspace doesn't
  default-open the Request review button.
- Add review-routes test that codifies the re-DELETE soft-deleted
  contract: first DELETE → 204, stale-token retry → 409 with
  tombstone, post-tombstone token → idempotent 204.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nside-citadel-xleqqa

# Conflicts:
#	packages/db/src/migrate.ts
@ovdmar

ovdmar commented May 26, 2026

Copy link
Copy Markdown
Owner Author

Re-opening to trigger CI workflow (initial PR creation did not fire on: pull_request)

@ovdmar ovdmar closed this May 26, 2026
@ovdmar ovdmar reopened this May 26, 2026
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