From 8ec8a68d4a4ed57372337a1d57860a591d16bcd5 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 11 Aug 2026 15:39:35 +0000 Subject: [PATCH] docs(runner): publish canonical operation groups Co-Authored-By: Paperclip --- .../scripts/generate-operation-groups.ts | 28 + .../spec/operation-groups/source.json | 341 ++++++++++ .../spec/paperclip-agent-operation-groups.md | 493 ++++++++++++++ .../src/catalog/operation-groups-doc.test.ts | 86 +++ .../src/catalog/operation-groups-doc.ts | 623 ++++++++++++++++++ .../src/catalog/operation-groups-inputs.ts | 91 +++ 6 files changed, 1662 insertions(+) create mode 100644 packages/paperclip-runner/scripts/generate-operation-groups.ts create mode 100644 packages/paperclip-runner/spec/operation-groups/source.json create mode 100644 packages/paperclip-runner/spec/paperclip-agent-operation-groups.md create mode 100644 packages/paperclip-runner/src/catalog/operation-groups-doc.test.ts create mode 100644 packages/paperclip-runner/src/catalog/operation-groups-doc.ts create mode 100644 packages/paperclip-runner/src/catalog/operation-groups-inputs.ts 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; +}