diff --git a/packages/paperclip-runner/scripts/generate-operation-groups.ts b/packages/paperclip-runner/scripts/generate-operation-groups.ts
new file mode 100644
index 00000000000..19bf6c9efdf
--- /dev/null
+++ b/packages/paperclip-runner/scripts/generate-operation-groups.ts
@@ -0,0 +1,28 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+import { renderOperationGroupsDocument } from "../src/catalog/operation-groups-doc.js";
+import {
+ loadOperationGroupsInput,
+ operationGroupsPackageRoot,
+} from "../src/catalog/operation-groups-inputs.js";
+
+const outputPath = resolve(
+ operationGroupsPackageRoot(),
+ "spec/paperclip-agent-operation-groups.md",
+);
+const generated = renderOperationGroupsDocument(loadOperationGroupsInput());
+const check = process.argv.includes("--check");
+
+if (check) {
+ const current = readFileSync(outputPath, "utf8");
+ if (current !== generated) {
+ throw new Error(
+ "paperclip-agent-operation-groups.md is stale; run the generator without --check.",
+ );
+ }
+ process.stdout.write("operation-groups spec is current\n");
+} else {
+ writeFileSync(outputPath, generated);
+ process.stdout.write(`wrote ${outputPath}\n`);
+}
diff --git a/packages/paperclip-runner/spec/operation-groups/source.json b/packages/paperclip-runner/spec/operation-groups/source.json
new file mode 100644
index 00000000000..a7b80e3603b
--- /dev/null
+++ b/packages/paperclip-runner/spec/operation-groups/source.json
@@ -0,0 +1,341 @@
+{
+ "schemaVersion": 1,
+ "documentTitle": "Paperclip agent operation groups",
+ "evalRepositoryUrl": "https://github.com/paperclipai/paperclip-evals/blob/master/",
+ "prpEventFamilyDescriptions": {
+ "runner": "Runner connection, drain, and diagnostic lifecycle.",
+ "runtime": "Runner phase transitions.",
+ "sandbox": "Sandbox resource measurements.",
+ "workspace": "Workspace readiness.",
+ "harness": "Provider harness startup, readiness, exit, and diagnostics.",
+ "session": "Provider-neutral session open, resume, reconciliation, close, and failure.",
+ "turn": "Model turn submission through terminal turn disposition.",
+ "item": "Provider-neutral model/tool item lifecycle.",
+ "mcp_app": "MCP App discovery, initialization, tool, action, host-context, and teardown evidence.",
+ "runtime_request": "Runtime permission/input request lifecycle.",
+ "interaction": "Issue-thread interaction proposal, materialization, response, delivery, and rejection.",
+ "run": "Structured result negotiation and terminal run outcome.",
+ "attention": "Continuation and attention routing lifecycle.",
+ "work": "Recorded work-assessment evidence.",
+ "issue": "Issue-status decision proposal outcome and application evidence."
+ },
+ "prpCommandFamilyDescriptions": {
+ "run": "Prepare or cancel a run.",
+ "session": "Open, snapshot, or close a normalized provider session.",
+ "turn": "Start, steer, interrupt, or stop a model turn.",
+ "request": "Resolve a pending runtime request.",
+ "interaction": "Acknowledge delivery of an interaction response.",
+ "runner": "Drain or shut down the runner process."
+ },
+ "optionalGrantGroups": [
+ {
+ "id": "discovery",
+ "description": "Company-visible task, agent, project, and goal discovery.",
+ "operationIds": ["search_tasks", "list_agents", "get_agent", "list_projects", "list_goals"]
+ },
+ {
+ "id": "delegation_dependencies",
+ "description": "Create delegated work and maintain dependency edges.",
+ "operationIds": ["create_task", "set_dependencies"]
+ },
+ {
+ "id": "governance",
+ "description": "Read, request, comment on, and decide approvals under governed-action checks.",
+ "operationIds": ["list_approvals", "get_approval", "get_approval_context", "request_approval", "decide_approval", "comment_on_approval"]
+ },
+ {
+ "id": "cases",
+ "description": "Read and update case summaries without reusing issue-document authority.",
+ "operationIds": ["list_cases", "upsert_case"]
+ },
+ {
+ "id": "workspace_runtime",
+ "description": "Inspect and control the active issue workspace runtime.",
+ "operationIds": ["get_workspace_runtime", "control_workspace_service"]
+ },
+ {
+ "id": "wake_scheduling",
+ "description": "Schedule a bounded continuation wake when the current task owns the future check.",
+ "operationIds": ["schedule_wake"]
+ },
+ {
+ "id": "routines",
+ "description": "Inspect or manage company routines.",
+ "operationIds": ["list_routines", "manage_routine"]
+ },
+ {
+ "id": "company_skills",
+ "description": "Inspect company skills and synchronize the current agent's skills.",
+ "operationIds": ["list_company_skills", "sync_company_skills"]
+ },
+ {
+ "id": "secrets",
+ "description": "Inspect secret metadata or use a brokered secret value without exposing plaintext evidence.",
+ "operationIds": ["list_secret_metadata", "read_secret_value"]
+ },
+ {
+ "id": "portability_admin",
+ "description": "Export portable company state; broad administration remains deferred until split into governed operations.",
+ "operationIds": ["export_company", "administer_company"]
+ },
+ {
+ "id": "test_escape_hatch",
+ "description": "Controlled skill-test transport only; never product coverage.",
+ "operationIds": ["generic_api_request"]
+ }
+ ],
+ "behaviorGroups": [
+ {
+ "id": "hb",
+ "legacyGroup": 1,
+ "name": "Heartbeat",
+ "owner": "control plane + always tools",
+ "operationIds": ["get_task_context"],
+ "controlPlaneOperationIds": ["select_work", "enforce_budget"],
+ "realSurface": "agent identity, inbox-lite, heartbeat context, budget and active-issue services",
+ "mockStateDomains": ["company", "actor", "wake", "task", "budget", "run"],
+ "prpEvidence": "runner/session/run context plus bounded task-context tool results",
+ "gap": "Production semantic binding is unbound; identity and work selection remain injected/control-plane-owned."
+ },
+ {
+ "id": "co",
+ "legacyGroup": 2,
+ "name": "Checkout",
+ "owner": "control plane",
+ "operationIds": [],
+ "controlPlaneOperationIds": ["checkout_task"],
+ "realSurface": "POST /api/issues/:id/checkout and execution-lock services",
+ "mockStateDomains": ["task", "actor", "run", "idempotency", "fault"],
+ "prpEvidence": "run preparation and issue-status decision evidence with checkout receipt",
+ "gap": "Intentionally no model tool; the production checkout receipt still needs the additive semantic-receipt envelope."
+ },
+ {
+ "id": "st",
+ "legacyGroup": 3,
+ "name": "Status",
+ "owner": "always tools + control-plane arbitration",
+ "operationIds": ["answer_status_question", "finish_task", "block_task", "request_review"],
+ "controlPlaneOperationIds": ["reconcile_run", "append_audit_record"],
+ "realSurface": "issue PATCH, review/liveness policy, and native finalization arbitration",
+ "mockStateDomains": ["task", "comments", "interactions", "blockers", "audit", "run"],
+ "prpEvidence": "semantic operation receipt, work assessment, issue-status decision, and terminal causality",
+ "gap": "Production semantic binding and additive typed operation/conflict receipts remain unimplemented."
+ },
+ {
+ "id": "cm",
+ "legacyGroup": 4,
+ "name": "Comments",
+ "owner": "always tools",
+ "operationIds": ["get_task_history", "report_progress"],
+ "controlPlaneOperationIds": ["append_audit_record"],
+ "realSurface": "issue comment list/get/create routes",
+ "mockStateDomains": ["task", "comments", "actor", "idempotency", "audit"],
+ "prpEvidence": "bounded read result or idempotent comment-write receipt plus audit reference",
+ "gap": "Active-task binding is unbound; cross-task comment mutation is deliberately outside V1."
+ },
+ {
+ "id": "se",
+ "legacyGroup": 5,
+ "name": "Search",
+ "owner": "optional discovery tools",
+ "operationIds": ["search_tasks", "list_agents", "get_agent", "list_projects", "list_goals"],
+ "controlPlaneOperationIds": [],
+ "realSurface": "company issue search and agent/project/goal list/get routes",
+ "mockStateDomains": ["company", "task", "actor", "project", "goal"],
+ "prpEvidence": "bounded redacted read projections through tool-result item events",
+ "gap": "Project and goal operations are scenario-only; every real service binding is unbound."
+ },
+ {
+ "id": "su",
+ "legacyGroup": 6,
+ "name": "Subtasks",
+ "owner": "optional delegation tools",
+ "operationIds": ["create_task"],
+ "controlPlaneOperationIds": ["route_wake"],
+ "realSurface": "company issue create, child issue, assignment, and wake services",
+ "mockStateDomains": ["company", "task", "actor", "blockers", "wake", "audit"],
+ "prpEvidence": "company/task state diff, audit reference, and continuation wake evidence",
+ "gap": "Production binding is unbound and must preserve assignment and governed-action checks."
+ },
+ {
+ "id": "bl",
+ "legacyGroup": 7,
+ "name": "Blockers",
+ "owner": "always/optional tools + control plane",
+ "operationIds": ["block_task", "set_dependencies"],
+ "controlPlaneOperationIds": ["schedule_blocker_wake", "route_wake"],
+ "realSurface": "issue relations, blocker projection, liveness validation, and blocker wake services",
+ "mockStateDomains": ["task", "blockers", "wake", "actor", "audit", "fault"],
+ "prpEvidence": "dependency diff, block receipt, attention routing, and issue-status decision",
+ "gap": "Production binding is unbound; cancelled-blocker and wake receipts need typed additive evidence."
+ },
+ {
+ "id": "dp",
+ "legacyGroup": 8,
+ "name": "Documents and plans",
+ "owner": "always tools; restore optional; destructive lifecycle control-plane-only",
+ "operationIds": ["list_documents", "read_document", "list_document_revisions", "write_document"],
+ "controlPlaneOperationIds": ["append_audit_record"],
+ "realSurface": "issue document list/read/upsert/revision/restore/lock/unlock/delete routes",
+ "mockStateDomains": ["task", "documents", "interactions", "idempotency", "audit", "fault"],
+ "prpEvidence": "bounded reads and revision-safe write/conflict/denial receipts with revision lineage",
+ "gap": "restore_document_revision is an approved optional-tool gap; lock/unlock/delete are intentionally control-plane-only."
+ },
+ {
+ "id": "ix",
+ "legacyGroup": 9,
+ "name": "Interactions",
+ "owner": "always tools + addressed resolver",
+ "operationIds": ["request_human_input"],
+ "controlPlaneOperationIds": ["route_wake"],
+ "realSurface": "issue-thread interaction create/respond/accept/reject/withdraw services",
+ "mockStateDomains": ["task", "documents", "interactions", "wake", "actor", "idempotency"],
+ "prpEvidence": "interaction proposal/materialization/response/delivery and attention events",
+ "gap": "Production binding and semantic resolution receipts are unbound."
+ },
+ {
+ "id": "ap",
+ "legacyGroup": 10,
+ "name": "Approvals",
+ "owner": "optional governance tools + governed approver",
+ "operationIds": ["list_approvals", "get_approval", "get_approval_context", "request_approval", "decide_approval", "comment_on_approval"],
+ "controlPlaneOperationIds": ["route_wake", "append_audit_record"],
+ "realSurface": "company approval, decision, issue-link, comment, and governed-action services",
+ "mockStateDomains": ["company", "task", "approvals", "actor", "wake", "audit", "idempotency"],
+ "prpEvidence": "governed semantic receipts, audit references, and attention/continuation linkage",
+ "gap": "Production binding and additive governed-action receipts are unbound; board-only authority stays outside grants."
+ },
+ {
+ "id": "ar",
+ "legacyGroup": 11,
+ "name": "Artifacts",
+ "owner": "always tools + artifact/work-product services",
+ "operationIds": ["register_deliverable"],
+ "controlPlaneOperationIds": ["append_audit_record"],
+ "realSurface": "attachment upload and issue work-product routes",
+ "mockStateDomains": ["task", "artifacts", "workProducts", "workspace", "audit", "idempotency"],
+ "prpEvidence": "artifact/work-product reference and durable inspectability receipt; never binary bytes",
+ "gap": "Production upload/register composite and additive durable-reference receipt are unbound."
+ },
+ {
+ "id": "er",
+ "legacyGroup": 12,
+ "name": "Errors and critical rules",
+ "owner": "runner/control plane + optional workspace/wake tools",
+ "operationIds": ["get_workspace_runtime", "control_workspace_service", "schedule_wake", "inspect_operation_result"],
+ "controlPlaneOperationIds": ["release_task", "enforce_budget", "persist_run", "replay_run", "reconcile_run"],
+ "realSurface": "workspace runtime, monitor/recovery, budget, run persistence/replay, release, and terminal services",
+ "mockStateDomains": ["workspace", "budget", "run", "wake", "audit", "idempotency", "fault"],
+ "prpEvidence": "runtime/workspace/attention/run lifecycle, typed denials, replay facts, and terminal causality",
+ "gap": "Budget stop reasons and semantic denial/conflict receipts require additive v1 envelopes; inspect_operation_result remains scenario-only."
+ },
+ {
+ "id": "rf",
+ "legacyGroup": 13,
+ "name": "Reference files",
+ "owner": "optional domain tools + test-only escape hatch",
+ "operationIds": ["list_cases", "upsert_case", "list_routines", "manage_routine", "list_company_skills", "sync_company_skills", "list_secret_metadata", "read_secret_value", "export_company", "administer_company", "generic_api_request"],
+ "controlPlaneOperationIds": ["append_audit_record"],
+ "realSurface": "case, routine, company-skill, secret, portability, and administration services",
+ "mockStateDomains": ["company", "cases", "routines", "skills", "secrets", "audit", "fault"],
+ "prpEvidence": "bounded domain projections, redacted broker receipts, company diffs, and audit references",
+ "gap": "These operations are scenario-only except generic_api_request, which is test-only; broad administer_company is deferred and cannot claim product coverage."
+ },
+ {
+ "id": "mh",
+ "legacyGroup": 14,
+ "name": "Multi-hop",
+ "owner": "composed semantic operations + control-plane continuation",
+ "operationIds": ["create_task", "set_dependencies", "request_human_input", "request_approval", "register_deliverable"],
+ "controlPlaneOperationIds": ["route_wake", "reconcile_run"],
+ "realSurface": "delegation, dependency, interaction, approval, artifact, and terminal orchestration services",
+ "mockStateDomains": ["task", "blockers", "interactions", "approvals", "artifacts", "wake", "run", "audit"],
+ "prpEvidence": "correlated operation receipts, state diffs, attention hops, work assessment, status decision, and terminal outcome",
+ "gap": "No generic transaction tool is allowed; shared mock/real conformance must prove each composed effect."
+ },
+ {
+ "id": "rs",
+ "legacyGroup": 15,
+ "name": "Restraint and no-call",
+ "owner": "policy/exposure layer",
+ "operationIds": ["answer_status_question", "read_secret_value", "generic_api_request"],
+ "controlPlaneOperationIds": ["enforce_budget"],
+ "realSurface": "task-mode, secret-broker, test-scope, pause, and budget policy checks",
+ "mockStateDomains": ["actor", "task", "budget", "secrets", "audit", "fault"],
+ "prpEvidence": "absence of forbidden effects plus typed policy denial/redaction receipts when a call is attempted",
+ "gap": "Typed redaction/authorization receipts need additive v1 evidence; generic_api_request is never a product fallback."
+ },
+ {
+ "id": "wk",
+ "legacyGroup": 16,
+ "name": "Wake situations",
+ "owner": "control plane + always context/history tools",
+ "operationIds": ["get_task_context", "get_task_history", "schedule_wake"],
+ "controlPlaneOperationIds": ["select_work", "route_wake"],
+ "realSurface": "wakeup requests, heartbeat context, comment/interaction/approval/blocker wake routing, and scheduled wake services",
+ "mockStateDomains": ["wake", "task", "comments", "interactions", "approvals", "blockers", "run"],
+ "prpEvidence": "attention request routing/resolution plus resumed session/run causality",
+ "gap": "Production scheduling binding is unbound; control-plane routing remains non-callable."
+ }
+ ],
+ "rules": {
+ "companyAuthorization": [
+ "Every entity read and write is resolved inside the authenticated actor's company; cross-company identifiers fail without disclosing protected facts.",
+ "Board actors use active membership and role permissions. Agent writes require a company-scoped run JWT and X-Paperclip-Run-Id; active-task tools cannot accept a caller-selected company or arbitrary task.",
+ "Optional tools are omitted unless every required claim, role, and task-mode condition is satisfied. A grant never bypasses approval, budget, pause, execution-lock, interaction-owner, or other governed-action checks."
+ ],
+ "taskModes": [
+ "standard permits the full eligible catalog subject to claims and role checks.",
+ "ask is answer-only: investigation reads are eligible, while implementation, document mutation, delegation, terminal, and governed writes are absent.",
+ "planning permits plan/document/progress/input operations but not terminal completion; accepted-plan continuation remains planning-specific.",
+ "skill_test exposes only scenario-admitted tools. generic_api_request additionally requires the explicit test grant and allowlist and never supplies product coverage."
+ ],
+ "redaction": [
+ "Tool descriptors declare observable redaction. Secret plaintext may reach only the model-use capsule authorized for a bound secret; it never reaches PRP, logs, artifacts, errors, audit details, or serialized tool results.",
+ "Authorization denials reveal stable codes, missing public claims, and remediation only; they do not include protected entity state, credentials, headers, cookies, or another company's policy.",
+ "Artifacts carry durable references, hashes, content type, and size on the wire; binary payloads remain in storage transports."
+ ],
+ "idempotency": [
+ "Every mutation whose descriptor says required must carry a stable idempotency key. Retrying the same key and equivalent input returns the prior receipt without duplicating side effects.",
+ "Reusing a key with different input is an idempotency conflict. Optimistic document writes additionally bind baseRevisionId and return the current revision on conflict.",
+ "PRP source event IDs are idempotent, source sequence gaps are evidence, and replay is side-effect free. Replaying a trace cannot repeat control-plane writes."
+ ],
+ "sideEffects": [
+ "read operations return bounded projections and do not mutate control-plane state.",
+ "task_write and company_write operations emit normalized state diffs and immutable audit references after production authorization succeeds.",
+ "governance, workspace_control, secret_read, and admin operations retain their production-specific gates; catalog exposure is necessary but never sufficient authority.",
+ "Terminal semantic operations propose intent. The control plane owns checkout, release, budget enforcement, persisted run evidence, replay, wake routing, audit append, and final status arbitration."
+ ]
+ },
+ "documentLifecycle": {
+ "scope": [
+ "Issue documents are active-task working records identified by (activeIssueId, key). Always-present document tools do not accept a company id or arbitrary issue id.",
+ "Cross-task document reads require a future explicit optional operation and grant; case documents and future company knowledge are separate resource types.",
+ "Locked-document fallback may create a deterministic new key only when lockedDocumentStrategy is create_new_document; the source stays locked and the returned receipt names the redirected key."
+ ],
+ "operations": [
+ {"action": "create", "semanticOperation": "write_document", "placement": "always_agent_tool", "contract": "baseRevisionId is null; create revision 1 and return document/revision/audit identity.", "status": "supported contract; production binding unbound"},
+ {"action": "read", "semanticOperation": "list_documents / read_document", "placement": "always_agent_tool", "contract": "List metadata or read the current active-task document by stable key.", "status": "supported contract; production binding unbound"},
+ {"action": "update", "semanticOperation": "write_document", "placement": "always_agent_tool", "contract": "Require the exact latest baseRevisionId and an idempotency key; append an immutable revision.", "status": "supported contract; production binding unbound"},
+ {"action": "revisions", "semanticOperation": "list_document_revisions", "placement": "always_agent_tool", "contract": "Return bounded immutable revision history newest first.", "status": "supported contract; production binding unbound"},
+ {"action": "restore", "semanticOperation": "restore_document_revision", "placement": "optional_agent_tool (documents:restore)", "contract": "Validate same-document lineage, reject locked documents, append a new revision, and return source/new revision identity; restoring current is a no-change duplicate.", "status": "approved catalog gap"},
+ {"action": "lock", "semanticOperation": "none", "placement": "control_plane_owned", "contract": "Board/administrative lifecycle action; agent writes receive document_locked or use explicit create-new fallback.", "status": "intentionally non-callable"},
+ {"action": "unlock", "semanticOperation": "none", "placement": "control_plane_owned", "contract": "Board/administrative lifecycle action; never inferred from a failed write.", "status": "intentionally non-callable"},
+ {"action": "delete", "semanticOperation": "none", "placement": "control_plane_owned", "contract": "Destructive audited action; pending targets become stale and the runner never recreates them automatically.", "status": "intentionally non-callable"}
+ ]
+ },
+ "legacySemanticTargetDispositions": {
+ "injected_actor_context": {"kind": "control_plane", "targets": ["select_work"], "reason": "Identity is launch context, not a model tool."},
+ "runner_work_selection": {"kind": "control_plane", "targets": ["select_work"], "reason": "Inbox selection belongs to the control plane."},
+ "get_project": {"kind": "folded", "targets": ["list_projects"], "reason": "The bounded discovery operation owns project list/get projection in V1."},
+ "wait_for_workspace_service": {"kind": "folded", "targets": ["control_workspace_service"], "reason": "Wait is a bounded action of workspace service control."},
+ "get_goal": {"kind": "folded", "targets": ["list_goals"], "reason": "The bounded discovery operation owns goal list/get projection in V1."},
+ "semantic_task_disposition": {"kind": "composite", "targets": ["answer_status_question", "finish_task", "block_task", "request_review"], "reason": "Generic issue PATCH is replaced by intent-specific terminal/status operations."},
+ "atomic_checkout": {"kind": "control_plane", "targets": ["checkout_task"], "reason": "Checkout is an atomic control-plane transaction."},
+ "runtime_release": {"kind": "control_plane", "targets": ["release_task"], "reason": "Release is runner/control-plane cleanup."},
+ "restore_document_revision": {"kind": "known_gap", "targets": [], "reason": "Approved optional documents:restore operation is not yet in the canonical catalog."},
+ "link_approval": {"kind": "folded", "targets": ["request_approval", "get_task_context"], "reason": "Issue linkage is part of approval request/context composites."},
+ "unlink_approval": {"kind": "control_plane", "targets": ["append_audit_record"], "reason": "No standalone agent unlink tool is approved in V1."},
+ "test_only_api_escape_hatch": {"kind": "folded", "targets": ["generic_api_request"], "reason": "Compatibility alias for the test-only escape hatch."}
+ }
+}
diff --git a/packages/paperclip-runner/spec/paperclip-agent-operation-groups.md b/packages/paperclip-runner/spec/paperclip-agent-operation-groups.md
new file mode 100644
index 00000000000..1b8218ac010
--- /dev/null
+++ b/packages/paperclip-runner/spec/paperclip-agent-operation-groups.md
@@ -0,0 +1,493 @@
+
+
+# Paperclip agent operation groups
+
+Status: canonical explanatory contract for the Paperclip runner V1 surface.
+
+This document keeps three independent meanings of **group** separate. PRP families describe wire evidence and controller commands; capability placement decides who owns an operation; behavioral eval groups organize the 106 scenario corpus. None of the three axes can be used as a substitute for another.
+
+The generated totals are **65 PRP events in 15 event families**, **13 controller commands in 6 command families**, **10 control-plane operations**, **41 reconciled semantic operations** (14 always, 27 optional), and **106 scenarios in 16 behavior groups**.
+
+## Axis 1: PRP v1 event and command families
+
+PRP records ordered, replayable execution evidence. It is not the model's Paperclip tool API. Events are ordered per `sourceInstanceId`; duplicate `sourceEventId` values are idempotent; source-sequence gaps remain evidence; replay is side-effect free; unknown required versions fail closed.
+
+### Event families
+
+| Family | Purpose | Events | Count |
+| --- | --- | --- | ---: |
+| `runner` | Runner connection, drain, and diagnostic lifecycle. | `runner.connected`
`runner.reconnected`
`runner.disconnected`
`runner.draining`
`runner.diagnostic` | 5 |
+| `runtime` | Runner phase transitions. | `runtime.phase.changed` | 1 |
+| `sandbox` | Sandbox resource measurements. | `sandbox.metric` | 1 |
+| `workspace` | Workspace readiness. | `workspace.ready` | 1 |
+| `harness` | Provider harness startup, readiness, exit, and diagnostics. | `harness.starting`
`harness.ready`
`harness.exited`
`harness.diagnostic` | 4 |
+| `session` | Provider-neutral session open, resume, reconciliation, close, and failure. | `session.starting`
`session.started`
`session.resuming`
`session.resumed`
`session.reconciled`
`session.closed`
`session.failed` | 7 |
+| `turn` | Model turn submission through terminal turn disposition. | `turn.submitted`
`turn.accepted`
`turn.started`
`turn.completed`
`turn.failed`
`turn.interrupted`
`turn.cancelled` | 7 |
+| `item` | Provider-neutral model/tool item lifecycle. | `item.started`
`item.delta`
`item.completed`
`item.failed` | 4 |
+| `mcp_app` | MCP App discovery, initialization, tool, action, host-context, and teardown evidence. | `mcp_app.discovered`
`mcp_app.resource.resolved`
`mcp_app.initializing`
`mcp_app.ready`
`mcp_app.tool_input`
`mcp_app.tool_result`
`mcp_app.action.requested`
`mcp_app.action.resolved`
`mcp_app.host_context.changed`
`mcp_app.failed`
`mcp_app.teardown` | 11 |
+| `runtime_request` | Runtime permission/input request lifecycle. | `runtime_request.created`
`runtime_request.resolved`
`runtime_request.expired`
`runtime_request.cancelled` | 4 |
+| `interaction` | Issue-thread interaction proposal, materialization, response, delivery, and rejection. | `interaction.request.proposed`
`interaction.request.materialized`
`interaction.request.rejected`
`interaction.response.progressed`
`interaction.response.resolved`
`interaction.response.delivered` | 6 |
+| `run` | Structured result negotiation and terminal run outcome. | `run.result.proposed`
`run.result.accepted`
`run.result.rejected`
`run.terminal` | 4 |
+| `attention` | Continuation and attention routing lifecycle. | `attention.request.proposed`
`attention.request.routed`
`attention.request.resolved`
`attention.request.expired`
`attention.request.superseded` | 5 |
+| `work` | Recorded work-assessment evidence. | `work.assessment.recorded` | 1 |
+| `issue` | Issue-status decision proposal outcome and application evidence. | `issue.status.decision.recorded`
`issue.status.decision.applied`
`issue.status.decision.rejected`
`issue.status.decision.superseded` | 4 |
+
+### Controller-command families
+
+| Family | Purpose | Commands | Count |
+| --- | --- | --- | ---: |
+| `run` | Prepare or cancel a run. | `run.prepare`
`run.cancel` | 2 |
+| `session` | Open, snapshot, or close a normalized provider session. | `session.open`
`session.snapshot`
`session.close` | 3 |
+| `turn` | Start, steer, interrupt, or stop a model turn. | `turn.start`
`turn.steer`
`turn.interrupt`
`turn.stop` | 4 |
+| `request` | Resolve a pending runtime request. | `request.resolve` | 1 |
+| `interaction` | Acknowledge delivery of an interaction response. | `interaction.receipt` | 1 |
+| `runner` | Drain or shut down the runner process. | `runner.drain`
`runner.shutdown` | 2 |
+
+## Axis 2: capability placement
+
+Placement has exactly three outcomes:
+
+- `control_plane_owned`: Paperclip or the runner performs the operation; it is absent from the model tool catalog.
+- `always_agent_tool`: every eligible active-task run receives the operation after task-mode and actor checks.
+- `optional_agent_tool`: the operation is exposed only when every declared claim, task-mode, role, and policy condition passes.
+
+### Control-plane-owned operations
+
+| Operation | Why it is not a model tool |
+| --- | --- |
+| `checkout_task` | Atomic checkout and execution-lock ownership. |
+| `release_task` | Run cleanup and checkout release. |
+| `select_work` | Scoped wake/inbox work selection. |
+| `route_wake` | Attention, blocker, interaction, approval, and continuation routing. |
+| `enforce_budget` | Budget hard stop, pause, and run-stop reason. |
+| `append_audit_record` | Immutable mutation evidence. |
+| `persist_run` | Durable run/event/checkpoint persistence. |
+| `replay_run` | Side-effect-free replay and reconstruction. |
+| `schedule_blocker_wake` | Dependency-resolution wake scheduling. |
+| `reconcile_run` | Work assessment, terminal status arbitration, and recovery reconciliation. |
+
+### Always-agent operations (14)
+
+`answer_status_question`, `block_task`, `finish_task`, `get_task_context`, `get_task_history`, `inspect_operation_result`, `list_document_revisions`, `list_documents`, `read_document`, `register_deliverable`, `report_progress`, `request_human_input`, `request_review`, `write_document`.
+
+### Optional operations (27) and grant groups (11)
+
+Grant groups are documentation/exposure bundles, not additional authority. The operation descriptor's exact `requiredClaims` remains decisive.
+
+| Grant group | Operations | Required claims represented | Purpose |
+| --- | --- | --- | --- |
+| `discovery` | `search_tasks`
`list_agents`
`get_agent`
`list_projects`
`list_goals` | `discovery:agents:read`
`discovery:goals:read`
`discovery:projects:read`
`discovery:tasks:read` | Company-visible task, agent, project, and goal discovery. |
+| `delegation_dependencies` | `create_task`
`set_dependencies` | `delegation:tasks:create`
`dependencies:write` | Create delegated work and maintain dependency edges. |
+| `governance` | `list_approvals`
`get_approval`
`get_approval_context`
`request_approval`
`decide_approval`
`comment_on_approval` | `governance:approvals:comment`
`governance:approvals:decide`
`governance:approvals:read`
`governance:approvals:request` | Read, request, comment on, and decide approvals under governed-action checks. |
+| `cases` | `list_cases`
`upsert_case` | `cases:read`
`cases:write` | Read and update case summaries without reusing issue-document authority. |
+| `workspace_runtime` | `get_workspace_runtime`
`control_workspace_service` | `workspace:control`
`workspace:read` | Inspect and control the active issue workspace runtime. |
+| `wake_scheduling` | `schedule_wake` | `control_plane:wakes` | Schedule a bounded continuation wake when the current task owns the future check. |
+| `routines` | `list_routines`
`manage_routine` | `routines:read`
`routines:write` | Inspect or manage company routines. |
+| `company_skills` | `list_company_skills`
`sync_company_skills` | `company_skills:read`
`company_skills:write` | Inspect company skills and synchronize the current agent's skills. |
+| `secrets` | `list_secret_metadata`
`read_secret_value` | `secrets:metadata:read`
`secrets:values:read` | Inspect secret metadata or use a brokered secret value without exposing plaintext evidence. |
+| `portability_admin` | `export_company`
`administer_company` | `company:admin`
`portability:export` | Export portable company state; broad administration remains deferred until split into governed operations. |
+| `test_escape_hatch` | `generic_api_request` | `test:generic_api_request` | Controlled skill-test transport only; never product coverage. |
+
+### Reconciled semantic-operation ledger
+
+`live_codex` means a live provider dispatcher exists against the deterministic runner/mock port; it does **not** mean a production Paperclip service is bound. Every row remains `realServiceBinding: unbound` until the shared mock/real adapter work lands. `generic_api_request` is test-only and cannot satisfy product coverage.
+
+| Operation | Placement | Claims | Modes / roles | Side effect | Idempotency | Redacts | Mock | Catalogs / current runner | Production / PRP evidence |
+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
+| `administer_company` | `optional_agent_tool` | `company:admin` | `standard`
`skill_test`
roles: `board`
`ceo`
`admin` | `admin` | `required` | no | `mock_extension:company.admin` | `scenario`
`scenario_mock` | `unbound`
company admin/portability item event plus audit record
catalog PRP status: `audit_pending` |
+| `answer_status_question` | `always_agent_tool` | none | `standard`
`ask`
`planning`
`skill_test` | `task_write` | `required` | no | `semantic_command:report_progress` | `scenario` + `live`
`live_codex` | `unbound`
semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events
catalog PRP status: `audit_pending` |
+| `block_task` | `always_agent_tool` | none | `standard`
`skill_test` | `task_write` | `required` | no | `semantic_command:block_task` | `scenario` + `live`
`live_codex` | `unbound`
semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events
catalog PRP status: `audit_pending` |
+| `comment_on_approval` | `optional_agent_tool` | `governance:approvals:comment` | `standard`
`ask`
`planning`
`skill_test` | `governance` | `required` | no | `semantic_command:comment_on_approval` | `scenario` + `live`
`live_codex` | `unbound`
approval lifecycle plus governed-wait continuation and audit events
catalog PRP status: `audit_pending` |
+| `control_workspace_service` | `optional_agent_tool` | `workspace:control` | `standard`
`skill_test` | `workspace_control` | `required` | no | `semantic_command:control_workspace_service` | `scenario` + `live`
`live_codex` | `unbound`
workspace service lifecycle event
catalog PRP status: `audit_pending` |
+| `create_task` | `optional_agent_tool` | `delegation:tasks:create` | `standard`
`skill_test` | `company_write` | `required` | no | `semantic_command:create_task` | `scenario` + `live`
`live_codex` | `unbound`
semantic-operation item event plus company-entity state diff and audit record
catalog PRP status: `audit_pending` |
+| `decide_approval` | `optional_agent_tool` | `governance:approvals:decide` | `standard`
`skill_test`
roles: `board`
`approver`
`security` | `governance` | `required` | no | `semantic_command:decide_approval` | `scenario` + `live`
`live_codex` | `unbound`
approval lifecycle plus governed-wait continuation and audit events
catalog PRP status: `audit_pending` |
+| `export_company` | `optional_agent_tool` | `portability:export` | `standard`
`skill_test` | `admin` | `required` | no | `mock_extension:portability.export` | `scenario`
`scenario_mock` | `unbound`
company admin/portability item event plus audit record
catalog PRP status: `audit_pending` |
+| `finish_task` | `always_agent_tool` | none | `standard`
`skill_test` | `task_write` | `required` | no | `semantic_command:finish_task` | `scenario` + `live`
`live_codex` | `unbound`
semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events
catalog PRP status: `audit_pending` |
+| `generic_api_request` | `optional_agent_tool` | `test:generic_api_request` | `skill_test` | `test_escape_hatch` | `required` | yes | `mock_extension:test.generic_api` | `scenario` + `live`
`test_only` | `unbound`
test-only; excluded from product PRP evidence
catalog PRP status: `audit_pending` |
+| `get_agent` | `optional_agent_tool` | `discovery:agents:read` | `standard`
`ask`
`planning`
`skill_test` | `read` | `none` | no | inline/no mapping | `live`
`live_codex` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `get_approval` | `optional_agent_tool` | `governance:approvals:read` | `standard`
`ask`
`planning`
`skill_test` | `read` | `none` | no | inline/no mapping | `live`
`live_codex` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `get_approval_context` | `optional_agent_tool` | `governance:approvals:read` | `standard`
`ask`
`planning`
`skill_test` | `read` | `none` | no | inline/no mapping | `live`
`live_codex` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `get_task_context` | `always_agent_tool` | none | `standard`
`ask`
`planning`
`skill_test` | `read` | `none` | no | `context_read:active_task` | `scenario` + `live`
`live_codex` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `get_task_history` | `always_agent_tool` | none | `standard`
`ask`
`planning`
`skill_test` | `read` | `none` | no | `snapshot_read:active_task_history` | `scenario` + `live`
`live_codex` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `get_workspace_runtime` | `optional_agent_tool` | `workspace:read` | `standard`
`ask`
`planning`
`skill_test` | `read` | `none` | no | `snapshot_read:active_task_workspace` | `scenario` + `live`
`live_codex` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `inspect_operation_result` | `always_agent_tool` | none | `standard`
`ask`
`planning`
`skill_test` | `read` | `none` | no | `operation_result` | `scenario`
`scenario_mock` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `list_agents` | `optional_agent_tool` | `discovery:agents:read` | `standard`
`ask`
`planning`
`skill_test` | `read` | `none` | no | `snapshot_read:company_actors` | `scenario` + `live`
`live_codex` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `list_approvals` | `optional_agent_tool` | `governance:approvals:read` | `standard`
`ask`
`planning`
`skill_test` | `read` | `none` | no | `snapshot_read:company_approvals` | `scenario` + `live`
`live_codex` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `list_cases` | `optional_agent_tool` | `cases:read` | `standard`
`skill_test` | `read` | `none` | no | `mock_extension:cases.list` | `scenario`
`scenario_mock` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `list_company_skills` | `optional_agent_tool` | `company_skills:read` | `standard`
`skill_test` | `read` | `none` | no | `mock_extension:company_skills.list` | `scenario`
`scenario_mock` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `list_document_revisions` | `always_agent_tool` | none | `standard`
`ask`
`planning`
`skill_test` | `read` | `none` | no | `snapshot_read:active_task_document_revisions` | `scenario` + `live`
`live_codex` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `list_documents` | `always_agent_tool` | none | `standard`
`ask`
`planning`
`skill_test` | `read` | `none` | no | `snapshot_read:active_task_documents` | `scenario` + `live`
`live_codex` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `list_goals` | `optional_agent_tool` | `discovery:goals:read` | `standard`
`skill_test` | `read` | `none` | no | `mock_extension:discovery.goals` | `scenario`
`scenario_mock` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `list_projects` | `optional_agent_tool` | `discovery:projects:read` | `standard`
`skill_test` | `read` | `none` | no | `mock_extension:discovery.projects` | `scenario`
`scenario_mock` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `list_routines` | `optional_agent_tool` | `routines:read` | `standard`
`skill_test` | `read` | `none` | no | `mock_extension:routines.list` | `scenario`
`scenario_mock` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `list_secret_metadata` | `optional_agent_tool` | `secrets:metadata:read` | `standard`
`skill_test` | `read` | `none` | no | `mock_extension:secrets.metadata` | `scenario`
`scenario_mock` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `manage_routine` | `optional_agent_tool` | `routines:write` | `standard`
`skill_test` | `admin` | `required` | no | `mock_extension:routines.manage` | `scenario`
`scenario_mock` | `unbound`
company admin/portability item event plus audit record
catalog PRP status: `audit_pending` |
+| `read_document` | `always_agent_tool` | none | `standard`
`ask`
`planning`
`skill_test` | `read` | `none` | no | `snapshot_read:active_task_document` | `scenario` + `live`
`live_codex` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `read_secret_value` | `optional_agent_tool` | `secrets:values:read` | `standard`
`skill_test` | `secret_read` | `none` | yes | `mock_extension:secrets.value` | `scenario`
`scenario_mock` | `unbound`
redacted tool-result item event; secret value never reaches the wire
catalog PRP status: `audit_pending` |
+| `register_deliverable` | `always_agent_tool` | none | `standard`
`planning`
`skill_test` | `task_write` | `required` | no | `semantic_command:register_deliverable` | `scenario` + `live`
`live_codex` | `unbound`
semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events
catalog PRP status: `audit_pending` |
+| `report_progress` | `always_agent_tool` | none | `standard`
`ask`
`planning`
`skill_test` | `task_write` | `required` | no | `semantic_command:report_progress` | `scenario` + `live`
`live_codex` | `unbound`
semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events
catalog PRP status: `audit_pending` |
+| `request_approval` | `optional_agent_tool` | `governance:approvals:request` | `standard`
`skill_test` | `governance` | `required` | no | `semantic_command:request_approval` | `scenario` + `live`
`live_codex` | `unbound`
approval lifecycle plus governed-wait continuation and audit events
catalog PRP status: `audit_pending` |
+| `request_human_input` | `always_agent_tool` | none | `standard`
`planning`
`skill_test` | `task_write` | `required` | no | `semantic_command:request_human_input` | `scenario` + `live`
`live_codex` | `unbound`
semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events
catalog PRP status: `audit_pending` |
+| `request_review` | `always_agent_tool` | none | `standard`
`skill_test` | `task_write` | `required` | no | `semantic_command:request_review` | `scenario` + `live`
`live_codex` | `unbound`
semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events
catalog PRP status: `audit_pending` |
+| `schedule_wake` | `optional_agent_tool` | `control_plane:wakes` | `standard`
`skill_test` | `task_write` | `required` | no | inline/no mapping | `live`
`live_codex` | `unbound`
semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events
catalog PRP status: `audit_pending` |
+| `search_tasks` | `optional_agent_tool` | `discovery:tasks:read` | `standard`
`ask`
`planning`
`skill_test` | `read` | `none` | no | `snapshot_read:company_tasks` | `scenario` + `live`
`live_codex` | `unbound`
read projection surfaced via a tool-result item event; no control-plane state diff
catalog PRP status: `audit_pending` |
+| `set_dependencies` | `optional_agent_tool` | `dependencies:write` | `standard`
`skill_test` | `company_write` | `required` | no | `semantic_command:set_dependencies` | `scenario` + `live`
`live_codex` | `unbound`
semantic-operation item event plus company-entity state diff and audit record
catalog PRP status: `audit_pending` |
+| `sync_company_skills` | `optional_agent_tool` | `company_skills:write` | `standard`
`skill_test` | `admin` | `required` | no | `mock_extension:company_skills.sync` | `scenario`
`scenario_mock` | `unbound`
company admin/portability item event plus audit record
catalog PRP status: `audit_pending` |
+| `upsert_case` | `optional_agent_tool` | `cases:write` | `standard`
`skill_test` | `company_write` | `required` | no | `mock_extension:cases.upsert` | `scenario`
`scenario_mock` | `unbound`
semantic-operation item event plus company-entity state diff and audit record
catalog PRP status: `audit_pending` |
+| `write_document` | `always_agent_tool` | none | `standard`
`planning`
`skill_test` | `task_write` | `required` | no | `semantic_command:write_document` | `scenario` + `live`
`live_codex` | `unbound`
semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events
catalog PRP status: `audit_pending` |
+
+## Axis 3: the 16 behavioral eval groups
+
+Behavior groups describe expected outcomes and trajectories. They do not grant tools and they do not define PRP event types. The matrix is generated from the behavior-group source plus the checked-in eval traceability manifest; scenario counts and membership cannot be hand-edited here.
+
+### Coverage matrix
+
+| Group | Owner | Semantic operations | Control-plane operations | Real Paperclip surface | Mock state | Scenarios | PRP evidence | Gap / disposition |
+| --- | --- | --- | --- | --- | --- | --- | ---: | --- | --- |
+| [`hb` — Heartbeat](#behavior-group-hb-heartbeat) | control plane + always tools | `get_task_context` | `select_work`
`enforce_budget` | agent identity, inbox-lite, heartbeat context, budget and active-issue services | `company`
`actor`
`wake`
`task`
`budget`
`run` | 5 | runner/session/run context plus bounded task-context tool results | Production semantic binding is unbound; identity and work selection remain injected/control-plane-owned. |
+| [`co` — Checkout](#behavior-group-co-checkout) | control plane | none | `checkout_task` | POST /api/issues/:id/checkout and execution-lock services | `task`
`actor`
`run`
`idempotency`
`fault` | 6 | run preparation and issue-status decision evidence with checkout receipt | Intentionally no model tool; the production checkout receipt still needs the additive semantic-receipt envelope. |
+| [`st` — Status](#behavior-group-st-status) | always tools + control-plane arbitration | `answer_status_question`
`finish_task`
`block_task`
`request_review` | `reconcile_run`
`append_audit_record` | issue PATCH, review/liveness policy, and native finalization arbitration | `task`
`comments`
`interactions`
`blockers`
`audit`
`run` | 8 | semantic operation receipt, work assessment, issue-status decision, and terminal causality | Production semantic binding and additive typed operation/conflict receipts remain unimplemented. |
+| [`cm` — Comments](#behavior-group-cm-comments) | always tools | `get_task_history`
`report_progress` | `append_audit_record` | issue comment list/get/create routes | `task`
`comments`
`actor`
`idempotency`
`audit` | 6 | bounded read result or idempotent comment-write receipt plus audit reference | Active-task binding is unbound; cross-task comment mutation is deliberately outside V1. |
+| [`se` — Search](#behavior-group-se-search) | optional discovery tools | `search_tasks`
`list_agents`
`get_agent`
`list_projects`
`list_goals` | none | company issue search and agent/project/goal list/get routes | `company`
`task`
`actor`
`project`
`goal` | 4 | bounded redacted read projections through tool-result item events | Project and goal operations are scenario-only; every real service binding is unbound. |
+| [`su` — Subtasks](#behavior-group-su-subtasks) | optional delegation tools | `create_task` | `route_wake` | company issue create, child issue, assignment, and wake services | `company`
`task`
`actor`
`blockers`
`wake`
`audit` | 4 | company/task state diff, audit reference, and continuation wake evidence | Production binding is unbound and must preserve assignment and governed-action checks. |
+| [`bl` — Blockers](#behavior-group-bl-blockers) | always/optional tools + control plane | `block_task`
`set_dependencies` | `schedule_blocker_wake`
`route_wake` | issue relations, blocker projection, liveness validation, and blocker wake services | `task`
`blockers`
`wake`
`actor`
`audit`
`fault` | 5 | dependency diff, block receipt, attention routing, and issue-status decision | Production binding is unbound; cancelled-blocker and wake receipts need typed additive evidence. |
+| [`dp` — Documents and plans](#behavior-group-dp-documents-and-plans) | always tools; restore optional; destructive lifecycle control-plane-only | `list_documents`
`read_document`
`list_document_revisions`
`write_document` | `append_audit_record` | issue document list/read/upsert/revision/restore/lock/unlock/delete routes | `task`
`documents`
`interactions`
`idempotency`
`audit`
`fault` | 3 | bounded reads and revision-safe write/conflict/denial receipts with revision lineage | restore_document_revision is an approved optional-tool gap; lock/unlock/delete are intentionally control-plane-only. |
+| [`ix` — Interactions](#behavior-group-ix-interactions) | always tools + addressed resolver | `request_human_input` | `route_wake` | issue-thread interaction create/respond/accept/reject/withdraw services | `task`
`documents`
`interactions`
`wake`
`actor`
`idempotency` | 9 | interaction proposal/materialization/response/delivery and attention events | Production binding and semantic resolution receipts are unbound. |
+| [`ap` — Approvals](#behavior-group-ap-approvals) | optional governance tools + governed approver | `list_approvals`
`get_approval`
`get_approval_context`
`request_approval`
`decide_approval`
`comment_on_approval` | `route_wake`
`append_audit_record` | company approval, decision, issue-link, comment, and governed-action services | `company`
`task`
`approvals`
`actor`
`wake`
`audit`
`idempotency` | 6 | governed semantic receipts, audit references, and attention/continuation linkage | Production binding and additive governed-action receipts are unbound; board-only authority stays outside grants. |
+| [`ar` — Artifacts](#behavior-group-ar-artifacts) | always tools + artifact/work-product services | `register_deliverable` | `append_audit_record` | attachment upload and issue work-product routes | `task`
`artifacts`
`workProducts`
`workspace`
`audit`
`idempotency` | 4 | artifact/work-product reference and durable inspectability receipt; never binary bytes | Production upload/register composite and additive durable-reference receipt are unbound. |
+| [`er` — Errors and critical rules](#behavior-group-er-errors-and-critical-rules) | runner/control plane + optional workspace/wake tools | `get_workspace_runtime`
`control_workspace_service`
`schedule_wake`
`inspect_operation_result` | `release_task`
`enforce_budget`
`persist_run`
`replay_run`
`reconcile_run` | workspace runtime, monitor/recovery, budget, run persistence/replay, release, and terminal services | `workspace`
`budget`
`run`
`wake`
`audit`
`idempotency`
`fault` | 9 | runtime/workspace/attention/run lifecycle, typed denials, replay facts, and terminal causality | Budget stop reasons and semantic denial/conflict receipts require additive v1 envelopes; inspect_operation_result remains scenario-only. |
+| [`rf` — Reference files](#behavior-group-rf-reference-files) | optional domain tools + test-only escape hatch | `list_cases`
`upsert_case`
`list_routines`
`manage_routine`
`list_company_skills`
`sync_company_skills`
`list_secret_metadata`
`read_secret_value`
`export_company`
`administer_company`
`generic_api_request` | `append_audit_record` | case, routine, company-skill, secret, portability, and administration services | `company`
`cases`
`routines`
`skills`
`secrets`
`audit`
`fault` | 22 | bounded domain projections, redacted broker receipts, company diffs, and audit references | These operations are scenario-only except generic_api_request, which is test-only; broad administer_company is deferred and cannot claim product coverage. |
+| [`mh` — Multi-hop](#behavior-group-mh-multi-hop) | composed semantic operations + control-plane continuation | `create_task`
`set_dependencies`
`request_human_input`
`request_approval`
`register_deliverable` | `route_wake`
`reconcile_run` | delegation, dependency, interaction, approval, artifact, and terminal orchestration services | `task`
`blockers`
`interactions`
`approvals`
`artifacts`
`wake`
`run`
`audit` | 4 | correlated operation receipts, state diffs, attention hops, work assessment, status decision, and terminal outcome | No generic transaction tool is allowed; shared mock/real conformance must prove each composed effect. |
+| [`rs` — Restraint and no-call](#behavior-group-rs-restraint-and-no-call) | policy/exposure layer | `answer_status_question`
`read_secret_value`
`generic_api_request` | `enforce_budget` | task-mode, secret-broker, test-scope, pause, and budget policy checks | `actor`
`task`
`budget`
`secrets`
`audit`
`fault` | 3 | absence of forbidden effects plus typed policy denial/redaction receipts when a call is attempted | Typed redaction/authorization receipts need additive v1 evidence; generic_api_request is never a product fallback. |
+| [`wk` — Wake situations](#behavior-group-wk-wake-situations) | control plane + always context/history tools | `get_task_context`
`get_task_history`
`schedule_wake` | `select_work`
`route_wake` | wakeup requests, heartbeat context, comment/interaction/approval/blocker wake routing, and scheduled wake services | `wake`
`task`
`comments`
`interactions`
`approvals`
`blockers`
`run` | 8 | attention request routing/resolution plus resumed session/run causality | Production scheduling binding is unbound; control-plane routing remains non-callable. |
+
+### Scenario links
+
+#### Behavior group hb: Heartbeat
+
+5 scenarios (legacy group 1):
+
+- [`hb-context-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/hb-context-01.yaml) — Prefer the compact heartbeat-context route before replaying the thread
+- [`hb-inbox-lite-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/hb-inbox-lite-01.yaml) — Normal heartbeat starts from the compact inbox, not a raw issue listing
+- [`hb-pick-priority-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/hb-pick-priority-01.yaml) — Pick-work priority prefers in_progress and skips blocked work
+- [`hb-scoped-wake-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/hb-scoped-wake-01.yaml) — Scoped wake payload skips identity/inbox and goes straight to checkout
+- [`hb-wake-comment-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/hb-wake-comment-01.yaml) — Comment wake fetches the triggering comment first, then responds on the issue
+
+#### Behavior group co: Checkout
+
+6 scenarios (legacy group 2):
+
+- [`co-409-stop-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/co-409-stop-01.yaml) — 409 on checkout means stop — no retry, no assignee patch, move on
+- [`co-409-stop-02`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/co-409-stop-02.yaml) — 409 on checkout is terminal even when the task was requested by name
+- [`co-before-work-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/co-before-work-01.yaml) — Checkout happens before any other write on the issue
+- [`co-body-contract-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/co-body-contract-01.yaml) — Checkout body carries agentId and expectedStatuses
+- [`co-no-status-patch-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/co-no-status-patch-01.yaml) — Enter in_progress by checkout, never by patching status
+- [`co-runid-header-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/co-runid-header-01.yaml) — Modifying calls carry the X-Paperclip-Run-Id audit header
+
+#### Behavior group st: Status
+
+8 scenarios (legacy group 3):
+
+- [`st-backlog-park-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/st-backlog-park-01.yaml) — Postponed work is parked in backlog, not cancelled or closed
+- [`st-blocked-owner-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/st-blocked-owner-01.yaml) — Blocking on another issue sets status blocked plus first-class blockedByIssueIds
+- [`st-crossteam-cancel-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/st-crossteam-cancel-01.yaml) — Cross-team tasks are never cancelled — reassign to the manager instead
+- [`st-done-comment-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/st-done-comment-01.yaml) — Closing a task is checkout, then PATCH done with an explanatory comment
+- [`st-env-blocked-notdone-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/st-env-blocked-notdone-01.yaml) — Impossible deliverable (absent mount, read-only prefix, no route) ends blocked with a named owner — never done or in_review
+- [`st-ephemeral-verify-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/st-ephemeral-verify-01.yaml) — Artifacts produced outside the synced workspace — the disposition PATCH must carry a persistence caveat (or relocation), never a bare done
+- [`st-handback-review-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/st-handback-review-01.yaml) — Board user asking for the task back gets reassigned + in_review, not done
+- [`st-unverified-toolchain-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/st-unverified-toolchain-01.yaml) — Unrunnable mandated verification (certified toolchain unobtainable) ends blocked stating what could not be verified — no unqualified completion claim
+
+#### Behavior group cm: Comments
+
+6 scenarios (legacy group 4):
+
+- [`cm-mention-structured-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/cm-mention-structured-01.yaml) — Machine-authored mentions use the structured agent:// form
+- [`cm-multiline-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/cm-multiline-01.yaml) — Multiline markdown comments keep their literal newlines
+- [`cm-prefixed-url-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/cm-prefixed-url-01.yaml) — Internal links always carry the company prefix
+- [`cm-progress-nextaction-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/cm-progress-nextaction-01.yaml) — Progress comments state what is done, what remains, and who owns the next step
+- [`cm-runid-modify-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/cm-runid-modify-01.yaml) — Comment POSTs carry the run-id audit header too
+- [`cm-ticket-link-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/cm-ticket-link-01.yaml) — Ticket ids in comment bodies become company-prefixed markdown links
+
+#### Behavior group se: Search
+
+4 scenarios (legacy group 5):
+
+- [`se-get-issue-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/se-get-issue-01.yaml) — Direct issue fetch plus thread read to answer a history question
+- [`se-q-comments-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/se-q-comments-01.yaml) — Search reaches comment bodies, not just titles
+- [`se-q-filters-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/se-q-filters-01.yaml) — Combine q with status/assignee filters
+- [`se-q-topic-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/se-q-topic-01.yaml) — Find an issue by topic using the q search param
+
+#### Behavior group su: Subtasks
+
+4 scenarios (legacy group 6):
+
+- [`su-crossteam-billing-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/su-crossteam-billing-01.yaml) — Cross-team delegation sets billingCode
+- [`su-inherit-workspace-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/su-inherit-workspace-01.yaml) — Non-child follow-up on the same code change inherits the execution workspace
+- [`su-no-poll-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/su-no-poll-01.yaml) — Delegate long work as a child issue and rely on wakes, not polling
+- [`su-parent-goal-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/su-parent-goal-01.yaml) — Subtasks are created with parentId and goalId set
+
+#### Behavior group bl: Blockers
+
+5 scenarios (legacy group 7):
+
+- [`bl-cancelled-not-resolved-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/bl-cancelled-not-resolved-01.yaml) — Cancelled blockers do not auto-resolve — remove them explicitly
+- [`bl-clear-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/bl-clear-01.yaml) — Clearing blockers sends an empty array (the set is replaced wholesale)
+- [`bl-create-blocked-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/bl-create-blocked-01.yaml) — New dependent work is created blocked with blockedByIssueIds at creation time
+- [`bl-firstclass-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/bl-firstclass-01.yaml) — Dependencies become first-class blockedByIssueIds, not prose
+- [`bl-read-owners-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/bl-read-owners-01.yaml) — Blocker owners are read from the issue's blockedBy field
+
+#### Behavior group dp: Documents and plans
+
+3 scenarios (legacy group 8):
+
+- [`dp-base-revision-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/dp-base-revision-01.yaml) — Updating an existing plan fetches it first and sends its latest baseRevisionId
+- [`dp-plan-doc-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/dp-plan-doc-01.yaml) — Plans go in the plan issue document, and the issue is not marked done
+- [`dp-plan-link-comment-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/dp-plan-link-comment-01.yaml) — Comments about a plan deep-link the plan document
+
+#### Behavior group ix: Interactions
+
+9 scenarios (legacy group 9):
+
+- [`ix-checkbox-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ix-checkbox-01.yaml) — Subset selection from a known list is a checkbox confirmation
+- [`ix-checkbox-result-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ix-checkbox-result-01.yaml) — Checkbox continuation wake acts on result.selectedOptionIds only
+- [`ix-confirmation-plan-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ix-confirmation-plan-01.yaml) — Plan sign-off is a request_confirmation bound to the latest revision, then in_review
+- [`ix-continuation-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ix-continuation-01.yaml) — request_confirmation sets a wake continuation policy when work must resume
+- [`ix-questions-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ix-questions-01.yaml) — A short typed form of questions is ask_user_questions, not a comment
+- [`ix-stale-target-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ix-stale-target-01.yaml) — A stale_target expiry means rebuild against the latest revision, fresh interaction
+- [`ix-suggest-tasks-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ix-suggest-tasks-01.yaml) — Proposing tasks for the board to accept uses suggest_tasks, not direct creation
+- [`ix-superseded-comment-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ix-superseded-comment-01.yaml) — A superseded_by_comment expiry means address the comment, then a new interaction
+- [`ix-verdicts-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ix-verdicts-01.yaml) — Per-item approve/reject decisions use request_item_verdicts with reasons on reject
+
+#### Behavior group ap: Approvals
+
+6 scenarios (legacy group 10):
+
+- [`ap-approval-deny-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-approval-deny-01.yaml) — A denied approval leaves the issue open with an explanatory comment
+- [`ap-approval-wake-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-approval-wake-01.yaml) — Approval wake reviews the approval, its issues, and closes what it resolves
+- [`ap-board-approval-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-board-approval-01.yaml) — Spend needs a request_board_approval linked to the issue, then a waiting posture
+- [`ap-mcp-expiry-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-mcp-expiry-01.yaml) — An expired MCP approval means one fresh idempotent re-call, then in_review again
+- [`ap-mcp-gate-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-mcp-gate-01.yaml) — Pending MCP tool approval means in_review posture, no retry, no done
+- [`ap-mcp-pathmissing-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-mcp-pathmissing-01.yaml) — approval_path_missing means stop and reroute, not retry loops or fake dispositions
+
+#### Behavior group ar: Artifacts
+
+4 scenarios (legacy group 11):
+
+- [`ar-no-done-without-upload-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ar-no-done-without-upload-01.yaml) — Closing a task with a file deliverable implies uploading it, even unprompted
+- [`ar-upload-before-done-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ar-upload-before-done-01.yaml) — File deliverables are uploaded to the issue before closing it
+- [`ar-workproduct-pr-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ar-workproduct-pr-01.yaml) — An opened PR is recorded as a pull_request work product, not just a comment
+- [`ar-workproduct-wsfile-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ar-workproduct-wsfile-01.yaml) — A file staying in the execution workspace gets a workspace_file resourceRef work product
+
+#### Behavior group er: Errors and critical rules
+
+9 scenarios (legacy group 12):
+
+- [`er-blocked-dedup-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/er-blocked-dedup-01.yaml) — Blocked task with no new context gets no re-comment and no checkout
+- [`er-budget-critical-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/er-budget-critical-01.yaml) — Above 80% budget usage, pick the critical task over the medium one
+- [`er-close-retry-fault-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/er-close-retry-fault-01.yaml) — C1 probe — closing PATCH survives two injected faults and keeps resending
+- [`er-exec-not-participant-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/er-exec-not-participant-01.yaml) — Non-participants never try to advance an execution stage
+- [`er-exec-participant-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/er-exec-participant-01.yaml) — Execution-policy reviewer approves via the normal PATCH with status done
+- [`er-mention-handoff-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/er-mention-handoff-01.yaml) — Explicit mention handoff self-assigns via checkout, never by patching assignee
+- [`er-mention-noassign-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/er-mention-noassign-01.yaml) — FYI mentions never trigger self-assignment
+- [`er-no-unassigned-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/er-no-unassigned-01.yaml) — Empty inbox means exit — never adopt unassigned work
+- [`er-release-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/er-release-01.yaml) — Handing a task back uses the release route, not cancel or assignee edits
+
+#### Behavior group rf: Reference files
+
+22 scenarios (legacy group 13):
+
+- [`rf-api-404-report-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-api-404-report-01.yaml) — A 404 on an issue lookup is reported honestly, not papered over
+- [`rf-api-cancel-obsolete-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-api-cancel-obsolete-01.yaml) — Obsolete work is cancelled, not marked done and not deleted
+- [`rf-api-mention-discipline-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-api-mention-discipline-01.yaml) — Status notes don't @-mention teammates who have nothing to act on
+- [`rf-api-mgr-heartbeat-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-api-mgr-heartbeat-01.yaml) — Manager-style heartbeat — team roster, workload read, summary comment
+- [`rf-api-review-changes-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-api-review-changes-01.yaml) — Reviewer requests changes with a non-done status and lets Paperclip reassign
+- [`rf-art-attachment-wp-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-art-attachment-wp-01.yaml) — Deliverable upload is registered as a primary attachment-backed work product
+- [`rf-case-child-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-case-child-01.yaml) — Bounded sub-output becomes a child case under the parent record
+- [`rf-case-lifecycle-link-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-case-lifecycle-link-01.yaml) — Move a case to in_progress and link its driving issue as reference context
+- [`rf-case-upsert-doc-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-case-upsert-doc-01.yaml) — Create a retry-safe case with a stable key and write its document body
+- [`rf-cskill-audit-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-cskill-audit-01.yaml) — Library-vs-attached audit reads both the company and agent skill surfaces
+- [`rf-cskill-install-attach-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-cskill-install-attach-01.yaml) — Catalog install is followed by an agent skills sync — install ≠ attach
+- [`rf-cskill-self-sync-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-cskill-self-sync-01.yaml) — Skills sync replaces the whole desired set — existing skills are preserved
+- [`rf-iws-start-url-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-iws-start-url-01.yaml) — Discover the issue workspace from the issue and start its service for QA
+- [`rf-iws-target-restart-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-iws-target-restart-01.yaml) — Bounce a specific workspace service via a targeted selector and verify health
+- [`rf-routine-create-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-routine-create-01.yaml) — Create a self-assigned routine with a weekly cron schedule trigger
+- [`rf-routine-manual-run-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-routine-manual-run-01.yaml) — Fire a routine once via the manual-run endpoint with an idempotency key
+- [`rf-routine-pause-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-routine-pause-01.yaml) — Pause a routine reversibly instead of archiving or deleting its trigger
+- [`rf-routine-policy-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-routine-policy-01.yaml) — Set routine concurrency and catch-up policies by their enum names
+- [`rf-routine-webhook-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-routine-webhook-01.yaml) — Add an HMAC-signed webhook trigger with a bounded replay window
+- [`rf-wf-export-preview-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-wf-export-preview-01.yaml) — Company export previews first, narrows with selectedFiles, keeps tasks out
+- [`rf-wf-invite-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-wf-invite-01.yaml) — OpenClaw invite — generate the prompt and post it paste-ready with the ws URL
+- [`rf-wf-project-setup-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-wf-project-setup-01.yaml) — New project with a repo-only workspace (repoUrl, no cwd)
+
+#### Behavior group mh: Multi-hop
+
+4 scenarios (legacy group 14):
+
+- [`mh-blocked-handoff-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/mh-blocked-handoff-01.yaml) — "Multi-hop: spawn a review task and block the source on it so work auto-resumes"
+- [`mh-children-complete-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/mh-children-complete-01.yaml) — "Multi-hop: children-completed wake wraps up the parent"
+- [`mh-plan-confirm-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/mh-plan-confirm-01.yaml) — "Multi-hop: write plan doc, request confirmation on it, park in_review"
+- [`mh-subtask-tree-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/mh-subtask-tree-01.yaml) — "Multi-hop: sequenced subtask tree with chained blockers"
+
+#### Behavior group rs: Restraint and no-call
+
+3 scenarios (legacy group 15):
+
+- [`rs-dependency-blocked-wake-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rs-dependency-blocked-wake-01.yaml) — A comment on a dependency-blocked issue is triaged, never force-unblocked
+- [`rs-question-only-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rs-question-only-01.yaml) — A status question gets an answer, not state changes
+- [`rs-secret-hygiene-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rs-secret-hygiene-01.yaml) — The API key never appears in comment bodies, even when asked to document auth
+
+#### Behavior group wk: Wake situations
+
+8 scenarios (legacy group 16):
+
+- [`wk-ask-mode-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/wk-ask-mode-01.yaml) — Ask-mode wake is answer-only — comment, no state or document writes
+- [`wk-plan-accepted-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/wk-plan-accepted-01.yaml) — Accepted-plan continuation creates subtasks, never re-plans or re-asks
+- [`wk-plan-annotation-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/wk-plan-annotation-01.yaml) — Plan-annotation wake revises the document and answers annotations via nested routes
+- [`wk-plan-directive-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/wk-plan-directive-01.yaml) — Planning-mode wake produces plan PUT, revision-bound confirmation, then in_review
+- [`wk-plan-directive-02`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/wk-plan-directive-02.yaml) — Layer probe — planning directive via wake prose only (no task-context markdown)
+- [`wk-recovery-processlost-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/wk-recovery-processlost-01.yaml) — Process-lost recovery verifies durable progress first and never redoes posted work
+- [`wk-resume-delta-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/wk-resume-delta-01.yaml) — Layer probe — resume delta with condensed contract only preserves close discipline
+- [`wk-skilltest-mode-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/wk-skilltest-mode-01.yaml) — Skill-test mode writes the structured result to the output document, scoped to this issue
+
+## Authorization and execution invariants
+
+### Company and actor authorization
+
+- Every entity read and write is resolved inside the authenticated actor's company; cross-company identifiers fail without disclosing protected facts.
+- Board actors use active membership and role permissions. Agent writes require a company-scoped run JWT and X-Paperclip-Run-Id; active-task tools cannot accept a caller-selected company or arbitrary task.
+- Optional tools are omitted unless every required claim, role, and task-mode condition is satisfied. A grant never bypasses approval, budget, pause, execution-lock, interaction-owner, or other governed-action checks.
+
+### Task modes and exposure
+
+- standard permits the full eligible catalog subject to claims and role checks.
+- ask is answer-only: investigation reads are eligible, while implementation, document mutation, delegation, terminal, and governed writes are absent.
+- planning permits plan/document/progress/input operations but not terminal completion; accepted-plan continuation remains planning-specific.
+- skill_test exposes only scenario-admitted tools. generic_api_request additionally requires the explicit test grant and allowlist and never supplies product coverage.
+
+### Redaction
+
+- Tool descriptors declare observable redaction. Secret plaintext may reach only the model-use capsule authorized for a bound secret; it never reaches PRP, logs, artifacts, errors, audit details, or serialized tool results.
+- Authorization denials reveal stable codes, missing public claims, and remediation only; they do not include protected entity state, credentials, headers, cookies, or another company's policy.
+- Artifacts carry durable references, hashes, content type, and size on the wire; binary payloads remain in storage transports.
+
+### Idempotency and replay
+
+- Every mutation whose descriptor says required must carry a stable idempotency key. Retrying the same key and equivalent input returns the prior receipt without duplicating side effects.
+- Reusing a key with different input is an idempotency conflict. Optimistic document writes additionally bind baseRevisionId and return the current revision on conflict.
+- PRP source event IDs are idempotent, source sequence gaps are evidence, and replay is side-effect free. Replaying a trace cannot repeat control-plane writes.
+
+### Side effects and terminal ownership
+
+- read operations return bounded projections and do not mutate control-plane state.
+- task_write and company_write operations emit normalized state diffs and immutable audit references after production authorization succeeds.
+- governance, workspace_control, secret_read, and admin operations retain their production-specific gates; catalog exposure is necessary but never sufficient authority.
+- Terminal semantic operations propose intent. The control plane owns checkout, release, budget enforcement, persisted run evidence, replay, wake routing, audit append, and final status arbitration.
+
+## Issue-document lifecycle
+
+Issue documents are active-task working records identified by (activeIssueId, key). Always-present document tools do not accept a company id or arbitrary issue id.
+
+Cross-task document reads require a future explicit optional operation and grant; case documents and future company knowledge are separate resource types.
+
+Locked-document fallback may create a deterministic new key only when lockedDocumentStrategy is create_new_document; the source stays locked and the returned receipt names the redirected key.
+
+| Action | Semantic operation | Placement | Contract | Status |
+| --- | --- | --- | --- | --- |
+| `create` | `write_document` | `always_agent_tool` | baseRevisionId is null; create revision 1 and return document/revision/audit identity. | supported contract; production binding unbound |
+| `read` | `list_documents / read_document` | `always_agent_tool` | List metadata or read the current active-task document by stable key. | supported contract; production binding unbound |
+| `update` | `write_document` | `always_agent_tool` | Require the exact latest baseRevisionId and an idempotency key; append an immutable revision. | supported contract; production binding unbound |
+| `revisions` | `list_document_revisions` | `always_agent_tool` | Return bounded immutable revision history newest first. | supported contract; production binding unbound |
+| `restore` | `restore_document_revision` | `optional_agent_tool (documents:restore)` | Validate same-document lineage, reject locked documents, append a new revision, and return source/new revision identity; restoring current is a no-change duplicate. | approved catalog gap |
+| `lock` | `none` | `control_plane_owned` | Board/administrative lifecycle action; agent writes receive document_locked or use explicit create-new fallback. | intentionally non-callable |
+| `unlock` | `none` | `control_plane_owned` | Board/administrative lifecycle action; never inferred from a failed write. | intentionally non-callable |
+| `delete` | `none` | `control_plane_owned` | Destructive audited action; pending targets become stale and the runner never recreates them automatically. | intentionally non-callable |
+
+All document writes produce a normalized receipt. Success includes the document key/id, prior and new revision identity, idempotency key, and audit reference. Missing/stale bases return `base_revision_required` or `stale_base_revision` with the current revision. Authorization, cross-company, and lock failures use stable redacted denial codes. The runner never overwrites after a conflict without a fresh read and explicit reconciled write.
+
+## Provenance, generation, and drift gates
+
+The machine-readable authority for this document's decisions is `spec/operation-groups/source.json`. The generator joins that source to these independently versioned authorities and rejects disagreement:
+
+- the package-exported `CAPABILITY_CANONICAL_CATALOG` in `src/catalog/` — the 41-operation authority, including enriched mock/redaction and divergence facts.
+- `src/tools/capability-semantic-tool-types.ts` — 10 control-plane-owned operation IDs.
+- `protocol/schemas/event.schema.json` and `command.schema.json` — PRP event/command members and families.
+- `spec/capability/eval-traceability.yaml` — all 16 groups and 106 scenario identities/source anchors.
+- `spec/capability/source-contract.json` and `mcp-tool-map.yaml` — legacy MCP placement and fold authority.
+- `generated/capability/semantic-tool-contracts.json` — generated provider contract; it must equal the live catalog exactly.
+- `src/catalog/index.ts` and `src/index.ts` — package export path for the canonical reconciliation authority.
+
+Regenerate and check reproducibly:
+
+```sh
+pnpm --filter @paperclipai/paperclip-runner exec tsx scripts/generate-operation-groups.ts
+pnpm --filter @paperclipai/paperclip-runner exec tsx scripts/generate-operation-groups.ts --check
+pnpm --filter @paperclipai/paperclip-runner exec vitest run src/catalog/operation-groups-doc.test.ts src/catalog/reconciliation.test.ts src/catalog/catalog-docs.test.ts
+```
+
+The `--check` path fails on catalog membership, optional-group coverage, control-plane coverage, PRP schema families/counts, behavior/scenario membership, legacy alias folds, source-contract targets, generated live contracts, package exports, or byte-level Markdown drift. Generation is offline and uses only checked-in inputs.
+
+Current responsibility-based paths are normative. Numbered `phase-*` or milestone paths are historical and must not be reintroduced.
+
+## Reconciliation appendix
+
+### Catalog split and deliberate replacement
+
+- Scenario/eval catalog: **37** operations.
+- Live dispatcher catalog: **28** operations.
+- Shared: **24**; union/canonical authority: **41**.
+- Scenario-only: `administer_company`, `export_company`, `inspect_operation_result`, `list_cases`, `list_company_skills`, `list_goals`, `list_projects`, `list_routines`, `list_secret_metadata`, `manage_routine`, `read_secret_value`, `sync_company_skills`, `upsert_case`.
+- Live-only: `get_agent`, `get_approval`, `get_approval_context`, `schedule_wake`.
+- The generated provider contract contains exactly the live catalog; the canonical union remains the migration authority until all scenario-only operations are either implemented, deferred, or removed by an explicit reconciliation decision.
+- `generic_api_request` stays exported only for controlled tests and cannot be cited as real-surface, mock-parity, or PRP product coverage.
+
+### Legacy MCP aliases
+
+The MCP inventory is a compatibility index, not a third product catalog. Every alias folds into an eval scenario and its source-contract semantic target must resolve to a canonical operation, a control-plane operation, an approved composite/fold, or a named gap.
+
+| MCP alias | Source placement / target | Reconciled target | Eval evidence |
+| --- | --- | --- | --- |
+| `paperclipMe` | `control_plane_owned` / `injected_actor_context` | `control_plane` → `select_work`
Identity is launch context, not a model tool. | [`hb-inbox-lite-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/hb-inbox-lite-01.yaml) |
+| `paperclipInboxLite` | `control_plane_owned` / `runner_work_selection` | `control_plane` → `select_work`
Inbox selection belongs to the control plane. | [`hb-inbox-lite-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/hb-inbox-lite-01.yaml) |
+| `paperclipListAgents` | `optional_agent_tool` / `list_agents` | `list_agents` | [`rf-api-mgr-heartbeat-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-api-mgr-heartbeat-01.yaml) |
+| `paperclipGetAgent` | `optional_agent_tool` / `get_agent` | `get_agent` | [`rf-api-mgr-heartbeat-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-api-mgr-heartbeat-01.yaml) |
+| `paperclipListIssues` | `optional_agent_tool` / `search_tasks` | `search_tasks` | [`se-q-filters-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/se-q-filters-01.yaml) |
+| `paperclipGetIssue` | `always_agent_tool` / `get_task_context` | `get_task_context` | [`se-get-issue-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/se-get-issue-01.yaml) |
+| `paperclipGetHeartbeatContext` | `always_agent_tool` / `get_task_context` | `get_task_context` | [`hb-context-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/hb-context-01.yaml) |
+| `paperclipListComments` | `always_agent_tool` / `get_task_history` | `get_task_history` | [`se-get-issue-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/se-get-issue-01.yaml) |
+| `paperclipGetComment` | `always_agent_tool` / `get_task_history` | `get_task_history` | [`hb-wake-comment-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/hb-wake-comment-01.yaml) |
+| `paperclipListIssueApprovals` | `always_agent_tool` / `get_task_context` | `get_task_context` | [`ap-board-approval-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-board-approval-01.yaml) |
+| `paperclipListDocuments` | `always_agent_tool` / `list_documents` | `list_documents` | [`dp-base-revision-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/dp-base-revision-01.yaml) |
+| `paperclipGetDocument` | `always_agent_tool` / `read_document` | `read_document` | [`dp-base-revision-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/dp-base-revision-01.yaml) |
+| `paperclipListDocumentRevisions` | `always_agent_tool` / `list_document_revisions` | `list_document_revisions` | [`dp-base-revision-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/dp-base-revision-01.yaml) |
+| `paperclipListProjects` | `optional_agent_tool` / `list_projects` | `list_projects` | [`rf-wf-project-setup-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-wf-project-setup-01.yaml) |
+| `paperclipGetProject` | `optional_agent_tool` / `get_project` | `folded` → `list_projects`
The bounded discovery operation owns project list/get projection in V1. | [`rf-wf-project-setup-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-wf-project-setup-01.yaml) |
+| `paperclipGetIssueWorkspaceRuntime` | `optional_agent_tool` / `get_workspace_runtime` | `get_workspace_runtime` | [`rf-iws-start-url-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-iws-start-url-01.yaml) |
+| `paperclipControlIssueWorkspaceServices` | `optional_agent_tool` / `control_workspace_service` | `control_workspace_service` | [`rf-iws-start-url-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-iws-start-url-01.yaml) |
+| `paperclipWaitForIssueWorkspaceService` | `optional_agent_tool` / `wait_for_workspace_service` | `folded` → `control_workspace_service`
Wait is a bounded action of workspace service control. | [`rf-iws-target-restart-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-iws-target-restart-01.yaml) |
+| `paperclipListGoals` | `optional_agent_tool` / `list_goals` | `list_goals` | [`su-parent-goal-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/su-parent-goal-01.yaml) |
+| `paperclipGetGoal` | `optional_agent_tool` / `get_goal` | `folded` → `list_goals`
The bounded discovery operation owns goal list/get projection in V1. | [`su-parent-goal-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/su-parent-goal-01.yaml) |
+| `paperclipListApprovals` | `optional_agent_tool` / `list_approvals` | `list_approvals` | [`ap-approval-wake-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-approval-wake-01.yaml) |
+| `paperclipCreateApproval` | `optional_agent_tool` / `request_approval` | `request_approval` | [`ap-board-approval-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-board-approval-01.yaml) |
+| `paperclipGetApproval` | `optional_agent_tool` / `get_approval` | `get_approval` | [`ap-approval-wake-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-approval-wake-01.yaml) |
+| `paperclipGetApprovalIssues` | `optional_agent_tool` / `get_approval_context` | `get_approval_context` | [`ap-approval-wake-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-approval-wake-01.yaml) |
+| `paperclipListApprovalComments` | `optional_agent_tool` / `get_approval_context` | `get_approval_context` | [`ap-approval-deny-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-approval-deny-01.yaml) |
+| `paperclipCreateIssue` | `optional_agent_tool` / `create_task` | `create_task` | [`su-parent-goal-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/su-parent-goal-01.yaml) |
+| `paperclipUpdateIssue` | `optional_agent_tool` / `semantic_task_disposition` | `composite` → `answer_status_question`, `finish_task`, `block_task`, `request_review`
Generic issue PATCH is replaced by intent-specific terminal/status operations. | [`st-done-comment-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/st-done-comment-01.yaml) |
+| `paperclipCheckoutIssue` | `control_plane_owned` / `atomic_checkout` | `control_plane` → `checkout_task`
Checkout is an atomic control-plane transaction. | [`co-body-contract-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/co-body-contract-01.yaml) |
+| `paperclipReleaseIssue` | `control_plane_owned` / `runtime_release` | `control_plane` → `release_task`
Release is runner/control-plane cleanup. | [`er-release-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/er-release-01.yaml) |
+| `paperclipAddComment` | `always_agent_tool` / `report_progress` | `report_progress` | [`cm-multiline-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/cm-multiline-01.yaml) |
+| `paperclipSuggestTasks` | `always_agent_tool` / `request_human_input` | `request_human_input` | [`ix-suggest-tasks-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ix-suggest-tasks-01.yaml) |
+| `paperclipAskUserQuestions` | `always_agent_tool` / `request_human_input` | `request_human_input` | [`ix-questions-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ix-questions-01.yaml) |
+| `paperclipRequestConfirmation` | `always_agent_tool` / `request_human_input` | `request_human_input` | [`ix-confirmation-plan-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ix-confirmation-plan-01.yaml) |
+| `paperclipRequestCheckboxConfirmation` | `always_agent_tool` / `request_human_input` | `request_human_input` | [`ix-checkbox-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ix-checkbox-01.yaml) |
+| `paperclipUpsertIssueDocument` | `always_agent_tool` / `write_document` | `write_document` | [`dp-plan-doc-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/dp-plan-doc-01.yaml) |
+| `paperclipRestoreIssueDocumentRevision` | `optional_agent_tool` / `restore_document_revision` | `known_gap` → named gap
Approved optional documents:restore operation is not yet in the canonical catalog. | [`dp-base-revision-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/dp-base-revision-01.yaml) |
+| `paperclipLinkIssueApproval` | `optional_agent_tool` / `link_approval` | `folded` → `request_approval`, `get_task_context`
Issue linkage is part of approval request/context composites. | [`ap-board-approval-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-board-approval-01.yaml) |
+| `paperclipUnlinkIssueApproval` | `optional_agent_tool` / `unlink_approval` | `control_plane` → `append_audit_record`
No standalone agent unlink tool is approved in V1. | [`ap-board-approval-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-board-approval-01.yaml) |
+| `paperclipApprovalDecision` | `optional_agent_tool` / `decide_approval` | `decide_approval` | [`ap-approval-wake-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-approval-wake-01.yaml) |
+| `paperclipAddApprovalComment` | `optional_agent_tool` / `comment_on_approval` | `comment_on_approval` | [`ap-approval-deny-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/ap-approval-deny-01.yaml) |
+| `paperclipApiRequest` | `optional_agent_tool` / `test_only_api_escape_hatch` | `folded` → `generic_api_request`
Compatibility alias for the test-only escape hatch. | [`rf-api-404-report-01`](https://github.com/paperclipai/paperclip-evals/blob/master/paperclip-skill-optimization/skills/paperclip/tests/cases/rf-api-404-report-01.yaml) |
+
+### PRP expressiveness boundary
+
+PRP v1 already represents runner/session/turn/item lifecycle, replay identity, capability negotiation, request/interaction routing, result negotiation, attention, work assessment, issue-status decision, and terminal causality. Provider-neutral semantic-operation receipts (including authorization/redaction/idempotency/conflict facts) and explicit budget-stop reasons are additive v1 work. Destructive administration, cross-company actions, board-only governance, document lock/unlock/delete, checkout selection, audit persistence, and final arbitration remain control-plane-local and do not become model tools or generic PRP commands.
diff --git a/packages/paperclip-runner/src/catalog/operation-groups-doc.test.ts b/packages/paperclip-runner/src/catalog/operation-groups-doc.test.ts
new file mode 100644
index 00000000000..a4cd34ad722
--- /dev/null
+++ b/packages/paperclip-runner/src/catalog/operation-groups-doc.test.ts
@@ -0,0 +1,86 @@
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { describe, expect, it } from "vitest";
+
+import {
+ renderOperationGroupsDocument,
+ validateOperationGroupsInput,
+} from "./operation-groups-doc.js";
+import {
+ loadOperationGroupsInput,
+ operationGroupsPackageRoot,
+} from "./operation-groups-inputs.js";
+
+describe("canonical operation-groups specification", () => {
+ const input = loadOperationGroupsInput();
+
+ it("reconciles catalog, aliases, source/generated contracts, PRP schemas, scenarios, and exports", () => {
+ expect(() => validateOperationGroupsInput(input)).not.toThrow();
+ });
+
+ it("rejects each protected catalog/alias/contract/export drift class", () => {
+ const variants = [
+ {
+ name: "catalog",
+ value: { ...input, canonicalOperations: input.canonicalOperations.slice(1) },
+ },
+ {
+ name: "alias",
+ value: { ...input, legacyAliases: input.legacyAliases.slice(1) },
+ },
+ {
+ name: "source contract",
+ value: {
+ ...input,
+ sourceContractToolMappings: {
+ ...input.sourceContractToolMappings,
+ paperclipMe: ["control_plane_owned", "unreviewed_target"] as const,
+ },
+ },
+ },
+ {
+ name: "generated contract",
+ value: {
+ ...input,
+ generatedContractOperationIds: input.generatedContractOperationIds.slice(1),
+ },
+ },
+ {
+ name: "package export",
+ value: { ...input, rootIndexSource: "" },
+ },
+ ];
+ for (const variant of variants) {
+ expect(
+ () => validateOperationGroupsInput(variant.value),
+ `${variant.name} drift should fail`,
+ ).toThrow(/Operation-groups drift detected/);
+ }
+ });
+
+ it("accounts for every operation and all 16/106 behavior groups", () => {
+ const behaviorOperationIds = new Set(
+ input.source.behaviorGroups.flatMap((group) => group.operationIds),
+ );
+ expect(behaviorOperationIds).toEqual(
+ new Set(input.canonicalOperations.map((operation) => operation.operationId)),
+ );
+ expect(input.source.behaviorGroups).toHaveLength(16);
+ expect(input.scenarios).toHaveLength(106);
+ expect(new Set(input.scenarios.map((scenario) => scenario.id)).size).toBe(106);
+ expect(input.eventTypes).toHaveLength(65);
+ expect(input.commandTypes).toHaveLength(13);
+ });
+
+ it("keeps the checked-in Markdown byte-identical to the generated contract", () => {
+ const current = readFileSync(
+ resolve(operationGroupsPackageRoot(), "spec/paperclip-agent-operation-groups.md"),
+ "utf8",
+ );
+ expect(current).toBe(renderOperationGroupsDocument(input));
+ for (const scenario of input.scenarios) {
+ expect(current).toContain(`[\`${scenario.id}\`](`);
+ }
+ expect(current).not.toMatch(/(?:^|\/)phase-[0-9]/m);
+ });
+});
diff --git a/packages/paperclip-runner/src/catalog/operation-groups-doc.ts b/packages/paperclip-runner/src/catalog/operation-groups-doc.ts
new file mode 100644
index 00000000000..80050c52d6f
--- /dev/null
+++ b/packages/paperclip-runner/src/catalog/operation-groups-doc.ts
@@ -0,0 +1,623 @@
+import type { CapabilityCatalogReconciliation } from "./reconciliation.js";
+
+export interface OperationGroupsCanonicalOperation {
+ operationId: string;
+ surfaces: readonly string[];
+ placement: "always_agent_tool" | "optional_agent_tool";
+ requiredClaims: readonly string[];
+ taskModes: readonly string[];
+ allowedRoles?: readonly string[];
+ sideEffectClass: string;
+ idempotency: string;
+ redacts: boolean;
+ mockCommandMapping: null | {
+ kind: string;
+ projection?: string;
+ commandKind?: string;
+ extension?: string;
+ };
+ realBindingStatus: string;
+ realServiceBinding: string;
+ prpEvidence: string;
+ prpBindingStatus: string;
+}
+
+export interface OperationGroupsSource {
+ schemaVersion: number;
+ documentTitle: string;
+ evalRepositoryUrl: string;
+ prpEventFamilyDescriptions: Record;
+ prpCommandFamilyDescriptions: Record;
+ optionalGrantGroups: Array<{
+ id: string;
+ description: string;
+ operationIds: string[];
+ }>;
+ behaviorGroups: BehaviorGroupSource[];
+ rules: Record<"companyAuthorization" | "taskModes" | "redaction" | "idempotency" | "sideEffects", string[]>;
+ documentLifecycle: {
+ scope: string[];
+ operations: Array<{
+ action: string;
+ semanticOperation: string;
+ placement: string;
+ contract: string;
+ status: string;
+ }>;
+ };
+ legacySemanticTargetDispositions: Record;
+}
+
+export interface BehaviorGroupSource {
+ id: string;
+ legacyGroup: number;
+ name: string;
+ owner: string;
+ operationIds: string[];
+ controlPlaneOperationIds: string[];
+ realSurface: string;
+ mockStateDomains: string[];
+ prpEvidence: string;
+ gap: string;
+}
+
+export interface OperationGroupsScenario {
+ id: string;
+ title: string;
+ group: string;
+ legacyGroup: number;
+ sourceAnchor: string;
+ expectedSemantics?: string[];
+ requiredGrants?: string[];
+ evidenceIds?: string[];
+}
+
+export interface LegacyAliasRow {
+ id: string;
+ name: string;
+ foldedInto: string;
+ sourceAnchor: string;
+}
+
+export interface OperationGroupsInput {
+ source: OperationGroupsSource;
+ canonicalOperations: readonly OperationGroupsCanonicalOperation[];
+ reconciliation: CapabilityCatalogReconciliation;
+ controlPlaneOperationIds: readonly string[];
+ liveCatalogOperationIds: readonly string[];
+ generatedContractOperationIds: readonly string[];
+ scenarios: readonly OperationGroupsScenario[];
+ eventTypes: readonly string[];
+ commandTypes: readonly string[];
+ legacyAliases: readonly LegacyAliasRow[];
+ sourceContractToolMappings: Readonly>;
+ rootIndexSource: string;
+ catalogIndexSource: string;
+}
+
+const EXPECTED_EVENT_COUNT = 65;
+const EXPECTED_COMMAND_COUNT = 13;
+const EXPECTED_SCENARIO_COUNT = 106;
+const EXPECTED_BEHAVIOR_GROUP_COUNT = 16;
+
+export function validateOperationGroupsInput(input: OperationGroupsInput): void {
+ const failures: string[] = [];
+ const fail = (message: string): void => {
+ failures.push(message);
+ };
+
+ if (input.source.schemaVersion !== 1) fail(`Unsupported operation-groups source schema ${input.source.schemaVersion}.`);
+ if (input.eventTypes.length !== EXPECTED_EVENT_COUNT) {
+ fail(`PRP event catalog has ${input.eventTypes.length} events; expected ${EXPECTED_EVENT_COUNT}.`);
+ }
+ if (input.commandTypes.length !== EXPECTED_COMMAND_COUNT) {
+ fail(`PRP command catalog has ${input.commandTypes.length} commands; expected ${EXPECTED_COMMAND_COUNT}.`);
+ }
+ if (input.scenarios.length !== EXPECTED_SCENARIO_COUNT) {
+ fail(`Behavior catalog has ${input.scenarios.length} scenarios; expected ${EXPECTED_SCENARIO_COUNT}.`);
+ }
+ if (input.source.behaviorGroups.length !== EXPECTED_BEHAVIOR_GROUP_COUNT) {
+ fail(`Operation-groups source has ${input.source.behaviorGroups.length} behavior groups; expected ${EXPECTED_BEHAVIOR_GROUP_COUNT}.`);
+ }
+
+ compareSets(
+ "PRP event-family descriptions",
+ Object.keys(input.source.prpEventFamilyDescriptions),
+ families(input.eventTypes),
+ fail,
+ );
+ compareSets(
+ "PRP command-family descriptions",
+ Object.keys(input.source.prpCommandFamilyDescriptions),
+ families(input.commandTypes),
+ fail,
+ );
+
+ const canonicalIds = input.canonicalOperations.map((operation) => operation.operationId);
+ const alwaysIds = input.canonicalOperations
+ .filter((operation) => operation.placement === "always_agent_tool")
+ .map((operation) => operation.operationId);
+ const optionalIds = input.canonicalOperations
+ .filter((operation) => operation.placement === "optional_agent_tool")
+ .map((operation) => operation.operationId);
+ const groupedOptionalIds = input.source.optionalGrantGroups.flatMap((group) => group.operationIds);
+ compareSets("optional grant-group operations", groupedOptionalIds, optionalIds, fail);
+ reportDuplicates("optional grant-group operations", groupedOptionalIds, fail);
+
+ const behaviorOperationIds = input.source.behaviorGroups.flatMap((group) => group.operationIds);
+ compareSets("behavior coverage semantic operations", behaviorOperationIds, canonicalIds, fail);
+ const behaviorControlPlaneIds = input.source.behaviorGroups.flatMap(
+ (group) => group.controlPlaneOperationIds,
+ );
+ compareSets(
+ "behavior coverage control-plane operations",
+ behaviorControlPlaneIds,
+ input.controlPlaneOperationIds,
+ fail,
+ );
+
+ const scenariosByGroup = groupBy(input.scenarios, (scenario) => scenario.group);
+ compareSets(
+ "behavior group ids",
+ input.source.behaviorGroups.map((group) => group.id),
+ [...scenariosByGroup.keys()],
+ fail,
+ );
+ for (const group of input.source.behaviorGroups) {
+ const rows = scenariosByGroup.get(group.id) ?? [];
+ const legacyGroups = new Set(rows.map((row) => row.legacyGroup));
+ if (legacyGroups.size !== 1 || !legacyGroups.has(group.legacyGroup)) {
+ fail(`Behavior group ${group.id} legacyGroup disagrees with scenario authority.`);
+ }
+ }
+ reportDuplicates("scenario ids", input.scenarios.map((scenario) => scenario.id), fail);
+
+ compareSets(
+ "generated semantic contract",
+ input.generatedContractOperationIds,
+ input.liveCatalogOperationIds,
+ fail,
+ );
+ const liveCanonicalIds = input.canonicalOperations
+ .filter((operation) => operation.surfaces.includes("live"))
+ .map((operation) => operation.operationId);
+ compareSets("canonical live surface", input.liveCatalogOperationIds, liveCanonicalIds, fail);
+
+ const sourceContractNames = Object.keys(input.sourceContractToolMappings);
+ compareSets(
+ "legacy alias inventory/source contract",
+ input.legacyAliases.map((alias) => alias.name),
+ sourceContractNames,
+ fail,
+ );
+ const scenarioIds = new Set(input.scenarios.map((scenario) => scenario.id));
+ const canonicalById = new Map(input.canonicalOperations.map((operation) => [operation.operationId, operation]));
+ const controlPlaneIds = new Set(input.controlPlaneOperationIds);
+ const nonCanonicalSourceTargets = new Set();
+ for (const alias of input.legacyAliases) {
+ const evalId = alias.foldedInto.replace(/^eval:/, "");
+ if (!scenarioIds.has(evalId)) fail(`Legacy alias ${alias.name} folds into missing scenario ${alias.foldedInto}.`);
+ const mapping = input.sourceContractToolMappings[alias.name];
+ if (mapping === undefined) continue;
+ const [placement, semanticTarget] = mapping;
+ const canonical = canonicalById.get(semanticTarget);
+ if (canonical !== undefined) {
+ if (canonical.placement !== placement) {
+ fail(`Source-contract placement for ${alias.name} is ${placement}; canonical ${semanticTarget} is ${canonical.placement}.`);
+ }
+ continue;
+ }
+ nonCanonicalSourceTargets.add(semanticTarget);
+ const disposition = input.source.legacySemanticTargetDispositions[semanticTarget];
+ if (disposition === undefined) {
+ fail(`Source-contract semantic target ${semanticTarget} has no canonical operation or reconciliation disposition.`);
+ continue;
+ }
+ for (const target of disposition.targets) {
+ if (!canonicalById.has(target) && !controlPlaneIds.has(target)) {
+ fail(`Legacy target ${semanticTarget} points at unknown reconciliation target ${target}.`);
+ }
+ }
+ }
+ compareSets(
+ "legacy source-target dispositions",
+ Object.keys(input.source.legacySemanticTargetDispositions),
+ [...nonCanonicalSourceTargets],
+ fail,
+ );
+
+ if (!input.rootIndexSource.includes('export * from "./catalog/index.js";')) {
+ fail("Package root does not export the canonical catalog barrel.");
+ }
+ if (!/export \* from ["']\.\/reconciliation\.js["'];/.test(input.catalogIndexSource)) {
+ fail("Catalog barrel does not export the canonical reconciliation authority.");
+ }
+
+ if (input.reconciliation.unionCount !== canonicalIds.length) {
+ fail("Catalog reconciliation union count disagrees with canonical operations.");
+ }
+ if (input.reconciliation.alwaysCount !== alwaysIds.length) {
+ fail("Catalog reconciliation always count disagrees with canonical operations.");
+ }
+ if (input.reconciliation.optionalCount !== optionalIds.length) {
+ fail("Catalog reconciliation optional count disagrees with canonical operations.");
+ }
+
+ if (failures.length > 0) {
+ throw new Error(`Operation-groups drift detected:\n- ${failures.join("\n- ")}`);
+ }
+}
+
+export function renderOperationGroupsDocument(input: OperationGroupsInput): string {
+ validateOperationGroupsInput(input);
+ const {
+ source,
+ canonicalOperations,
+ reconciliation,
+ controlPlaneOperationIds,
+ scenarios,
+ eventTypes,
+ commandTypes,
+ legacyAliases,
+ sourceContractToolMappings,
+ } = input;
+ const lines: string[] = [];
+ const always = canonicalOperations.filter((operation) => operation.placement === "always_agent_tool");
+ const optional = canonicalOperations.filter((operation) => operation.placement === "optional_agent_tool");
+ const scenarioById = new Map(scenarios.map((scenario) => [scenario.id, scenario]));
+
+ lines.push(
+ "",
+ "",
+ `# ${source.documentTitle}`,
+ "",
+ "Status: canonical explanatory contract for the Paperclip runner V1 surface.",
+ "",
+ "This document keeps three independent meanings of **group** separate. PRP families describe wire evidence and controller commands; capability placement decides who owns an operation; behavioral eval groups organize the 106 scenario corpus. None of the three axes can be used as a substitute for another.",
+ "",
+ "The generated totals are **65 PRP events in 15 event families**, **13 controller commands in 6 command families**, **10 control-plane operations**, **41 reconciled semantic operations** (14 always, 27 optional), and **106 scenarios in 16 behavior groups**.",
+ "",
+ "## Axis 1: PRP v1 event and command families",
+ "",
+ "PRP records ordered, replayable execution evidence. It is not the model's Paperclip tool API. Events are ordered per `sourceInstanceId`; duplicate `sourceEventId` values are idempotent; source-sequence gaps remain evidence; replay is side-effect free; unknown required versions fail closed.",
+ "",
+ "### Event families",
+ "",
+ "| Family | Purpose | Events | Count |",
+ "| --- | --- | --- | ---: |",
+ );
+ for (const [family, members] of groupedFamilies(eventTypes)) {
+ lines.push(row([
+ code(family),
+ source.prpEventFamilyDescriptions[family] ?? "",
+ members.map(code).join("
"),
+ String(members.length),
+ ]));
+ }
+ lines.push(
+ "",
+ "### Controller-command families",
+ "",
+ "| Family | Purpose | Commands | Count |",
+ "| --- | --- | --- | ---: |",
+ );
+ for (const [family, members] of groupedFamilies(commandTypes)) {
+ lines.push(row([
+ code(family),
+ source.prpCommandFamilyDescriptions[family] ?? "",
+ members.map(code).join("
"),
+ String(members.length),
+ ]));
+ }
+
+ lines.push(
+ "",
+ "## Axis 2: capability placement",
+ "",
+ "Placement has exactly three outcomes:",
+ "",
+ "- `control_plane_owned`: Paperclip or the runner performs the operation; it is absent from the model tool catalog.",
+ "- `always_agent_tool`: every eligible active-task run receives the operation after task-mode and actor checks.",
+ "- `optional_agent_tool`: the operation is exposed only when every declared claim, task-mode, role, and policy condition passes.",
+ "",
+ "### Control-plane-owned operations",
+ "",
+ "| Operation | Why it is not a model tool |",
+ "| --- | --- |",
+ );
+ const controlPlaneReasons: Record = {
+ checkout_task: "Atomic checkout and execution-lock ownership.",
+ release_task: "Run cleanup and checkout release.",
+ select_work: "Scoped wake/inbox work selection.",
+ route_wake: "Attention, blocker, interaction, approval, and continuation routing.",
+ enforce_budget: "Budget hard stop, pause, and run-stop reason.",
+ append_audit_record: "Immutable mutation evidence.",
+ persist_run: "Durable run/event/checkpoint persistence.",
+ replay_run: "Side-effect-free replay and reconstruction.",
+ schedule_blocker_wake: "Dependency-resolution wake scheduling.",
+ reconcile_run: "Work assessment, terminal status arbitration, and recovery reconciliation.",
+ };
+ for (const operationId of controlPlaneOperationIds) {
+ lines.push(row([code(operationId), controlPlaneReasons[operationId] ?? "Control-plane invariant."]));
+ }
+
+ lines.push(
+ "",
+ `### Always-agent operations (${always.length})`,
+ "",
+ always.map((operation) => code(operation.operationId)).join(", ") + ".",
+ "",
+ `### Optional operations (${optional.length}) and grant groups (${source.optionalGrantGroups.length})`,
+ "",
+ "Grant groups are documentation/exposure bundles, not additional authority. The operation descriptor's exact `requiredClaims` remains decisive.",
+ "",
+ "| Grant group | Operations | Required claims represented | Purpose |",
+ "| --- | --- | --- | --- |",
+ );
+ const operationById = new Map(canonicalOperations.map((operation) => [operation.operationId, operation]));
+ for (const group of source.optionalGrantGroups) {
+ const claims = unique(group.operationIds.flatMap((id) => operationById.get(id)?.requiredClaims ?? []));
+ lines.push(row([
+ code(group.id),
+ group.operationIds.map(code).join("
"),
+ claims.length === 0 ? "none" : claims.map(code).join("
"),
+ group.description,
+ ]));
+ }
+
+ lines.push(
+ "",
+ "### Reconciled semantic-operation ledger",
+ "",
+ "`live_codex` means a live provider dispatcher exists against the deterministic runner/mock port; it does **not** mean a production Paperclip service is bound. Every row remains `realServiceBinding: unbound` until the shared mock/real adapter work lands. `generic_api_request` is test-only and cannot satisfy product coverage.",
+ "",
+ "| Operation | Placement | Claims | Modes / roles | Side effect | Idempotency | Redacts | Mock | Catalogs / current runner | Production / PRP evidence |",
+ "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
+ );
+ for (const operation of canonicalOperations) {
+ lines.push(row([
+ code(operation.operationId),
+ code(operation.placement),
+ listOrNone(operation.requiredClaims),
+ `${listOrNone(operation.taskModes)}${operation.allowedRoles === undefined ? "" : `
roles: ${listOrNone(operation.allowedRoles)}`}`,
+ code(operation.sideEffectClass),
+ code(operation.idempotency),
+ operation.redacts ? "yes" : "no",
+ operation.mockCommandMapping === null ? "inline/no mapping" : code(mockMappingLabel(operation.mockCommandMapping)),
+ `${operation.surfaces.map(code).join(" + ")}
${code(operation.realBindingStatus)}`,
+ `${code(operation.realServiceBinding)}
${operation.prpEvidence}
catalog PRP status: ${code(operation.prpBindingStatus)}`,
+ ]));
+ }
+
+ lines.push(
+ "",
+ "## Axis 3: the 16 behavioral eval groups",
+ "",
+ "Behavior groups describe expected outcomes and trajectories. They do not grant tools and they do not define PRP event types. The matrix is generated from the behavior-group source plus the checked-in eval traceability manifest; scenario counts and membership cannot be hand-edited here.",
+ "",
+ "### Coverage matrix",
+ "",
+ "| Group | Owner | Semantic operations | Control-plane operations | Real Paperclip surface | Mock state | Scenarios | PRP evidence | Gap / disposition |",
+ "| --- | --- | --- | --- | --- | --- | --- | ---: | --- | --- |",
+ );
+ const scenariosByGroup = groupBy(scenarios, (scenario) => scenario.group);
+ for (const group of source.behaviorGroups) {
+ lines.push(row([
+ `[\`${group.id}\` — ${group.name}](#behavior-group-${group.id}-${slug(group.name)})`,
+ group.owner,
+ group.operationIds.length === 0 ? "none" : group.operationIds.map(code).join("
"),
+ group.controlPlaneOperationIds.length === 0 ? "none" : group.controlPlaneOperationIds.map(code).join("
"),
+ group.realSurface,
+ group.mockStateDomains.map(code).join("
"),
+ String((scenariosByGroup.get(group.id) ?? []).length),
+ group.prpEvidence,
+ group.gap,
+ ]));
+ }
+
+ lines.push("", "### Scenario links");
+ for (const group of source.behaviorGroups) {
+ const members = [...(scenariosByGroup.get(group.id) ?? [])].sort((left, right) => left.id.localeCompare(right.id));
+ lines.push(
+ "",
+ `#### Behavior group ${group.id}: ${group.name}`,
+ "",
+ `${members.length} scenarios (legacy group ${group.legacyGroup}):`,
+ "",
+ );
+ for (const scenario of members) {
+ lines.push(`- [\`${scenario.id}\`](${scenarioUrl(source, scenario)}) — ${scenario.title}`);
+ }
+ }
+
+ lines.push("", "## Authorization and execution invariants");
+ pushRuleSection(lines, "Company and actor authorization", source.rules.companyAuthorization);
+ pushRuleSection(lines, "Task modes and exposure", source.rules.taskModes);
+ pushRuleSection(lines, "Redaction", source.rules.redaction);
+ pushRuleSection(lines, "Idempotency and replay", source.rules.idempotency);
+ pushRuleSection(lines, "Side effects and terminal ownership", source.rules.sideEffects);
+
+ lines.push("", "## Issue-document lifecycle", "");
+ for (const paragraph of source.documentLifecycle.scope) lines.push(paragraph, "");
+ lines.push(
+ "| Action | Semantic operation | Placement | Contract | Status |",
+ "| --- | --- | --- | --- | --- |",
+ );
+ for (const operation of source.documentLifecycle.operations) {
+ lines.push(row([
+ code(operation.action),
+ code(operation.semanticOperation),
+ code(operation.placement),
+ operation.contract,
+ operation.status,
+ ]));
+ }
+ lines.push(
+ "",
+ "All document writes produce a normalized receipt. Success includes the document key/id, prior and new revision identity, idempotency key, and audit reference. Missing/stale bases return `base_revision_required` or `stale_base_revision` with the current revision. Authorization, cross-company, and lock failures use stable redacted denial codes. The runner never overwrites after a conflict without a fresh read and explicit reconciled write.",
+ "",
+ "## Provenance, generation, and drift gates",
+ "",
+ "The machine-readable authority for this document's decisions is `spec/operation-groups/source.json`. The generator joins that source to these independently versioned authorities and rejects disagreement:",
+ "",
+ "- the package-exported `CAPABILITY_CANONICAL_CATALOG` in `src/catalog/` — the 41-operation authority, including enriched mock/redaction and divergence facts.",
+ "- `src/tools/capability-semantic-tool-types.ts` — 10 control-plane-owned operation IDs.",
+ "- `protocol/schemas/event.schema.json` and `command.schema.json` — PRP event/command members and families.",
+ "- `spec/capability/eval-traceability.yaml` — all 16 groups and 106 scenario identities/source anchors.",
+ "- `spec/capability/source-contract.json` and `mcp-tool-map.yaml` — legacy MCP placement and fold authority.",
+ "- `generated/capability/semantic-tool-contracts.json` — generated provider contract; it must equal the live catalog exactly.",
+ "- `src/catalog/index.ts` and `src/index.ts` — package export path for the canonical reconciliation authority.",
+ "",
+ "Regenerate and check reproducibly:",
+ "",
+ "```sh",
+ "pnpm --filter @paperclipai/paperclip-runner exec tsx scripts/generate-operation-groups.ts",
+ "pnpm --filter @paperclipai/paperclip-runner exec tsx scripts/generate-operation-groups.ts --check",
+ "pnpm --filter @paperclipai/paperclip-runner exec vitest run src/catalog/operation-groups-doc.test.ts src/catalog/reconciliation.test.ts src/catalog/catalog-docs.test.ts",
+ "```",
+ "",
+ "The `--check` path fails on catalog membership, optional-group coverage, control-plane coverage, PRP schema families/counts, behavior/scenario membership, legacy alias folds, source-contract targets, generated live contracts, package exports, or byte-level Markdown drift. Generation is offline and uses only checked-in inputs.",
+ "",
+ "Current responsibility-based paths are normative. Numbered `phase-*` or milestone paths are historical and must not be reintroduced.",
+ "",
+ "## Reconciliation appendix",
+ "",
+ "### Catalog split and deliberate replacement",
+ "",
+ `- Scenario/eval catalog: **${reconciliation.scenarioCount}** operations.`,
+ `- Live dispatcher catalog: **${reconciliation.liveCount}** operations.`,
+ `- Shared: **${reconciliation.sharedCount}**; union/canonical authority: **${reconciliation.unionCount}**.`,
+ `- Scenario-only: ${reconciliation.scenarioOnly.map(code).join(", ")}.`,
+ `- Live-only: ${reconciliation.liveOnly.map(code).join(", ")}.`,
+ "- The generated provider contract contains exactly the live catalog; the canonical union remains the migration authority until all scenario-only operations are either implemented, deferred, or removed by an explicit reconciliation decision.",
+ "- `generic_api_request` stays exported only for controlled tests and cannot be cited as real-surface, mock-parity, or PRP product coverage.",
+ "",
+ "### Legacy MCP aliases",
+ "",
+ "The MCP inventory is a compatibility index, not a third product catalog. Every alias folds into an eval scenario and its source-contract semantic target must resolve to a canonical operation, a control-plane operation, an approved composite/fold, or a named gap.",
+ "",
+ "| MCP alias | Source placement / target | Reconciled target | Eval evidence |",
+ "| --- | --- | --- | --- |",
+ );
+ for (const alias of legacyAliases) {
+ const mapping = sourceContractToolMappings[alias.name];
+ const semanticTarget = mapping?.[1] ?? "missing";
+ const disposition = source.legacySemanticTargetDispositions[semanticTarget];
+ const reconciled = disposition === undefined
+ ? code(semanticTarget)
+ : `${code(disposition.kind)} → ${disposition.targets.length === 0 ? "named gap" : disposition.targets.map(code).join(", ")}
${disposition.reason}`;
+ const scenarioId = alias.foldedInto.replace(/^eval:/, "");
+ const scenario = scenarioById.get(scenarioId);
+ const evidence = scenario === undefined
+ ? code(alias.foldedInto)
+ : `[\`${scenario.id}\`](${scenarioUrl(source, scenario)})`;
+ lines.push(row([
+ code(alias.name),
+ `${mapping === undefined ? "missing" : code(mapping[0])} / ${code(semanticTarget)}`,
+ reconciled,
+ evidence,
+ ]));
+ }
+
+ lines.push(
+ "",
+ "### PRP expressiveness boundary",
+ "",
+ "PRP v1 already represents runner/session/turn/item lifecycle, replay identity, capability negotiation, request/interaction routing, result negotiation, attention, work assessment, issue-status decision, and terminal causality. Provider-neutral semantic-operation receipts (including authorization/redaction/idempotency/conflict facts) and explicit budget-stop reasons are additive v1 work. Destructive administration, cross-company actions, board-only governance, document lock/unlock/delete, checkout selection, audit persistence, and final arbitration remain control-plane-local and do not become model tools or generic PRP commands.",
+ "",
+ );
+ return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
+}
+
+function pushRuleSection(lines: string[], title: string, rules: readonly string[]): void {
+ lines.push("", `### ${title}`, "");
+ for (const rule of rules) lines.push(`- ${rule}`);
+}
+
+function compareSets(
+ label: string,
+ actual: readonly string[],
+ expected: readonly string[],
+ fail: (message: string) => void,
+): void {
+ const actualSet = new Set(actual);
+ const expectedSet = new Set(expected);
+ const missing = [...expectedSet].filter((value) => !actualSet.has(value)).sort();
+ const extra = [...actualSet].filter((value) => !expectedSet.has(value)).sort();
+ if (missing.length > 0 || extra.length > 0) {
+ fail(`${label} disagree (missing: ${missing.join(", ") || "none"}; extra: ${extra.join(", ") || "none"}).`);
+ }
+}
+
+function reportDuplicates(label: string, values: readonly string[], fail: (message: string) => void): void {
+ const seen = new Set();
+ const duplicates = new Set();
+ for (const value of values) {
+ if (seen.has(value)) duplicates.add(value);
+ seen.add(value);
+ }
+ if (duplicates.size > 0) fail(`${label} contain duplicates: ${[...duplicates].sort().join(", ")}.`);
+}
+
+function family(value: string): string {
+ return value.slice(0, value.indexOf("."));
+}
+
+function families(values: readonly string[]): string[] {
+ return unique(values.map(family));
+}
+
+function groupedFamilies(values: readonly string[]): Array<[string, string[]]> {
+ return [...groupBy(values, family).entries()];
+}
+
+function groupBy(values: readonly T[], key: (value: T) => string): Map {
+ const grouped = new Map();
+ for (const value of values) {
+ const id = key(value);
+ const bucket = grouped.get(id) ?? [];
+ bucket.push(value);
+ grouped.set(id, bucket);
+ }
+ return grouped;
+}
+
+function unique(values: readonly string[]): string[] {
+ return [...new Set(values)].sort();
+}
+
+function code(value: string): string {
+ return `\`${value}\``;
+}
+
+function listOrNone(values: readonly string[]): string {
+ return values.length === 0 ? "none" : values.map(code).join("
");
+}
+
+function mockMappingLabel(mapping: NonNullable): string {
+ if ("projection" in mapping) return `${mapping.kind}:${mapping.projection}`;
+ if ("commandKind" in mapping) return `${mapping.kind}:${mapping.commandKind}`;
+ if ("extension" in mapping) return `${mapping.kind}:${mapping.extension}`;
+ return mapping.kind;
+}
+
+function row(cells: readonly string[]): string {
+ return `| ${cells.map(cell).join(" | ")} |`;
+}
+
+function cell(value: string): string {
+ return value.replaceAll("|", "\\|").replaceAll("\n", " ");
+}
+
+function slug(value: string): string {
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
+}
+
+function scenarioUrl(source: OperationGroupsSource, scenario: OperationGroupsScenario): string {
+ const relative = scenario.sourceAnchor.replace(/^paperclip-evals\//, "");
+ return `${source.evalRepositoryUrl}${relative}`;
+}
diff --git a/packages/paperclip-runner/src/catalog/operation-groups-inputs.ts b/packages/paperclip-runner/src/catalog/operation-groups-inputs.ts
new file mode 100644
index 00000000000..817391e4e0e
--- /dev/null
+++ b/packages/paperclip-runner/src/catalog/operation-groups-inputs.ts
@@ -0,0 +1,91 @@
+import { readFileSync } from "node:fs";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { CAPABILITY_SEMANTIC_TOOL_CATALOG as LIVE_CATALOG } from "../semantic-tools/catalog.js";
+import { CAPABILITY_CONTROL_PLANE_OWNED_OPERATION_IDS } from "../tools/capability-semantic-tool-types.js";
+import {
+ CAPABILITY_CANONICAL_CATALOG,
+ capabilityCatalogReconciliation,
+} from "./reconciliation.js";
+import type {
+ LegacyAliasRow,
+ OperationGroupsInput,
+ OperationGroupsScenario,
+ OperationGroupsSource,
+} from "./operation-groups-doc.js";
+
+const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
+
+interface EnumSchema {
+ properties?: Record;
+}
+
+interface ScenarioManifest {
+ rows: OperationGroupsScenario[];
+}
+
+interface LegacyAliasManifest {
+ rows: LegacyAliasRow[];
+}
+
+interface GeneratedContract {
+ name: string;
+ annotations?: { operationId?: string };
+}
+
+interface SourceContract {
+ toolMappings: Record;
+}
+
+export function loadOperationGroupsInput(): OperationGroupsInput {
+ const source = readJson("spec/operation-groups/source.json");
+ const eventSchema = readJson("protocol/schemas/event.schema.json");
+ const commandSchema = readJson("protocol/schemas/command.schema.json");
+ const scenarioManifest = readHeaderJson("spec/capability/eval-traceability.yaml");
+ const aliasManifest = readHeaderJson("spec/capability/mcp-tool-map.yaml");
+ const generatedContracts = readJson(
+ "generated/capability/semantic-tool-contracts.json",
+ );
+ const sourceContract = readJson("spec/capability/source-contract.json");
+ const eventTypes = eventSchema.properties?.eventType?.enum;
+ const commandTypes = commandSchema.properties?.type?.enum;
+ if (eventTypes === undefined || commandTypes === undefined) {
+ throw new Error("PRP event/command schemas do not expose their canonical enums.");
+ }
+ return {
+ source,
+ canonicalOperations: CAPABILITY_CANONICAL_CATALOG,
+ reconciliation: capabilityCatalogReconciliation(),
+ controlPlaneOperationIds: CAPABILITY_CONTROL_PLANE_OWNED_OPERATION_IDS,
+ liveCatalogOperationIds: LIVE_CATALOG.map((operation) => operation.operationId),
+ generatedContractOperationIds: generatedContracts.map(
+ (contract) => contract.annotations?.operationId ?? contract.name,
+ ),
+ scenarios: scenarioManifest.rows,
+ eventTypes,
+ commandTypes,
+ legacyAliases: aliasManifest.rows,
+ sourceContractToolMappings: sourceContract.toolMappings,
+ rootIndexSource: readText("src/index.ts"),
+ catalogIndexSource: readText("src/catalog/index.ts"),
+ };
+}
+
+export function operationGroupsPackageRoot(): string {
+ return packageRoot;
+}
+
+function readText(relativePath: string): string {
+ return readFileSync(resolve(packageRoot, relativePath), "utf8");
+}
+
+function readJson(relativePath: string): T {
+ return JSON.parse(readText(relativePath)) as T;
+}
+
+function readHeaderJson(relativePath: string): T {
+ const source = readText(relativePath);
+ const body = source.startsWith("#") ? source.slice(source.indexOf("\n") + 1) : source;
+ return JSON.parse(body) as T;
+}