mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 18:31:34 +01:00
docs(runner): publish canonical operation groups
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
0a0eb19713
commit
8ec8a68d4a
6 files changed
+1662
No files matched your search
@@ -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`);
|
||||
}
|
||||
@@ -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."}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
<!-- GENERATED FILE — DO NOT EDIT. Run `pnpm --filter @paperclipai/paperclip-runner exec tsx scripts/generate-operation-groups.ts`. -->
|
||||
|
||||
# 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`<br>`runner.reconnected`<br>`runner.disconnected`<br>`runner.draining`<br>`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`<br>`harness.ready`<br>`harness.exited`<br>`harness.diagnostic` | 4 |
|
||||
| `session` | Provider-neutral session open, resume, reconciliation, close, and failure. | `session.starting`<br>`session.started`<br>`session.resuming`<br>`session.resumed`<br>`session.reconciled`<br>`session.closed`<br>`session.failed` | 7 |
|
||||
| `turn` | Model turn submission through terminal turn disposition. | `turn.submitted`<br>`turn.accepted`<br>`turn.started`<br>`turn.completed`<br>`turn.failed`<br>`turn.interrupted`<br>`turn.cancelled` | 7 |
|
||||
| `item` | Provider-neutral model/tool item lifecycle. | `item.started`<br>`item.delta`<br>`item.completed`<br>`item.failed` | 4 |
|
||||
| `mcp_app` | MCP App discovery, initialization, tool, action, host-context, and teardown evidence. | `mcp_app.discovered`<br>`mcp_app.resource.resolved`<br>`mcp_app.initializing`<br>`mcp_app.ready`<br>`mcp_app.tool_input`<br>`mcp_app.tool_result`<br>`mcp_app.action.requested`<br>`mcp_app.action.resolved`<br>`mcp_app.host_context.changed`<br>`mcp_app.failed`<br>`mcp_app.teardown` | 11 |
|
||||
| `runtime_request` | Runtime permission/input request lifecycle. | `runtime_request.created`<br>`runtime_request.resolved`<br>`runtime_request.expired`<br>`runtime_request.cancelled` | 4 |
|
||||
| `interaction` | Issue-thread interaction proposal, materialization, response, delivery, and rejection. | `interaction.request.proposed`<br>`interaction.request.materialized`<br>`interaction.request.rejected`<br>`interaction.response.progressed`<br>`interaction.response.resolved`<br>`interaction.response.delivered` | 6 |
|
||||
| `run` | Structured result negotiation and terminal run outcome. | `run.result.proposed`<br>`run.result.accepted`<br>`run.result.rejected`<br>`run.terminal` | 4 |
|
||||
| `attention` | Continuation and attention routing lifecycle. | `attention.request.proposed`<br>`attention.request.routed`<br>`attention.request.resolved`<br>`attention.request.expired`<br>`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`<br>`issue.status.decision.applied`<br>`issue.status.decision.rejected`<br>`issue.status.decision.superseded` | 4 |
|
||||
|
||||
### Controller-command families
|
||||
|
||||
| Family | Purpose | Commands | Count |
|
||||
| --- | --- | --- | ---: |
|
||||
| `run` | Prepare or cancel a run. | `run.prepare`<br>`run.cancel` | 2 |
|
||||
| `session` | Open, snapshot, or close a normalized provider session. | `session.open`<br>`session.snapshot`<br>`session.close` | 3 |
|
||||
| `turn` | Start, steer, interrupt, or stop a model turn. | `turn.start`<br>`turn.steer`<br>`turn.interrupt`<br>`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`<br>`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`<br>`list_agents`<br>`get_agent`<br>`list_projects`<br>`list_goals` | `discovery:agents:read`<br>`discovery:goals:read`<br>`discovery:projects:read`<br>`discovery:tasks:read` | Company-visible task, agent, project, and goal discovery. |
|
||||
| `delegation_dependencies` | `create_task`<br>`set_dependencies` | `delegation:tasks:create`<br>`dependencies:write` | Create delegated work and maintain dependency edges. |
|
||||
| `governance` | `list_approvals`<br>`get_approval`<br>`get_approval_context`<br>`request_approval`<br>`decide_approval`<br>`comment_on_approval` | `governance:approvals:comment`<br>`governance:approvals:decide`<br>`governance:approvals:read`<br>`governance:approvals:request` | Read, request, comment on, and decide approvals under governed-action checks. |
|
||||
| `cases` | `list_cases`<br>`upsert_case` | `cases:read`<br>`cases:write` | Read and update case summaries without reusing issue-document authority. |
|
||||
| `workspace_runtime` | `get_workspace_runtime`<br>`control_workspace_service` | `workspace:control`<br>`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`<br>`manage_routine` | `routines:read`<br>`routines:write` | Inspect or manage company routines. |
|
||||
| `company_skills` | `list_company_skills`<br>`sync_company_skills` | `company_skills:read`<br>`company_skills:write` | Inspect company skills and synchronize the current agent's skills. |
|
||||
| `secrets` | `list_secret_metadata`<br>`read_secret_value` | `secrets:metadata:read`<br>`secrets:values:read` | Inspect secret metadata or use a brokered secret value without exposing plaintext evidence. |
|
||||
| `portability_admin` | `export_company`<br>`administer_company` | `company:admin`<br>`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`<br>`skill_test`<br>roles: `board`<br>`ceo`<br>`admin` | `admin` | `required` | no | `mock_extension:company.admin` | `scenario`<br>`scenario_mock` | `unbound`<br>company admin/portability item event plus audit record<br>catalog PRP status: `audit_pending` |
|
||||
| `answer_status_question` | `always_agent_tool` | none | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `task_write` | `required` | no | `semantic_command:report_progress` | `scenario` + `live`<br>`live_codex` | `unbound`<br>semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events<br>catalog PRP status: `audit_pending` |
|
||||
| `block_task` | `always_agent_tool` | none | `standard`<br>`skill_test` | `task_write` | `required` | no | `semantic_command:block_task` | `scenario` + `live`<br>`live_codex` | `unbound`<br>semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events<br>catalog PRP status: `audit_pending` |
|
||||
| `comment_on_approval` | `optional_agent_tool` | `governance:approvals:comment` | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `governance` | `required` | no | `semantic_command:comment_on_approval` | `scenario` + `live`<br>`live_codex` | `unbound`<br>approval lifecycle plus governed-wait continuation and audit events<br>catalog PRP status: `audit_pending` |
|
||||
| `control_workspace_service` | `optional_agent_tool` | `workspace:control` | `standard`<br>`skill_test` | `workspace_control` | `required` | no | `semantic_command:control_workspace_service` | `scenario` + `live`<br>`live_codex` | `unbound`<br>workspace service lifecycle event<br>catalog PRP status: `audit_pending` |
|
||||
| `create_task` | `optional_agent_tool` | `delegation:tasks:create` | `standard`<br>`skill_test` | `company_write` | `required` | no | `semantic_command:create_task` | `scenario` + `live`<br>`live_codex` | `unbound`<br>semantic-operation item event plus company-entity state diff and audit record<br>catalog PRP status: `audit_pending` |
|
||||
| `decide_approval` | `optional_agent_tool` | `governance:approvals:decide` | `standard`<br>`skill_test`<br>roles: `board`<br>`approver`<br>`security` | `governance` | `required` | no | `semantic_command:decide_approval` | `scenario` + `live`<br>`live_codex` | `unbound`<br>approval lifecycle plus governed-wait continuation and audit events<br>catalog PRP status: `audit_pending` |
|
||||
| `export_company` | `optional_agent_tool` | `portability:export` | `standard`<br>`skill_test` | `admin` | `required` | no | `mock_extension:portability.export` | `scenario`<br>`scenario_mock` | `unbound`<br>company admin/portability item event plus audit record<br>catalog PRP status: `audit_pending` |
|
||||
| `finish_task` | `always_agent_tool` | none | `standard`<br>`skill_test` | `task_write` | `required` | no | `semantic_command:finish_task` | `scenario` + `live`<br>`live_codex` | `unbound`<br>semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events<br>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`<br>`test_only` | `unbound`<br>test-only; excluded from product PRP evidence<br>catalog PRP status: `audit_pending` |
|
||||
| `get_agent` | `optional_agent_tool` | `discovery:agents:read` | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | inline/no mapping | `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `get_approval` | `optional_agent_tool` | `governance:approvals:read` | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | inline/no mapping | `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `get_approval_context` | `optional_agent_tool` | `governance:approvals:read` | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | inline/no mapping | `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `get_task_context` | `always_agent_tool` | none | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `context_read:active_task` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `get_task_history` | `always_agent_tool` | none | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `snapshot_read:active_task_history` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `get_workspace_runtime` | `optional_agent_tool` | `workspace:read` | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `snapshot_read:active_task_workspace` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `inspect_operation_result` | `always_agent_tool` | none | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `operation_result` | `scenario`<br>`scenario_mock` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `list_agents` | `optional_agent_tool` | `discovery:agents:read` | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `snapshot_read:company_actors` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `list_approvals` | `optional_agent_tool` | `governance:approvals:read` | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `snapshot_read:company_approvals` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `list_cases` | `optional_agent_tool` | `cases:read` | `standard`<br>`skill_test` | `read` | `none` | no | `mock_extension:cases.list` | `scenario`<br>`scenario_mock` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `list_company_skills` | `optional_agent_tool` | `company_skills:read` | `standard`<br>`skill_test` | `read` | `none` | no | `mock_extension:company_skills.list` | `scenario`<br>`scenario_mock` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `list_document_revisions` | `always_agent_tool` | none | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `snapshot_read:active_task_document_revisions` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `list_documents` | `always_agent_tool` | none | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `snapshot_read:active_task_documents` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `list_goals` | `optional_agent_tool` | `discovery:goals:read` | `standard`<br>`skill_test` | `read` | `none` | no | `mock_extension:discovery.goals` | `scenario`<br>`scenario_mock` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `list_projects` | `optional_agent_tool` | `discovery:projects:read` | `standard`<br>`skill_test` | `read` | `none` | no | `mock_extension:discovery.projects` | `scenario`<br>`scenario_mock` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `list_routines` | `optional_agent_tool` | `routines:read` | `standard`<br>`skill_test` | `read` | `none` | no | `mock_extension:routines.list` | `scenario`<br>`scenario_mock` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `list_secret_metadata` | `optional_agent_tool` | `secrets:metadata:read` | `standard`<br>`skill_test` | `read` | `none` | no | `mock_extension:secrets.metadata` | `scenario`<br>`scenario_mock` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `manage_routine` | `optional_agent_tool` | `routines:write` | `standard`<br>`skill_test` | `admin` | `required` | no | `mock_extension:routines.manage` | `scenario`<br>`scenario_mock` | `unbound`<br>company admin/portability item event plus audit record<br>catalog PRP status: `audit_pending` |
|
||||
| `read_document` | `always_agent_tool` | none | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `snapshot_read:active_task_document` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `read_secret_value` | `optional_agent_tool` | `secrets:values:read` | `standard`<br>`skill_test` | `secret_read` | `none` | yes | `mock_extension:secrets.value` | `scenario`<br>`scenario_mock` | `unbound`<br>redacted tool-result item event; secret value never reaches the wire<br>catalog PRP status: `audit_pending` |
|
||||
| `register_deliverable` | `always_agent_tool` | none | `standard`<br>`planning`<br>`skill_test` | `task_write` | `required` | no | `semantic_command:register_deliverable` | `scenario` + `live`<br>`live_codex` | `unbound`<br>semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events<br>catalog PRP status: `audit_pending` |
|
||||
| `report_progress` | `always_agent_tool` | none | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `task_write` | `required` | no | `semantic_command:report_progress` | `scenario` + `live`<br>`live_codex` | `unbound`<br>semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events<br>catalog PRP status: `audit_pending` |
|
||||
| `request_approval` | `optional_agent_tool` | `governance:approvals:request` | `standard`<br>`skill_test` | `governance` | `required` | no | `semantic_command:request_approval` | `scenario` + `live`<br>`live_codex` | `unbound`<br>approval lifecycle plus governed-wait continuation and audit events<br>catalog PRP status: `audit_pending` |
|
||||
| `request_human_input` | `always_agent_tool` | none | `standard`<br>`planning`<br>`skill_test` | `task_write` | `required` | no | `semantic_command:request_human_input` | `scenario` + `live`<br>`live_codex` | `unbound`<br>semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events<br>catalog PRP status: `audit_pending` |
|
||||
| `request_review` | `always_agent_tool` | none | `standard`<br>`skill_test` | `task_write` | `required` | no | `semantic_command:request_review` | `scenario` + `live`<br>`live_codex` | `unbound`<br>semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events<br>catalog PRP status: `audit_pending` |
|
||||
| `schedule_wake` | `optional_agent_tool` | `control_plane:wakes` | `standard`<br>`skill_test` | `task_write` | `required` | no | inline/no mapping | `live`<br>`live_codex` | `unbound`<br>semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events<br>catalog PRP status: `audit_pending` |
|
||||
| `search_tasks` | `optional_agent_tool` | `discovery:tasks:read` | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `snapshot_read:company_tasks` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `set_dependencies` | `optional_agent_tool` | `dependencies:write` | `standard`<br>`skill_test` | `company_write` | `required` | no | `semantic_command:set_dependencies` | `scenario` + `live`<br>`live_codex` | `unbound`<br>semantic-operation item event plus company-entity state diff and audit record<br>catalog PRP status: `audit_pending` |
|
||||
| `sync_company_skills` | `optional_agent_tool` | `company_skills:write` | `standard`<br>`skill_test` | `admin` | `required` | no | `mock_extension:company_skills.sync` | `scenario`<br>`scenario_mock` | `unbound`<br>company admin/portability item event plus audit record<br>catalog PRP status: `audit_pending` |
|
||||
| `upsert_case` | `optional_agent_tool` | `cases:write` | `standard`<br>`skill_test` | `company_write` | `required` | no | `mock_extension:cases.upsert` | `scenario`<br>`scenario_mock` | `unbound`<br>semantic-operation item event plus company-entity state diff and audit record<br>catalog PRP status: `audit_pending` |
|
||||
| `write_document` | `always_agent_tool` | none | `standard`<br>`planning`<br>`skill_test` | `task_write` | `required` | no | `semantic_command:write_document` | `scenario` + `live`<br>`live_codex` | `unbound`<br>semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events<br>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`<br>`enforce_budget` | agent identity, inbox-lite, heartbeat context, budget and active-issue services | `company`<br>`actor`<br>`wake`<br>`task`<br>`budget`<br>`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`<br>`actor`<br>`run`<br>`idempotency`<br>`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`<br>`finish_task`<br>`block_task`<br>`request_review` | `reconcile_run`<br>`append_audit_record` | issue PATCH, review/liveness policy, and native finalization arbitration | `task`<br>`comments`<br>`interactions`<br>`blockers`<br>`audit`<br>`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`<br>`report_progress` | `append_audit_record` | issue comment list/get/create routes | `task`<br>`comments`<br>`actor`<br>`idempotency`<br>`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`<br>`list_agents`<br>`get_agent`<br>`list_projects`<br>`list_goals` | none | company issue search and agent/project/goal list/get routes | `company`<br>`task`<br>`actor`<br>`project`<br>`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`<br>`task`<br>`actor`<br>`blockers`<br>`wake`<br>`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`<br>`set_dependencies` | `schedule_blocker_wake`<br>`route_wake` | issue relations, blocker projection, liveness validation, and blocker wake services | `task`<br>`blockers`<br>`wake`<br>`actor`<br>`audit`<br>`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`<br>`read_document`<br>`list_document_revisions`<br>`write_document` | `append_audit_record` | issue document list/read/upsert/revision/restore/lock/unlock/delete routes | `task`<br>`documents`<br>`interactions`<br>`idempotency`<br>`audit`<br>`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`<br>`documents`<br>`interactions`<br>`wake`<br>`actor`<br>`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`<br>`get_approval`<br>`get_approval_context`<br>`request_approval`<br>`decide_approval`<br>`comment_on_approval` | `route_wake`<br>`append_audit_record` | company approval, decision, issue-link, comment, and governed-action services | `company`<br>`task`<br>`approvals`<br>`actor`<br>`wake`<br>`audit`<br>`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`<br>`artifacts`<br>`workProducts`<br>`workspace`<br>`audit`<br>`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`<br>`control_workspace_service`<br>`schedule_wake`<br>`inspect_operation_result` | `release_task`<br>`enforce_budget`<br>`persist_run`<br>`replay_run`<br>`reconcile_run` | workspace runtime, monitor/recovery, budget, run persistence/replay, release, and terminal services | `workspace`<br>`budget`<br>`run`<br>`wake`<br>`audit`<br>`idempotency`<br>`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`<br>`upsert_case`<br>`list_routines`<br>`manage_routine`<br>`list_company_skills`<br>`sync_company_skills`<br>`list_secret_metadata`<br>`read_secret_value`<br>`export_company`<br>`administer_company`<br>`generic_api_request` | `append_audit_record` | case, routine, company-skill, secret, portability, and administration services | `company`<br>`cases`<br>`routines`<br>`skills`<br>`secrets`<br>`audit`<br>`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`<br>`set_dependencies`<br>`request_human_input`<br>`request_approval`<br>`register_deliverable` | `route_wake`<br>`reconcile_run` | delegation, dependency, interaction, approval, artifact, and terminal orchestration services | `task`<br>`blockers`<br>`interactions`<br>`approvals`<br>`artifacts`<br>`wake`<br>`run`<br>`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`<br>`read_secret_value`<br>`generic_api_request` | `enforce_budget` | task-mode, secret-broker, test-scope, pause, and budget policy checks | `actor`<br>`task`<br>`budget`<br>`secrets`<br>`audit`<br>`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`<br>`get_task_history`<br>`schedule_wake` | `select_work`<br>`route_wake` | wakeup requests, heartbeat context, comment/interaction/approval/blocker wake routing, and scheduled wake services | `wake`<br>`task`<br>`comments`<br>`interactions`<br>`approvals`<br>`blockers`<br>`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`<br>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`<br>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`<br>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`<br>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`<br>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`<br>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`<br>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`<br>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<br>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`<br>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`<br>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`<br>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.
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<string, string>;
|
||||
prpCommandFamilyDescriptions: Record<string, string>;
|
||||
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<string, {
|
||||
kind: "control_plane" | "folded" | "composite" | "known_gap";
|
||||
targets: string[];
|
||||
reason: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
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<Record<string, readonly [string, string]>>;
|
||||
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<string>();
|
||||
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(
|
||||
"<!-- GENERATED FILE — DO NOT EDIT. Run `pnpm --filter @paperclipai/paperclip-runner exec tsx scripts/generate-operation-groups.ts`. -->",
|
||||
"",
|
||||
`# ${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("<br>"),
|
||||
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("<br>"),
|
||||
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<string, string> = {
|
||||
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("<br>"),
|
||||
claims.length === 0 ? "none" : claims.map(code).join("<br>"),
|
||||
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 ? "" : `<br>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(" + ")}<br>${code(operation.realBindingStatus)}`,
|
||||
`${code(operation.realServiceBinding)}<br>${operation.prpEvidence}<br>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("<br>"),
|
||||
group.controlPlaneOperationIds.length === 0 ? "none" : group.controlPlaneOperationIds.map(code).join("<br>"),
|
||||
group.realSurface,
|
||||
group.mockStateDomains.map(code).join("<br>"),
|
||||
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(", ")}<br>${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<string>();
|
||||
const duplicates = new Set<string>();
|
||||
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<T>(values: readonly T[], key: (value: T) => string): Map<string, T[]> {
|
||||
const grouped = new Map<string, T[]>();
|
||||
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("<br>");
|
||||
}
|
||||
|
||||
function mockMappingLabel(mapping: NonNullable<OperationGroupsCanonicalOperation["mockCommandMapping"]>): 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}`;
|
||||
}
|
||||
@@ -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<string, { enum?: string[] }>;
|
||||
}
|
||||
|
||||
interface ScenarioManifest {
|
||||
rows: OperationGroupsScenario[];
|
||||
}
|
||||
|
||||
interface LegacyAliasManifest {
|
||||
rows: LegacyAliasRow[];
|
||||
}
|
||||
|
||||
interface GeneratedContract {
|
||||
name: string;
|
||||
annotations?: { operationId?: string };
|
||||
}
|
||||
|
||||
interface SourceContract {
|
||||
toolMappings: Record<string, readonly [string, string]>;
|
||||
}
|
||||
|
||||
export function loadOperationGroupsInput(): OperationGroupsInput {
|
||||
const source = readJson<OperationGroupsSource>("spec/operation-groups/source.json");
|
||||
const eventSchema = readJson<EnumSchema>("protocol/schemas/event.schema.json");
|
||||
const commandSchema = readJson<EnumSchema>("protocol/schemas/command.schema.json");
|
||||
const scenarioManifest = readHeaderJson<ScenarioManifest>("spec/capability/eval-traceability.yaml");
|
||||
const aliasManifest = readHeaderJson<LegacyAliasManifest>("spec/capability/mcp-tool-map.yaml");
|
||||
const generatedContracts = readJson<GeneratedContract[]>(
|
||||
"generated/capability/semantic-tool-contracts.json",
|
||||
);
|
||||
const sourceContract = readJson<SourceContract>("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<T>(relativePath: string): T {
|
||||
return JSON.parse(readText(relativePath)) as T;
|
||||
}
|
||||
|
||||
function readHeaderJson<T>(relativePath: string): T {
|
||||
const source = readText(relativePath);
|
||||
const body = source.startsWith("#") ? source.slice(source.indexOf("\n") + 1) : source;
|
||||
return JSON.parse(body) as T;
|
||||
}
|
||||
Reference in new issue
Block a user