feat(api): GET /api/agent/proposals/{id} — read a proposed agent write #343

Merged
james merged 2 commits from 342-proposal-read-endpoint into main 2026-06-29 02:17:54 +00:00
Owner

Write tools never mutate — they persist a ProposedChange (summary + structured diff + safe/destructive classification) as an agent_proposals row, and the conversation's awaiting_confirmation pause carries only the proposalId. There was no way to read that proposal back, so a client couldn't render what's about to change before committing/rejecting it. This adds the read side; commit/reject (#51) already exist.

What's in it

  • GET /api/agent/proposals/{id}ProposalDto: proposalId, action (safe_create|safe_update|destructive_update|destructive_delete), tool, entity{type,id}, summary, diff, status, createdAt, expiresAt. The original tool input stays server-side (apply-time replay detail).
  • Per-user 404 — a proposal owned by another user, or missing, is a 404 (don't leak existence), matching the commit/reject discipline.
  • Expiry is read-only — a still-pending proposal past its expiry reads as expired, computed without mutating (the commit path keeps its lazy expired persist; a GET must not write).
  • zod DTO (lib/dto/proposal.ts), RFC 7807 errors, registered in the generated OpenAPI spec (committed openapi.json).

Verification (run locally on this branch)

  • typecheck ✓ · lint
  • test against both engines (ephemeral Postgres via TEST_POSTGRES_URL): 1211 passed (+5 GET tests: 401, owner-read, cross-user 404, missing 404, pending-past-expiry → expired + asserts the row stays pending)
  • openapi:check ✓ up to date · openapi:coverage 116 → 117 pairs
  • semgrep (CI pack set) ✓ 0 findings on the new files

Out of scope

  • The @carol/api-client hook for this endpoint (lands with the conversation hooks).
  • The chat UI.

Part of epic #47; unblocks the chat-UI confirmation flow.

Closes #342

🤖 Generated with Claude Code

Write tools never mutate — they persist a `ProposedChange` (summary + structured diff + safe/destructive classification) as an `agent_proposals` row, and the conversation's `awaiting_confirmation` pause carries only the `proposalId`. There was no way to read that proposal back, so a client couldn't render *what's about to change* before committing/rejecting it. This adds the read side; commit/reject (#51) already exist. ## What's in it - **`GET /api/agent/proposals/{id}`** → `ProposalDto`: `proposalId`, `action` (`safe_create|safe_update|destructive_update|destructive_delete`), `tool`, `entity{type,id}`, `summary`, `diff`, `status`, `createdAt`, `expiresAt`. The original tool `input` stays server-side (apply-time replay detail). - **Per-user 404** — a proposal owned by another user, or missing, is a 404 (don't leak existence), matching the commit/reject discipline. - **Expiry is read-only** — a still-`pending` proposal past its expiry reads as `expired`, computed without mutating (the commit path keeps its lazy `expired` persist; a GET must not write). - zod DTO (`lib/dto/proposal.ts`), RFC 7807 errors, registered in the generated OpenAPI spec (committed `openapi.json`). ## Verification (run locally on this branch) - `typecheck` ✓ · `lint` ✓ - `test` against **both engines** (ephemeral Postgres via `TEST_POSTGRES_URL`): **1211 passed** (+5 GET tests: 401, owner-read, cross-user 404, missing 404, pending-past-expiry → `expired` + asserts the row stays `pending`) - `openapi:check` ✓ up to date · `openapi:coverage` 116 → **117** pairs - semgrep (CI pack set) ✓ 0 findings on the new files ## Out of scope - The `@carol/api-client` hook for this endpoint (lands with the conversation hooks). - The chat UI. Part of epic #47; unblocks the chat-UI confirmation flow. Closes #342 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(api): GET /api/agent/proposals/{id} — read a proposed agent write
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 16s
PR / Static analysis (pull_request) Successful in 1m38s
PR / OSV-Scanner (pull_request) Successful in 21s
PR / Test (postgres) (pull_request) Successful in 2m55s
PR / OpenAPI (pull_request) Failing after 3m12s
PR / Lint (pull_request) Successful in 3m27s
PR / Build (pull_request) Successful in 4m7s
PR / Package age policy (soft) (pull_request) Successful in 54s
Secrets / gitleaks (pull_request) Successful in 51s
PR / pnpm audit (pull_request) Successful in 4m14s
PR / Typecheck (pull_request) Successful in 4m45s
PR / Trivy (image) (pull_request) Successful in 2m43s
PR / Client (web export smoke) (pull_request) Successful in 4m52s
PR / Test (sqlite) (pull_request) Successful in 5m2s
PR / E2E (Playwright) (pull_request) Successful in 5m8s
PR / Coverage (soft) (pull_request) Successful in 2m35s
1c9c2e4aa7
Write tools never mutate; they persist a ProposedChange (summary +
structured diff + safe/destructive classification) as an agent_proposals
row, and the conversation's awaiting_confirmation pause carries only the
proposalId. There was no way to read that proposal back, so a client
couldn't render what's about to change before committing/rejecting it.

Add a read endpoint returning a ProposalDto (proposalId, action, tool,
entity, summary, diff, status, createdAt, expiresAt). Per-user scoped: a
proposal owned by another user — or missing — is a 404 (don't leak
existence), matching the commit/reject discipline. A still-pending
proposal past its expiry reads as `expired`, computed read-only (the GET
never mutates; the commit path keeps its lazy expired persist). The
original tool input stays server-side.

Unblocks the chat-UI confirmation card and any MCP confirmation prompt.

Closes #342

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

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 79.3% ≥ 50%
Branches 71.0% ⚠️ ≥ 75%
Functions 80.3% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 79.3% ✅ | ≥ 50% | | Branches | 71.0% ⚠️ | ≥ 75% | | Functions | 80.3% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
chore(api-client): regenerate types for the proposal-read endpoint
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 48s
PR / Static analysis (pull_request) Successful in 1m24s
PR / OSV-Scanner (pull_request) Successful in 19s
PR / Trivy (image) (pull_request) Successful in 1m31s
PR / Client (web export smoke) (pull_request) Successful in 5m23s
PR / pnpm audit (pull_request) Successful in 4m41s
PR / Test (postgres) (pull_request) Successful in 5m43s
PR / Package age policy (soft) (pull_request) Successful in 19s
PR / Lint (pull_request) Successful in 5m53s
PR / Build (pull_request) Successful in 6m0s
Secrets / gitleaks (pull_request) Successful in 29s
PR / Test (sqlite) (pull_request) Successful in 6m13s
PR / OpenAPI (pull_request) Successful in 6m37s
PR / Typecheck (pull_request) Successful in 6m51s
PR / Coverage (soft) (pull_request) Successful in 5m44s
PR / E2E (Playwright) (pull_request) Successful in 12m14s
2a9595486d
The committed @carol/api-client generated client tracks openapi.json;
adding GET /api/agent/proposals/{id} drifted it. Regenerate so the
`check` gate passes and the new path + ProposalDto are typed for the
upcoming conversation hooks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
james merged commit af007e4152 into main 2026-06-29 02:17:54 +00:00
james deleted branch 342-proposal-read-endpoint 2026-06-29 02:17:55 +00:00
Sign in to join this conversation.
No description provided.