Files
PaperClipAI/packages/paperclip-runner/protocol/schemas/provider-event.schema.json
T
DottaandClaude Opus 5.5 22281d4b3a feat(task-chat): approve harness-proposed plans (Codex plan mode, Claude ExitPlanMode) (#19291)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents run through the native runner. Codex and Claude Code each
have their own plan mode, and each produces a proposed-plan document.
> - Paperclip already has a planning work mode, an issue `plan`
document, and a `request_confirmation` approval gate for plans.
> - These parts were not connected. The Codex proposed-plan item was
transcript text only. The server path never put Codex into plan mode.
Claude was never put into plan mode, and its ExitPlanMode plan was
dropped. A planning run could close its task as done with no approval.
> - This pull request turns a harness's own proposed plan into the
issue's Plan document and the existing approval gate, for Codex and
Claude.
> - The benefit is that users approve or send back the agent's real plan
from the task chat. Approve resumes the agent. Request changes sends the
reason back to the agent.

## Linked Issues or Issue Description

Refs #19252

**Subsystem affected**
Native runner provider events (Codex and ACPX Claude), the runner Codex
provider (Rust), server native plan synchronization, and the task chat
UI.

**Problem or motivation**
Codex and Claude both produce a proposed-plan document in their native
plan mode. Paperclip did not use it. In live tests on the server path, a
Codex planning run was not in Codex plan mode, saved its own
`implementation-plan` document, and marked the task done without
approval. Claude planning runs were not in Claude plan mode, and the
ExitPlanMode plan text never reached Paperclip.

**Proposed solution**
Add a canonical `plan.proposed` event for a harness's proposed plan. Put
Codex and Claude into their native plan mode for planning runs. Save a
completed proposal as the issue `plan` document with the existing
`request_confirmation` gate. Agents can also propose a plan on a
standard task; the work mode stays standard.

**Alternatives considered**
Reuse `plan.updated`. Its schema is the within-turn checklist and is not
the durable Plan document, so a separate event keeps the two clear.
Stronger prompt text without native plan mode. Live tests showed that
the model kept its own document workflow.

**Roadmap alignment**
This reuses the issue plan document, planning work mode and approval
gate that already exist. It adds one canonical provider event.

## What Changed

- `packages/paperclip-runner/protocol/schemas/*`: add the
`plan.proposed` event (`paperclip.plan.proposed.v1`: `proposalId`,
`status`, `markdown` up to 60k characters, `source`). Regenerate the
schema bundle, validators and manifest.
- `packages/paperclip-runner/src/provider-events.ts`: map the Codex
`plan` item and a `<proposed_plan>` block in a completed Codex message
to `plan.proposed`. Add `claudePlanProposalEvent`.
-
`packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs`:
send Codex's native `collaborationMode: plan` on `turn/start` for a
read-only (planning) Codex run. Before this change the server path never
sent it.
- `runner-core/src/acpx_event_payload.rs`: admit `plan.proposed` as ACPX
rich display activity.
- `drivers/acpx/codex-runtime-adapter.ts`: set Claude Code to native
plan mode for a read-only run. Capture Claude's ExitPlanMode plan from
its permission request, record it, and decline the native "Ready to
code?" prompt.
- `drivers/acpx/codex-acpx-driver.ts`, `cli/acpx-runtime-sidecar.ts`:
emit the captured proposal as a rich event. Complete that turn instead
of reporting it as cancelled.
- `backends/codex-native-backend.ts`: per-harness plan-mode
instructions, and a short opt-in hint for Claude on standard tasks.
- `server/src/services/native-runtime/native-session-executor.ts`:
synchronize a completed `plan.proposed` into the `plan` document and a
`request_confirmation` in planning and standard mode. Do not change the
issue status during the run (that revoked the run's lease). The run
finalizer turns the pending confirmation into the review wait.
- `server/src/services/heartbeat-run-summary.ts`: keep a
`<proposed_plan>` block out of the posted reply.
- UI: new `plan_proposal` provider family. The task chat shows a live
"Writing plan" card while a proposal streams and a one-line record when
it completes. Resolved plan reviews keep a receipt ("Approved Plan v1",
"Requested changes to Plan v1: reason"). Plan previews skip section
headings.
- Storybook: `Tasks and conversations/.../Task chat/Proposed plan
approval` shows the journey.
- `doc/SPEC-implementation.md`: describe harness-proposed plan approval.

## Verification

- `cargo test -p paperclip-runner-core --lib` (new:
`read_only_codex_turns_use_native_plan_collaboration_mode`)
- `cd packages/paperclip-runner && npx vitest run
src/provider-events.test.ts
src/drivers/acpx/codex-runtime-adapter.test.ts`
- `cd server && npx vitest run
src/__tests__/native-plan-proposal-sync.test.ts
src/__tests__/heartbeat-run-summary.test.ts`
- `cd ui && npx vitest run
src/components/transcript/native-run-events.test.ts
src/components/task-chat`
- Live test on a local worktree instance with real harnesses:
- Codex (`gpt-5.6-sol`), planning task: Codex asked one question, then
produced a native plan item. The task showed Plan v1 with Approve plan /
Request changes and moved to in review. Approve changed the work mode to
standard. Codex then implemented the plan and its tests passed.
- Codex, Request changes with a reason: Codex woke in planning mode and
produced Plan v2 that includes the change.
- Claude (`claude-opus-5-5`), planning task: Claude was in plan mode.
ExitPlanMode became Plan v1 and the turn completed. Approve resumed
Claude in standard mode, and Claude implemented the plan.
- Claude, standard task that asks for a plan first: Claude stopped
before code changes and asked for plan approval. The work mode stayed
standard.
- Storybook: open `Proposed plan approval` and check each stage.

## Risks

- Codex planning runs now really use Codex plan mode on the server path.
Codex can ask a clarifying question before it proposes. This matches
Codex plan mode.
- Claude planning runs now use Claude plan mode. A declined ExitPlanMode
ends the Claude turn; the runner reports it as completed. Claude's tool
row for that request records a rejection from Claude's side.
- Codex does not stream the proposed plan on the server path yet, so the
live card shows a placeholder until the plan completes.
- A `<proposed_plan>` block in any completed Codex message on a standard
task now asks for approval. This is the documented opt-in path.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with
tool use (shell, file edits, browser). Long context. No extended
thinking mode was used.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 20:53:33 +00:00

207 lines
16 KiB
JSON

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json",
"title": "Provider-neutral runner activity payload",
"oneOf": [
{ "$ref": "#/$defs/plan" },
{ "$ref": "#/$defs/planProposal" },
{ "$ref": "#/$defs/toolExecution" },
{ "$ref": "#/$defs/research" },
{ "$ref": "#/$defs/delegation" },
{ "$ref": "#/$defs/modelRoute" },
{ "$ref": "#/$defs/modelVerification" },
{ "$ref": "#/$defs/contextCompacted" },
{ "$ref": "#/$defs/artifactViewed" },
{ "$ref": "#/$defs/artifactGenerated" },
{ "$ref": "#/$defs/reviewMode" },
{ "$ref": "#/$defs/hook" },
{ "$ref": "#/$defs/memoryCitation" },
{ "$ref": "#/$defs/safetyReview" },
{ "$ref": "#/$defs/terminalInput" },
{ "$ref": "#/$defs/wait" },
{ "$ref": "#/$defs/providerNotice" }
],
"$defs": {
"id": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" },
"shortText": { "type": "string", "maxLength": 4000 },
"nullableShortText": { "type": ["string", "null"], "maxLength": 4000 },
"safePath": { "type": ["string", "null"], "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).*$" },
"safeUrl": { "type": ["string", "null"], "maxLength": 8192, "pattern": "^https?://" },
"plan": {
"type": "object",
"description": "A complete provider-authored within-turn checklist snapshot. Consumers replace the prior snapshot with the same planId in PRP sourceSeq order; this is not Paperclip's durable Plan document.",
"required": ["schema", "planId", "revision", "steps", "complete", "syncStatus"],
"properties": {
"schema": { "const": "paperclip.plan.updated.v1" },
"planId": { "$ref": "#/$defs/id" },
"revision": { "type": "integer", "minimum": 1 },
"explanation": { "$ref": "#/$defs/nullableShortText" },
"steps": { "type": "array", "maxItems": 256, "items": { "type": "object", "required": ["stepId", "body", "status"], "properties": { "stepId": { "$ref": "#/$defs/id" }, "body": { "$ref": "#/$defs/shortText" }, "status": { "enum": ["pending", "in_progress", "completed", "blocked"] } }, "additionalProperties": false } },
"complete": { "type": "boolean" },
"syncStatus": { "description": "Legacy compatibility field. Within-turn checklist snapshots use not_applicable.", "enum": ["streaming", "pending", "synchronized", "conflict", "not_applicable"] },
"documentRevision": { "description": "Legacy compatibility field. Within-turn checklist snapshots use null.", "type": ["integer", "null"], "minimum": 1 }
},
"additionalProperties": false
},
"planProposal": {
"type": "object",
"description": "A provider-authored proposed plan document awaiting human approval (Codex plan item, Claude ExitPlanMode). Paperclip may synchronize a completed proposal into the issue's durable Plan document; it is distinct from the within-turn plan.updated checklist.",
"required": ["schema", "proposalId", "status", "markdown", "source"],
"properties": {
"schema": { "const": "paperclip.plan.proposed.v1" },
"proposalId": { "$ref": "#/$defs/id" },
"status": { "enum": ["streaming", "completed"] },
"markdown": { "type": "string", "maxLength": 60000 },
"source": { "enum": ["codex_plan_item", "claude_exit_plan_mode"] }
},
"additionalProperties": false
},
"toolExecution": {
"type": "object",
"required": ["schema", "executionId", "transport", "operation", "status", "output", "outputBytes", "outputTruncated", "outputDigest"],
"properties": {
"schema": { "const": "paperclip.tool.execution.v1" },
"executionId": { "$ref": "#/$defs/id" },
"transport": { "enum": ["process", "mcp", "dynamic", "builtin"] },
"operation": { "enum": ["read", "search", "list", "execute", "edit", "unknown"] },
"name": { "$ref": "#/$defs/nullableShortText" },
"target": { "$ref": "#/$defs/safePath" },
"namespace": { "type": ["string", "null"], "maxLength": 240 },
"readOnly": { "type": ["boolean", "null"] },
"inputUpdated": { "type": "boolean" },
"status": { "enum": ["running", "completed", "failed", "cancelled", "interrupted"] },
"durationMs": { "type": ["integer", "null"], "minimum": 0 },
"exitCode": { "type": ["integer", "null"] },
"progress": { "$ref": "#/$defs/nullableShortText" },
"subject": { "$ref": "#/$defs/nullableShortText" },
"description": { "$ref": "#/$defs/nullableShortText" },
"blockedReason": { "$ref": "#/$defs/nullableShortText" },
"output": { "type": ["string", "null"], "maxLength": 65536 },
"outputBytes": { "type": "integer", "minimum": 0 },
"outputTruncated": { "type": "boolean" },
"outputDigest": { "type": ["string", "null"], "pattern": "^sha256:[a-f0-9]{64}$" },
"parentItemId": { "anyOf": [{ "$ref": "#/$defs/id" }, { "type": "null" }] },
"fileChanges": {
"description": "Files changed by this one edit call, each with a bounded patch. Paths are workspace-relative.",
"type": "array",
"maxItems": 32,
"items": {
"type": "object",
"required": ["path", "operation", "previousPath", "additions", "deletions", "diff", "diffTruncated"],
"properties": {
"path": { "type": "string", "minLength": 1, "maxLength": 1024, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$" },
"operation": { "enum": ["create", "modify", "delete", "rename"] },
"previousPath": { "$ref": "#/$defs/safePath" },
"additions": { "type": "integer", "minimum": 0 },
"deletions": { "type": "integer", "minimum": 0 },
"diff": { "type": ["string", "null"], "maxLength": 32768 },
"diffTruncated": { "type": "boolean" }
},
"additionalProperties": false
}
}
},
"additionalProperties": false
},
"research": {
"type": "object",
"required": ["schema", "researchId", "action", "status", "sources"],
"properties": {
"schema": { "const": "paperclip.research.v1" },
"researchId": { "$ref": "#/$defs/id" },
"action": { "enum": ["search", "open_page", "find_in_page", "other"] },
"status": { "enum": ["running", "completed", "failed", "cancelled"] },
"query": { "$ref": "#/$defs/nullableShortText" },
"url": { "$ref": "#/$defs/safeUrl" },
"pattern": { "$ref": "#/$defs/nullableShortText" },
"sources": { "type": "array", "maxItems": 64, "items": { "type": "object", "required": ["sourceId", "title", "url", "snippet"], "properties": { "sourceId": { "$ref": "#/$defs/id" }, "title": { "$ref": "#/$defs/shortText" }, "url": { "$ref": "#/$defs/safeUrl" }, "snippet": { "$ref": "#/$defs/nullableShortText" } }, "additionalProperties": false } }
},
"additionalProperties": false
},
"delegation": {
"type": "object",
"required": ["schema", "delegationId", "action", "status", "children"],
"properties": {
"schema": { "const": "paperclip.delegation.v1" },
"delegationId": { "$ref": "#/$defs/id" },
"action": { "enum": ["spawn", "message", "resume", "wait", "close"] },
"status": { "enum": ["running", "waiting", "completed", "failed", "interrupted", "closed"] },
"children": { "type": "array", "maxItems": 64, "items": { "type": "object", "required": ["childId", "role", "model", "status", "summary", "activitySummary"], "properties": { "childId": { "$ref": "#/$defs/id" }, "role": { "type": ["string", "null"], "maxLength": 160 }, "model": { "type": ["string", "null"], "maxLength": 240 }, "status": { "enum": ["running", "waiting", "completed", "failed", "interrupted", "closed"] }, "summary": { "$ref": "#/$defs/nullableShortText" }, "activitySummary": { "$ref": "#/$defs/nullableShortText" }, "prompt": { "$ref": "#/$defs/nullableShortText" }, "result": { "$ref": "#/$defs/nullableShortText" } }, "additionalProperties": false } }
},
"additionalProperties": false
},
"modelRoute": {
"type": "object",
"required": ["schema", "routeId", "provider", "requestedModel", "fromModel", "effectiveModel", "reason"],
"properties": { "schema": { "const": "paperclip.model.route_changed.v1" }, "routeId": { "$ref": "#/$defs/id" }, "provider": { "type": "string", "minLength": 1, "maxLength": 160 }, "requestedModel": { "type": "string", "minLength": 1, "maxLength": 240 }, "fromModel": { "type": ["string", "null"], "maxLength": 240 }, "effectiveModel": { "type": "string", "minLength": 1, "maxLength": 240 }, "reason": { "$ref": "#/$defs/shortText" } },
"additionalProperties": false
},
"modelVerification": {
"type": "object",
"required": ["schema", "verificationId", "status", "classes", "buffering", "summary"],
"properties": { "schema": { "const": "paperclip.model.verification.v1" }, "verificationId": { "$ref": "#/$defs/id" }, "status": { "enum": ["running", "completed", "failed"] }, "classes": { "type": "array", "maxItems": 32, "items": { "type": "string", "maxLength": 160 } }, "buffering": { "type": "boolean" }, "summary": { "$ref": "#/$defs/nullableShortText" } },
"additionalProperties": false
},
"contextCompacted": {
"type": "object",
"required": ["schema", "compactionId", "reason", "preTokens", "postTokens", "sameSession"],
"properties": { "schema": { "const": "paperclip.context.compacted.v1" }, "compactionId": { "$ref": "#/$defs/id" }, "reason": { "enum": ["context_window", "manual", "provider", "unknown"] }, "preTokens": { "type": ["integer", "null"], "minimum": 0 }, "postTokens": { "type": ["integer", "null"], "minimum": 0 }, "sameSession": { "type": "boolean" } },
"additionalProperties": false
},
"artifactViewed": {
"type": "object",
"required": ["schema", "artifactId", "reference", "mediaType", "title"],
"properties": { "schema": { "const": "paperclip.artifact.viewed.v1" }, "artifactId": { "$ref": "#/$defs/id" }, "reference": { "$ref": "#/$defs/safePath" }, "mediaType": { "type": ["string", "null"], "maxLength": 160 }, "title": { "$ref": "#/$defs/nullableShortText" } },
"additionalProperties": false
},
"artifactGenerated": {
"type": "object",
"required": ["schema", "artifactId", "status", "reference", "mediaType", "registered", "failure"],
"properties": { "schema": { "const": "paperclip.artifact.generated.v1" }, "artifactId": { "$ref": "#/$defs/id" }, "status": { "enum": ["running", "completed", "failed"] }, "reference": { "$ref": "#/$defs/safePath" }, "mediaType": { "type": ["string", "null"], "maxLength": 160 }, "registered": { "type": "boolean" }, "transparentBackground": { "type": ["boolean", "null"] }, "failure": { "$ref": "#/$defs/nullableShortText" } },
"additionalProperties": false
},
"reviewMode": {
"type": "object",
"required": ["schema", "reviewId", "state", "scope"],
"properties": { "schema": { "const": "paperclip.review.mode_changed.v1" }, "reviewId": { "$ref": "#/$defs/id" }, "state": { "enum": ["entered", "exited"] }, "scope": { "$ref": "#/$defs/nullableShortText" } },
"additionalProperties": false
},
"hook": {
"type": "object",
"required": ["schema", "hookId", "event", "scope", "status", "blocking", "durationMs", "summary"],
"properties": { "schema": { "const": "paperclip.hook.v1" }, "hookId": { "$ref": "#/$defs/id" }, "event": { "type": "string", "minLength": 1, "maxLength": 160 }, "scope": { "type": "string", "minLength": 1, "maxLength": 160 }, "status": { "enum": ["running", "completed", "failed", "cancelled", "blocked", "stopped"] }, "blocking": { "type": "boolean" }, "durationMs": { "type": ["integer", "null"], "minimum": 0 }, "summary": { "$ref": "#/$defs/nullableShortText" }, "outcome": { "enum": ["running", "passed", "context", "feedback", "blocked", "stopped", "failed"] }, "message": { "$ref": "#/$defs/nullableShortText" }, "source": { "type": ["string", "null"], "maxLength": 160 }, "toolName": { "type": ["string", "null"], "maxLength": 160 } },
"additionalProperties": false
},
"memoryCitation": {
"type": "object",
"required": ["schema", "citationId", "messageItemId", "label", "available", "reference"],
"properties": { "schema": { "const": "paperclip.memory.citation.v1" }, "citationId": { "$ref": "#/$defs/id" }, "messageItemId": { "$ref": "#/$defs/id" }, "label": { "$ref": "#/$defs/shortText" }, "available": { "type": "boolean" }, "reference": { "type": ["string", "null"], "maxLength": 4096 } },
"additionalProperties": false
},
"safetyReview": {
"type": "object",
"required": ["schema", "reviewId", "targetExecutionId", "status", "decision", "summary"],
"properties": { "schema": { "const": "paperclip.safety.review.v1" }, "reviewId": { "$ref": "#/$defs/id" }, "targetExecutionId": { "anyOf": [{ "$ref": "#/$defs/id" }, { "type": "null" }] }, "status": { "enum": ["running", "completed", "failed"] }, "decision": { "enum": ["pending", "allowed", "denied", "user_required", "timed_out", "aborted", "unknown"] }, "summary": { "$ref": "#/$defs/nullableShortText" }, "actionKind": { "enum": ["command", "execve", "writeStdin", "applyPatch", "networkAccess", "mcpToolCall", "requestPermissions", "unknown"] }, "action": { "type": ["string", "null"], "maxLength": 1000 }, "riskLevel": { "enum": ["low", "medium", "high", "critical", null] }, "userConsent": { "enum": ["unknown", "low", "medium", "high", null] }, "rationale": { "$ref": "#/$defs/nullableShortText" }, "decidedBy": { "type": ["string", "null"], "maxLength": 160 } },
"additionalProperties": false
},
"terminalInput": {
"type": "object",
"required": ["schema", "executionId", "origin", "inputClass", "byteCount"],
"properties": { "schema": { "const": "paperclip.terminal.input_sent.v1" }, "executionId": { "$ref": "#/$defs/id" }, "origin": { "enum": ["agent", "user", "system"] }, "inputClass": { "enum": ["text", "control", "eof"] }, "byteCount": { "type": "integer", "minimum": 0 } },
"additionalProperties": false
},
"wait": {
"type": "object",
"required": ["schema", "waitId", "reason", "status", "plannedDurationMs", "elapsedDurationMs"],
"properties": { "schema": { "const": "paperclip.wait.v1" }, "waitId": { "$ref": "#/$defs/id" }, "reason": { "enum": ["timer", "provider", "rate_limit", "unknown"] }, "status": { "enum": ["running", "completed", "interrupted", "cancelled"] }, "plannedDurationMs": { "type": ["integer", "null"], "minimum": 0 }, "elapsedDurationMs": { "type": ["integer", "null"], "minimum": 0 } },
"additionalProperties": false
},
"providerNotice": {
"type": "object",
"required": ["schema", "noticeId", "severity", "category", "scope", "recoverable", "userActionable", "summary"],
"properties": { "schema": { "const": "paperclip.provider.notice.v1" }, "noticeId": { "$ref": "#/$defs/id" }, "severity": { "enum": ["info", "warning", "error"] }, "category": { "type": "string", "minLength": 1, "maxLength": 160 }, "scope": { "enum": ["turn", "session", "environment", "account"] }, "recoverable": { "type": "boolean" }, "userActionable": { "type": "boolean" }, "summary": { "$ref": "#/$defs/shortText" }, "provenance": {"type":"object","required":["method","eventType","sessionId","turnId"],"properties":{"method":{"type":"string","maxLength":160},"eventType":{"type":"string","maxLength":160},"sessionId":{"type":"string","maxLength":240},"turnId":{"type":"string","maxLength":240},"agentId":{"type":"string","maxLength":240},"timestamp":{"type":"string","maxLength":80}},"additionalProperties":false}, "details": {"type":"array","maxItems":64,"items":{"type":"object","required":["name","value"],"properties":{"name":{"type":"string","minLength":1,"maxLength":160},"value":{"type":"string","maxLength":4000}},"additionalProperties":false}} },
"additionalProperties": false
}
}
}