127 lines
5.3 KiB
Markdown
127 lines
5.3 KiB
Markdown
|
|
# Resource Proposals
|
||
|
|
|
||
|
|
A proposal is a pending change to a Prompt or Skill, submitted by
|
||
|
|
either a Claude Code session (via the `propose_prompt` / `propose_skill`
|
||
|
|
MCP tools) or a human (via the web UI / CLI). Reviewers triage the
|
||
|
|
queue and either approve — at which point the proposal becomes a real
|
||
|
|
prompt or skill — or reject with a note.
|
||
|
|
|
||
|
|
This is the path by which Claude **proposes back** to mcpd: things the
|
||
|
|
session learned that future sessions would benefit from. The
|
||
|
|
`propose-learnings` global skill (seeded by mcpd at startup) explains
|
||
|
|
the discipline to Claude.
|
||
|
|
|
||
|
|
## Model
|
||
|
|
|
||
|
|
`ResourceProposal` shares the schema's discriminator pattern with
|
||
|
|
`ResourceRevision` — single table, `resourceType` field disambiguates
|
||
|
|
prompts vs skills.
|
||
|
|
|
||
|
|
| Field | Purpose |
|
||
|
|
|----------------------|--------------------------------------------------------|
|
||
|
|
| `resourceType` | `'prompt'` \| `'skill'`. |
|
||
|
|
| `name` | Proposed resource name. |
|
||
|
|
| `body` | Proposed body (`{ content, priority?, metadata?, … }`).|
|
||
|
|
| `projectId` / `agentId` | Scope of the proposal (XOR; null/null = global). |
|
||
|
|
| `createdBySession` | mcplocal session that proposed (when from Claude). |
|
||
|
|
| `createdByUserId` | User who proposed (when via UI/CLI). |
|
||
|
|
| `status` | `'pending'` → `'approved'` \| `'rejected'`. |
|
||
|
|
| `reviewerNote` | Set on approval or rejection. |
|
||
|
|
| `approvedRevisionId` | Set when approved — points at the resulting revision. |
|
||
|
|
|
||
|
|
Two unique constraints — `(resourceType, name, projectId)` and
|
||
|
|
`(resourceType, name, agentId)` — mirror the Prompt / Skill scoping
|
||
|
|
rules. The same `?? ''` workaround for nullable-FK lookups applies.
|
||
|
|
|
||
|
|
## Reviewer flow
|
||
|
|
|
||
|
|
### CLI
|
||
|
|
|
||
|
|
```bash
|
||
|
|
mcpctl review pending # list pending
|
||
|
|
mcpctl review next # show oldest pending
|
||
|
|
mcpctl review show <id> # full detail
|
||
|
|
mcpctl review diff <id> # before/after diff
|
||
|
|
mcpctl review approve <id> # POST /proposals/:id/approve
|
||
|
|
mcpctl review reject <id> --reason "explain" # rejected with note
|
||
|
|
```
|
||
|
|
|
||
|
|
### Web UI
|
||
|
|
|
||
|
|
`/proposals` shows a Pending / Approved / Rejected tab view; the
|
||
|
|
sidebar nav badge polls every 30 s and shows the pending count in
|
||
|
|
amber. Click a row to see the full body, the diff against the current
|
||
|
|
resource (if any), and approve / reject controls.
|
||
|
|
|
||
|
|
### Approval is atomic
|
||
|
|
|
||
|
|
Approval runs in a single Prisma transaction:
|
||
|
|
|
||
|
|
1. Look up the pending proposal.
|
||
|
|
2. Dispatch by `resourceType` to the registered handler
|
||
|
|
(`PromptService` or `SkillService` registers itself at construction).
|
||
|
|
3. Handler upserts the underlying resource — creating it if new, or
|
||
|
|
updating + auto-bumping patch semver if it exists.
|
||
|
|
4. Handler records a `ResourceRevision` linking back to the proposal.
|
||
|
|
5. Proposal status flips to `approved`, `approvedRevisionId` set.
|
||
|
|
|
||
|
|
If any step fails, the transaction rolls back and the proposal stays
|
||
|
|
`pending`. There is no half-approved state.
|
||
|
|
|
||
|
|
## Claude side: `propose_prompt` and `propose_skill`
|
||
|
|
|
||
|
|
Both tools are registered by the `gate` plugin in mcplocal. They post
|
||
|
|
to `/api/v1/proposals` with the appropriate `resourceType`.
|
||
|
|
|
||
|
|
The `propose-learnings` global skill (seeded by mcpd) tells Claude
|
||
|
|
*when* to use them:
|
||
|
|
|
||
|
|
- `propose_prompt` for project-specific knowledge — gotchas,
|
||
|
|
conventions, hidden constraints. Cheap to add, easy to reject.
|
||
|
|
- `propose_skill` for cross-cutting knowledge — debugging discipline,
|
||
|
|
release hygiene, security review style. Larger blast radius; lean
|
||
|
|
toward `propose_prompt` unless you have a clear cross-project reason.
|
||
|
|
|
||
|
|
The `gate-encouragement-propose` system prompt (priority 10, sits in
|
||
|
|
the gating bundle) is the trigger that makes Claude actually consider
|
||
|
|
proposing. Without that, the tools exist but Claude rarely engages.
|
||
|
|
|
||
|
|
## Backwards compat
|
||
|
|
|
||
|
|
PR-1 / PR-2 deferred the cutover from the prompt-only `PromptRequest`
|
||
|
|
table to `ResourceProposal`. Both run side-by-side today:
|
||
|
|
|
||
|
|
- mcplocal's `propose_prompt` still POSTs to the legacy
|
||
|
|
`/api/v1/projects/:name/promptrequests` URL.
|
||
|
|
- mcplocal's `propose_skill` (newer) POSTs to `/api/v1/proposals`
|
||
|
|
directly.
|
||
|
|
- The legacy `/api/v1/promptrequests*` routes remain in mcpd.
|
||
|
|
- `mcpctl approve promptrequest <name>` still works.
|
||
|
|
|
||
|
|
A focused follow-up PR will:
|
||
|
|
|
||
|
|
1. Migrate existing `PromptRequest` rows into `ResourceProposal`
|
||
|
|
(resourceType=prompt).
|
||
|
|
2. Rename `PromptRequest` to `_PromptRequest_legacy`.
|
||
|
|
3. Update mcplocal's `propose_prompt` to use `/api/v1/proposals`.
|
||
|
|
4. Keep the legacy URL as a thin translation shim through one release.
|
||
|
|
5. Drop `_PromptRequest_legacy` after that.
|
||
|
|
|
||
|
|
This stays separate so the cutover is reviewable independently of
|
||
|
|
the larger Skills + Revisions + Proposals work.
|
||
|
|
|
||
|
|
## RBAC
|
||
|
|
|
||
|
|
Proposals piggyback on the `prompts` permission for now — anyone with
|
||
|
|
`view:prompts` can read the queue, anyone with `edit:prompts` can
|
||
|
|
approve or reject. Splitting out a dedicated `proposals` permission
|
||
|
|
(or a "reviewer" role) is straightforward if granularity becomes
|
||
|
|
useful.
|
||
|
|
|
||
|
|
## Audit emission
|
||
|
|
|
||
|
|
Proposal create / approve / reject events flow through the existing
|
||
|
|
audit pipeline. Approval events also reference the resulting
|
||
|
|
revision id, so you can join "proposal approved at T" against
|
||
|
|
"revision X created at T" without polling.
|