diff --git a/packages/paperclip-runner/docs/capability-contract.md b/packages/paperclip-runner/docs/capability-contract.md index 364e01f13d..f433b654ca 100644 --- a/packages/paperclip-runner/docs/capability-contract.md +++ b/packages/paperclip-runner/docs/capability-contract.md @@ -8,9 +8,9 @@ The skill/reference inventory and eval cases are the only normative behavior sou ## Baseline Counts -- Skill/reference headings: 153 +- Skill/reference headings: 154 - Eval cases: 106 across 16 groups -- Total normative rows: 259 +- Total normative rows: 260 - Legacy MCP aliases folded into normative rows: 42 | Eval group | Cases | @@ -44,32 +44,33 @@ The skill/reference inventory and eval cases are the only normative behavior sou | skill:skills/paperclip/SKILL.md:paperclip-skill:10 | optional_agent_tool | skills/paperclip/SKILL.md:10 | | skill:skills/paperclip/SKILL.md:terminology:14 | optional_agent_tool | skills/paperclip/SKILL.md:14 | | skill:skills/paperclip/SKILL.md:authentication:18 | control_plane_owned | skills/paperclip/SKILL.md:18 | -| skill:skills/paperclip/SKILL.md:server-verified-external-chat-turns:30 | control_plane_owned | skills/paperclip/SKILL.md:30 | -| skill:skills/paperclip/SKILL.md:the-heartbeat-procedure:70 | optional_agent_tool | skills/paperclip/SKILL.md:70 | -| skill:skills/paperclip/SKILL.md:generated-artifacts-and-work-products:142 | always_agent_tool | skills/paperclip/SKILL.md:142 | -| skill:skills/paperclip/SKILL.md:status-quick-guide:190 | control_plane_owned | skills/paperclip/SKILL.md:190 | -| skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:200 | optional_agent_tool | skills/paperclip/SKILL.md:200 | -| skill:skills/paperclip/SKILL.md:delegating-review-tasks:213 | always_agent_tool | skills/paperclip/SKILL.md:213 | -| skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:224 | control_plane_owned | skills/paperclip/SKILL.md:224 | -| skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:232 | control_plane_owned | skills/paperclip/SKILL.md:232 | -| skill:skills/paperclip/SKILL.md:requesting-board-approval:257 | optional_agent_tool | skills/paperclip/SKILL.md:257 | -| skill:skills/paperclip/SKILL.md:issue-thread-interactions:278 | optional_agent_tool | skills/paperclip/SKILL.md:278 | -| skill:skills/paperclip/SKILL.md:standalone-decisions:307 | optional_agent_tool | skills/paperclip/SKILL.md:307 | -| skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:411 | optional_agent_tool | skills/paperclip/SKILL.md:411 | -| skill:skills/paperclip/SKILL.md:niche-workflow-pointers:453 | optional_agent_tool | skills/paperclip/SKILL.md:453 | -| skill:skills/paperclip/SKILL.md:cases:463 | optional_agent_tool | skills/paperclip/SKILL.md:463 | -| skill:skills/paperclip/SKILL.md:company-skills-workflow:468 | optional_agent_tool | skills/paperclip/SKILL.md:468 | -| skill:skills/paperclip/SKILL.md:routines:479 | optional_agent_tool | skills/paperclip/SKILL.md:479 | -| skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:490 | optional_agent_tool | skills/paperclip/SKILL.md:490 | -| skill:skills/paperclip/SKILL.md:proposing-credentials-safely:497 | optional_agent_tool | skills/paperclip/SKILL.md:497 | -| skill:skills/paperclip/SKILL.md:reading-granted-secrets:504 | optional_agent_tool | skills/paperclip/SKILL.md:504 | -| skill:skills/paperclip/SKILL.md:critical-rules:530 | optional_agent_tool | skills/paperclip/SKILL.md:530 | -| skill:skills/paperclip/SKILL.md:comment-style-required:554 | always_agent_tool | skills/paperclip/SKILL.md:554 | -| skill:skills/paperclip/SKILL.md:update:586 | optional_agent_tool | skills/paperclip/SKILL.md:586 | -| skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:596 | optional_agent_tool | skills/paperclip/SKILL.md:596 | -| skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:629 | optional_agent_tool | skills/paperclip/SKILL.md:629 | -| skill:skills/paperclip/SKILL.md:searching-issues:658 | optional_agent_tool | skills/paperclip/SKILL.md:658 | -| skill:skills/paperclip/SKILL.md:full-reference:668 | optional_agent_tool | skills/paperclip/SKILL.md:668 | +| skill:skills/paperclip/SKILL.md:conversation-tasks:30 | optional_agent_tool | skills/paperclip/SKILL.md:30 | +| skill:skills/paperclip/SKILL.md:server-verified-external-chat-turns:47 | control_plane_owned | skills/paperclip/SKILL.md:47 | +| skill:skills/paperclip/SKILL.md:the-heartbeat-procedure:87 | optional_agent_tool | skills/paperclip/SKILL.md:87 | +| skill:skills/paperclip/SKILL.md:generated-artifacts-and-work-products:159 | always_agent_tool | skills/paperclip/SKILL.md:159 | +| skill:skills/paperclip/SKILL.md:status-quick-guide:207 | control_plane_owned | skills/paperclip/SKILL.md:207 | +| skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:217 | optional_agent_tool | skills/paperclip/SKILL.md:217 | +| skill:skills/paperclip/SKILL.md:delegating-review-tasks:230 | always_agent_tool | skills/paperclip/SKILL.md:230 | +| skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:241 | control_plane_owned | skills/paperclip/SKILL.md:241 | +| skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:249 | control_plane_owned | skills/paperclip/SKILL.md:249 | +| skill:skills/paperclip/SKILL.md:requesting-board-approval:274 | optional_agent_tool | skills/paperclip/SKILL.md:274 | +| skill:skills/paperclip/SKILL.md:issue-thread-interactions:295 | optional_agent_tool | skills/paperclip/SKILL.md:295 | +| skill:skills/paperclip/SKILL.md:standalone-decisions:324 | optional_agent_tool | skills/paperclip/SKILL.md:324 | +| skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:428 | optional_agent_tool | skills/paperclip/SKILL.md:428 | +| skill:skills/paperclip/SKILL.md:niche-workflow-pointers:470 | optional_agent_tool | skills/paperclip/SKILL.md:470 | +| skill:skills/paperclip/SKILL.md:cases:480 | optional_agent_tool | skills/paperclip/SKILL.md:480 | +| skill:skills/paperclip/SKILL.md:company-skills-workflow:485 | optional_agent_tool | skills/paperclip/SKILL.md:485 | +| skill:skills/paperclip/SKILL.md:routines:496 | optional_agent_tool | skills/paperclip/SKILL.md:496 | +| skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:507 | optional_agent_tool | skills/paperclip/SKILL.md:507 | +| skill:skills/paperclip/SKILL.md:proposing-credentials-safely:514 | optional_agent_tool | skills/paperclip/SKILL.md:514 | +| skill:skills/paperclip/SKILL.md:reading-granted-secrets:521 | optional_agent_tool | skills/paperclip/SKILL.md:521 | +| skill:skills/paperclip/SKILL.md:critical-rules:547 | optional_agent_tool | skills/paperclip/SKILL.md:547 | +| skill:skills/paperclip/SKILL.md:comment-style-required:571 | always_agent_tool | skills/paperclip/SKILL.md:571 | +| skill:skills/paperclip/SKILL.md:update:603 | optional_agent_tool | skills/paperclip/SKILL.md:603 | +| skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:613 | optional_agent_tool | skills/paperclip/SKILL.md:613 | +| skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:646 | optional_agent_tool | skills/paperclip/SKILL.md:646 | +| skill:skills/paperclip/SKILL.md:searching-issues:675 | optional_agent_tool | skills/paperclip/SKILL.md:675 | +| skill:skills/paperclip/SKILL.md:full-reference:685 | optional_agent_tool | skills/paperclip/SKILL.md:685 | | skill:skills/paperclip/references/artifacts.md:generated-artifacts-and-work-products:1 | always_agent_tool | skills/paperclip/references/artifacts.md:1 | | skill:skills/paperclip/references/artifacts.md:workspace-only-file-references:15 | optional_agent_tool | skills/paperclip/references/artifacts.md:15 | | skill:skills/paperclip/references/cases.md:cases:1 | optional_agent_tool | skills/paperclip/references/cases.md:1 | @@ -172,28 +173,28 @@ The skill/reference inventory and eval cases are the only normative behavior sou | skill:skills/paperclip/references/api-reference.md:openclaw-invite-prompt-ceo:731 | optional_agent_tool | skills/paperclip/references/api-reference.md:731 | | skill:skills/paperclip/references/api-reference.md:setting-agent-instructions-path:750 | optional_agent_tool | skills/paperclip/references/api-reference.md:750 | | skill:skills/paperclip/references/api-reference.md:project-setup-create-workspace:783 | optional_agent_tool | skills/paperclip/references/api-reference.md:783 | -| skill:skills/paperclip/references/api-reference.md:option-a-one-call-create-with-workspace:787 | optional_agent_tool | skills/paperclip/references/api-reference.md:787 | -| skill:skills/paperclip/references/api-reference.md:option-b-two-calls-project-first-then-workspace:806 | optional_agent_tool | skills/paperclip/references/api-reference.md:806 | -| skill:skills/paperclip/references/api-reference.md:governance-and-approvals:835 | optional_agent_tool | skills/paperclip/references/api-reference.md:835 | -| skill:skills/paperclip/references/api-reference.md:requesting-a-hire-management-only:839 | optional_agent_tool | skills/paperclip/references/api-reference.md:839 | -| skill:skills/paperclip/references/api-reference.md:ceo-strategy-approval:859 | optional_agent_tool | skills/paperclip/references/api-reference.md:859 | -| skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:868 | always_agent_tool | skills/paperclip/references/api-reference.md:868 | -| skill:skills/paperclip/references/api-reference.md:checkbox-confirmations:926 | always_agent_tool | skills/paperclip/references/api-reference.md:926 | -| skill:skills/paperclip/references/api-reference.md:item-verdict-requests:1041 | optional_agent_tool | skills/paperclip/references/api-reference.md:1041 | -| skill:skills/paperclip/references/api-reference.md:checking-approval-status:1151 | optional_agent_tool | skills/paperclip/references/api-reference.md:1151 | -| skill:skills/paperclip/references/api-reference.md:approval-follow-up-requesting-agent:1157 | always_agent_tool | skills/paperclip/references/api-reference.md:1157 | -| skill:skills/paperclip/references/api-reference.md:issue-lifecycle:1175 | always_agent_tool | skills/paperclip/references/api-reference.md:1175 | -| skill:skills/paperclip/references/api-reference.md:error-handling:1205 | control_plane_owned | skills/paperclip/references/api-reference.md:1205 | -| skill:skills/paperclip/references/api-reference.md:full-api-reference:1219 | optional_agent_tool | skills/paperclip/references/api-reference.md:1219 | -| skill:skills/paperclip/references/api-reference.md:agents:1221 | optional_agent_tool | skills/paperclip/references/api-reference.md:1221 | -| skill:skills/paperclip/references/api-reference.md:issues-tasks:1242 | optional_agent_tool | skills/paperclip/references/api-reference.md:1242 | -| skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1282 | optional_agent_tool | skills/paperclip/references/api-reference.md:1282 | -| skill:skills/paperclip/references/api-reference.md:routines:1306 | optional_agent_tool | skills/paperclip/references/api-reference.md:1306 | -| skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1322 | optional_agent_tool | skills/paperclip/references/api-reference.md:1322 | -| skill:skills/paperclip/references/api-reference.md:secrets:1344 | optional_agent_tool | skills/paperclip/references/api-reference.md:1344 | -| skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1357 | optional_agent_tool | skills/paperclip/references/api-reference.md:1357 | -| skill:skills/paperclip/references/api-reference.md:agent-secret-access:1457 | optional_agent_tool | skills/paperclip/references/api-reference.md:1457 | -| skill:skills/paperclip/references/api-reference.md:common-mistakes:1497 | optional_agent_tool | skills/paperclip/references/api-reference.md:1497 | +| skill:skills/paperclip/references/api-reference.md:option-a-one-call-create-with-workspace:807 | optional_agent_tool | skills/paperclip/references/api-reference.md:807 | +| skill:skills/paperclip/references/api-reference.md:option-b-two-calls-project-first-then-workspace:826 | optional_agent_tool | skills/paperclip/references/api-reference.md:826 | +| skill:skills/paperclip/references/api-reference.md:governance-and-approvals:855 | optional_agent_tool | skills/paperclip/references/api-reference.md:855 | +| skill:skills/paperclip/references/api-reference.md:requesting-a-hire-management-only:859 | optional_agent_tool | skills/paperclip/references/api-reference.md:859 | +| skill:skills/paperclip/references/api-reference.md:ceo-strategy-approval:879 | optional_agent_tool | skills/paperclip/references/api-reference.md:879 | +| skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:888 | always_agent_tool | skills/paperclip/references/api-reference.md:888 | +| skill:skills/paperclip/references/api-reference.md:checkbox-confirmations:946 | always_agent_tool | skills/paperclip/references/api-reference.md:946 | +| skill:skills/paperclip/references/api-reference.md:item-verdict-requests:1061 | optional_agent_tool | skills/paperclip/references/api-reference.md:1061 | +| skill:skills/paperclip/references/api-reference.md:checking-approval-status:1171 | optional_agent_tool | skills/paperclip/references/api-reference.md:1171 | +| skill:skills/paperclip/references/api-reference.md:approval-follow-up-requesting-agent:1177 | always_agent_tool | skills/paperclip/references/api-reference.md:1177 | +| skill:skills/paperclip/references/api-reference.md:issue-lifecycle:1195 | always_agent_tool | skills/paperclip/references/api-reference.md:1195 | +| skill:skills/paperclip/references/api-reference.md:error-handling:1225 | control_plane_owned | skills/paperclip/references/api-reference.md:1225 | +| skill:skills/paperclip/references/api-reference.md:full-api-reference:1239 | optional_agent_tool | skills/paperclip/references/api-reference.md:1239 | +| skill:skills/paperclip/references/api-reference.md:agents:1241 | optional_agent_tool | skills/paperclip/references/api-reference.md:1241 | +| skill:skills/paperclip/references/api-reference.md:issues-tasks:1262 | optional_agent_tool | skills/paperclip/references/api-reference.md:1262 | +| skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1302 | optional_agent_tool | skills/paperclip/references/api-reference.md:1302 | +| skill:skills/paperclip/references/api-reference.md:routines:1326 | optional_agent_tool | skills/paperclip/references/api-reference.md:1326 | +| skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1342 | optional_agent_tool | skills/paperclip/references/api-reference.md:1342 | +| skill:skills/paperclip/references/api-reference.md:secrets:1364 | optional_agent_tool | skills/paperclip/references/api-reference.md:1364 | +| skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1377 | optional_agent_tool | skills/paperclip/references/api-reference.md:1377 | +| skill:skills/paperclip/references/api-reference.md:agent-secret-access:1477 | optional_agent_tool | skills/paperclip/references/api-reference.md:1477 | +| skill:skills/paperclip/references/api-reference.md:common-mistakes:1517 | optional_agent_tool | skills/paperclip/references/api-reference.md:1517 | ## Legacy MCP Alias Index diff --git a/packages/paperclip-runner/docs/capability-disposition.md b/packages/paperclip-runner/docs/capability-disposition.md index 62f281797b..7eec1d8b47 100644 --- a/packages/paperclip-runner/docs/capability-disposition.md +++ b/packages/paperclip-runner/docs/capability-disposition.md @@ -12,10 +12,10 @@ the authoritative rows. Only two sources are normative: 1. The Paperclip skill and its seven references (`SKILL.md` plus - `references/*.md`), contributing **152 headings**. + `references/*.md`), contributing **153 headings**. 2. The Paperclip Evals corpus, contributing **106 cases across 16 groups**. -Together these produce **258 normative rows**. The legacy Paperclip MCP tool +Together these produce **259 normative rows**. The legacy Paperclip MCP tool surface (**41 tools**) is not a production capability surface; each MCP name is folded one-to-one into a normative eval row as a traceability alias and inherits that row's disposition. The contract prints the alias index only so the diff --git a/packages/paperclip-runner/generated/capability/capabilities.yaml b/packages/paperclip-runner/generated/capability/capabilities.yaml index 380d91840f..e5b21c57d3 100644 --- a/packages/paperclip-runner/generated/capability/capabilities.yaml +++ b/packages/paperclip-runner/generated/capability/capabilities.yaml @@ -31,232 +31,241 @@ { "id": "skill:skills/paperclip/SKILL.md:30", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L30:server-verified-external-chat-turns", + "sourceAnchor": "skills/paperclip/SKILL.md#L30:conversation-tasks", + "heading": "Conversation tasks", + "primaryDisposition": "control_plane_owned", + "semanticOperation": "runtime_reconciliation", + "expectedMockState": "runtime_decision_record" + }, + { + "id": "skill:skills/paperclip/SKILL.md:47", + "kind": "skill_heading", + "sourceAnchor": "skills/paperclip/SKILL.md#L47:server-verified-external-chat-turns", "heading": "Server-Verified External Chat Turns", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:70", + "id": "skill:skills/paperclip/SKILL.md:87", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L70:the-heartbeat-procedure", + "sourceAnchor": "skills/paperclip/SKILL.md#L87:the-heartbeat-procedure", "heading": "The Heartbeat Procedure", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:142", + "id": "skill:skills/paperclip/SKILL.md:159", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L142:generated-artifacts-and-work-products", + "sourceAnchor": "skills/paperclip/SKILL.md#L159:generated-artifacts-and-work-products", "heading": "Generated Artifacts and Work Products", "primaryDisposition": "always_agent_tool", "semanticOperation": "register_deliverable", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:190", + "id": "skill:skills/paperclip/SKILL.md:207", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L190:status-quick-guide", + "sourceAnchor": "skills/paperclip/SKILL.md#L207:status-quick-guide", "heading": "Status Quick Guide", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:200", + "id": "skill:skills/paperclip/SKILL.md:217", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L200:monitors-and-watchers-say-only-what-you-actually-scheduled", + "sourceAnchor": "skills/paperclip/SKILL.md#L217:monitors-and-watchers-say-only-what-you-actually-scheduled", "heading": "Monitors and Watchers (say only what you actually scheduled)", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:213", + "id": "skill:skills/paperclip/SKILL.md:230", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L213:delegating-review-tasks", + "sourceAnchor": "skills/paperclip/SKILL.md#L230:delegating-review-tasks", "heading": "Delegating review tasks", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:224", + "id": "skill:skills/paperclip/SKILL.md:241", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L224:managing-a-user-s-inbox", + "sourceAnchor": "skills/paperclip/SKILL.md#L241:managing-a-user-s-inbox", "heading": "Managing A User's Inbox", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:232", + "id": "skill:skills/paperclip/SKILL.md:249", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L232:issue-dependencies-blockers", + "sourceAnchor": "skills/paperclip/SKILL.md#L249:issue-dependencies-blockers", "heading": "Issue Dependencies (Blockers)", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:257", + "id": "skill:skills/paperclip/SKILL.md:274", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L257:requesting-board-approval", + "sourceAnchor": "skills/paperclip/SKILL.md#L274:requesting-board-approval", "heading": "Requesting Board Approval", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:278", + "id": "skill:skills/paperclip/SKILL.md:295", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L278:issue-thread-interactions", + "sourceAnchor": "skills/paperclip/SKILL.md#L295:issue-thread-interactions", "heading": "Issue-Thread Interactions", "primaryDisposition": "always_agent_tool", "semanticOperation": "request_human_input", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:307", + "id": "skill:skills/paperclip/SKILL.md:324", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L307:standalone-decisions", + "sourceAnchor": "skills/paperclip/SKILL.md#L324:standalone-decisions", "heading": "Standalone Decisions", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:411", + "id": "skill:skills/paperclip/SKILL.md:428", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L411:mcp-tool-approval-gates", + "sourceAnchor": "skills/paperclip/SKILL.md#L428:mcp-tool-approval-gates", "heading": "MCP Tool Approval Gates", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:453", + "id": "skill:skills/paperclip/SKILL.md:470", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L453:niche-workflow-pointers", + "sourceAnchor": "skills/paperclip/SKILL.md#L470:niche-workflow-pointers", "heading": "Niche Workflow Pointers", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:463", + "id": "skill:skills/paperclip/SKILL.md:480", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L463:cases", + "sourceAnchor": "skills/paperclip/SKILL.md#L480:cases", "heading": "Cases", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:468", + "id": "skill:skills/paperclip/SKILL.md:485", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L468:company-skills-workflow", + "sourceAnchor": "skills/paperclip/SKILL.md#L485:company-skills-workflow", "heading": "Company Skills Workflow", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:479", + "id": "skill:skills/paperclip/SKILL.md:496", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L479:routines", + "sourceAnchor": "skills/paperclip/SKILL.md#L496:routines", "heading": "Routines", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:490", + "id": "skill:skills/paperclip/SKILL.md:507", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L490:issue-workspace-runtime-controls", + "sourceAnchor": "skills/paperclip/SKILL.md#L507:issue-workspace-runtime-controls", "heading": "Issue Workspace Runtime Controls", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:497", + "id": "skill:skills/paperclip/SKILL.md:514", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L497:proposing-credentials-safely", + "sourceAnchor": "skills/paperclip/SKILL.md#L514:proposing-credentials-safely", "heading": "Proposing Credentials Safely", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:504", + "id": "skill:skills/paperclip/SKILL.md:521", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L504:reading-granted-secrets", + "sourceAnchor": "skills/paperclip/SKILL.md#L521:reading-granted-secrets", "heading": "Reading Granted Secrets", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:530", + "id": "skill:skills/paperclip/SKILL.md:547", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L530:critical-rules", + "sourceAnchor": "skills/paperclip/SKILL.md#L547:critical-rules", "heading": "Critical Rules", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:554", + "id": "skill:skills/paperclip/SKILL.md:571", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L554:comment-style-required", + "sourceAnchor": "skills/paperclip/SKILL.md#L571:comment-style-required", "heading": "Comment Style (Required)", "primaryDisposition": "always_agent_tool", "semanticOperation": "report_progress", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:586", + "id": "skill:skills/paperclip/SKILL.md:603", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L586:update", + "sourceAnchor": "skills/paperclip/SKILL.md#L603:update", "heading": "Update", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:596", + "id": "skill:skills/paperclip/SKILL.md:613", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L596:planning-required-when-planning-requested", + "sourceAnchor": "skills/paperclip/SKILL.md#L613:planning-required-when-planning-requested", "heading": "Planning (Required when planning requested)", "primaryDisposition": "always_agent_tool", "semanticOperation": "write_document", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:629", + "id": "skill:skills/paperclip/SKILL.md:646", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L629:key-endpoints-hot-routes", + "sourceAnchor": "skills/paperclip/SKILL.md#L646:key-endpoints-hot-routes", "heading": "Key Endpoints (Hot Routes)", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:658", + "id": "skill:skills/paperclip/SKILL.md:675", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L658:searching-issues", + "sourceAnchor": "skills/paperclip/SKILL.md#L675:searching-issues", "heading": "Searching Issues", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:668", + "id": "skill:skills/paperclip/SKILL.md:685", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L668:full-reference", + "sourceAnchor": "skills/paperclip/SKILL.md#L685:full-reference", "heading": "Full Reference", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", @@ -677,207 +686,207 @@ "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:787", + "id": "skill:skills/paperclip/references/api-reference.md:807", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L787:option-a-one-call-create-with-workspace", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L807:option-a-one-call-create-with-workspace", "heading": "Option A: One-call create with workspace", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:806", + "id": "skill:skills/paperclip/references/api-reference.md:826", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L806:option-b-two-calls-project-first-then-workspace", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L826:option-b-two-calls-project-first-then-workspace", "heading": "Option B: Two calls (project first, then workspace)", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:835", + "id": "skill:skills/paperclip/references/api-reference.md:855", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L835:governance-and-approvals", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L855:governance-and-approvals", "heading": "Governance and Approvals", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, - { - "id": "skill:skills/paperclip/references/api-reference.md:839", - "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L839:requesting-a-hire-management-only", - "heading": "Requesting a hire (management only)", - "primaryDisposition": "optional_agent_tool", - "semanticOperation": "scoped_discovery", - "expectedMockState": "operation_result" - }, { "id": "skill:skills/paperclip/references/api-reference.md:859", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L859:ceo-strategy-approval", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L859:requesting-a-hire-management-only", + "heading": "Requesting a hire (management only)", + "primaryDisposition": "optional_agent_tool", + "semanticOperation": "scoped_discovery", + "expectedMockState": "operation_result" + }, + { + "id": "skill:skills/paperclip/references/api-reference.md:879", + "kind": "skill_heading", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L879:ceo-strategy-approval", "heading": "CEO strategy approval", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:868", + "id": "skill:skills/paperclip/references/api-reference.md:888", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L868:issue-thread-confirmations", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L888:issue-thread-confirmations", "heading": "Issue-thread confirmations", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:926", + "id": "skill:skills/paperclip/references/api-reference.md:946", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L926:checkbox-confirmations", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L946:checkbox-confirmations", "heading": "Checkbox confirmations", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1041", + "id": "skill:skills/paperclip/references/api-reference.md:1061", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1041:item-verdict-requests", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1061:item-verdict-requests", "heading": "Item verdict requests", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1151", + "id": "skill:skills/paperclip/references/api-reference.md:1171", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1151:checking-approval-status", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1171:checking-approval-status", "heading": "Checking approval status", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1157", + "id": "skill:skills/paperclip/references/api-reference.md:1177", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1157:approval-follow-up-requesting-agent", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1177:approval-follow-up-requesting-agent", "heading": "Approval follow-up (requesting agent)", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1175", + "id": "skill:skills/paperclip/references/api-reference.md:1195", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1175:issue-lifecycle", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1195:issue-lifecycle", "heading": "Issue Lifecycle", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1205", + "id": "skill:skills/paperclip/references/api-reference.md:1225", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1205:error-handling", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1225:error-handling", "heading": "Error Handling", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1219", + "id": "skill:skills/paperclip/references/api-reference.md:1239", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1219:full-api-reference", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1239:full-api-reference", "heading": "Full API Reference", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1221", + "id": "skill:skills/paperclip/references/api-reference.md:1241", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1221:agents", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1241:agents", "heading": "Agents", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1242", + "id": "skill:skills/paperclip/references/api-reference.md:1262", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1242:issues-tasks", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1262:issues-tasks", "heading": "Issues (Tasks)", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1282", + "id": "skill:skills/paperclip/references/api-reference.md:1302", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1282:companies-projects-goals", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1302:companies-projects-goals", "heading": "Companies, Projects, Goals", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1306", + "id": "skill:skills/paperclip/references/api-reference.md:1326", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1306:routines", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1326:routines", "heading": "Routines", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1322", + "id": "skill:skills/paperclip/references/api-reference.md:1342", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1322:approvals-costs-activity-dashboard", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1342:approvals-costs-activity-dashboard", "heading": "Approvals, Costs, Activity, Dashboard", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1344", + "id": "skill:skills/paperclip/references/api-reference.md:1364", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1344:secrets", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1364:secrets", "heading": "Secrets", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1357", + "id": "skill:skills/paperclip/references/api-reference.md:1377", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1357:agent-secret-proposals", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1377:agent-secret-proposals", "heading": "Agent secret proposals", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1410", + "id": "skill:skills/paperclip/references/api-reference.md:1430", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1410:re-bind-an-existing-secret-under-a-new-path-no-secret-id", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1430:re-bind-an-existing-secret-under-a-new-path-no-secret-id", "heading": "Re-bind an existing secret under a new path (no secret ID)", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1457", + "id": "skill:skills/paperclip/references/api-reference.md:1477", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1457:agent-secret-access", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1477:agent-secret-access", "heading": "Agent secret access", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1497", + "id": "skill:skills/paperclip/references/api-reference.md:1517", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1497:common-mistakes", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1517:common-mistakes", "heading": "Common Mistakes", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", diff --git a/packages/paperclip-runner/generated/capability/capability-contract.md b/packages/paperclip-runner/generated/capability/capability-contract.md index 04ebb6dec2..4d6b252ccb 100644 --- a/packages/paperclip-runner/generated/capability/capability-contract.md +++ b/packages/paperclip-runner/generated/capability/capability-contract.md @@ -2,9 +2,9 @@ Generated by `scripts/generate-capability-contract.mjs`; do not edit generated files. -- Skill/reference headings: 154 +- Skill/reference headings: 155 - Legacy MCP tools: 42 - Eval cases: 106 across 16 groups -- Deterministic content SHA-256: `7d89b580b41830403a625dc44644e5faf9b5eb83a27706bc2d624d9da464d331` +- Deterministic content SHA-256: `f83b043e95387a56679241e89e330600e198b890e83e1b21012cf2f4b9210a00` Every row has exactly one primary disposition, a source anchor, a semantic operation, and a mock-state expectation. diff --git a/packages/paperclip-runner/generated/capability/semantic-tool-contracts.json b/packages/paperclip-runner/generated/capability/semantic-tool-contracts.json index c0d2b550b6..30bc9e8336 100644 --- a/packages/paperclip-runner/generated/capability/semantic-tool-contracts.json +++ b/packages/paperclip-runner/generated/capability/semantic-tool-contracts.json @@ -1 +1 @@ -[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active mock task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active mock task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active mock task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create or update an active-task document with optimistic revision safety.","inputSchema":{"additionalProperties":false,"properties":{"baseRevisionId":{"description":"Current revision id, or null when creating.","maxLength":20000,"type":["string","null"]},"body":{"description":"Markdown document body.","maxLength":200000,"minLength":1,"type":"string"},"changeSummary":{"description":"Optional revision summary.","maxLength":20000,"type":["string","null"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"title":{"description":"Document title.","maxLength":300,"minLength":1,"type":"string"}},"required":["idempotencyKey","key","title","body","baseRevisionId"],"type":"object"},"name":"write_document","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"request_human_input","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a typed, durable interaction on the active mock task.","inputSchema":{"additionalProperties":false,"properties":{"continuationPolicy":{"enum":["none","wake_assignee","wake_assignee_on_accept"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"interactionKind":{"enum":["confirmation","checkbox","questions","suggest_tasks","item_verdicts"]},"payload":{"additionalProperties":true,"description":"Kind-specific interaction data. For interactionKind='questions', use exactly {version:1, questions:[{id,prompt,selectionMode:'single'|'multi',required?,options:[{id,label,description?,freeText?}]}]}; option keys are id/label, not value, and question choice cardinality is selectionMode, not type. For confirmation, payload may be {}. Keep all ids stable across retries.","type":"object"},"prompt":{"description":"Question or decision prompt.","maxLength":10000,"minLength":1,"type":"string"},"targetRevisionId":{"description":"Optional bound document revision.","maxLength":20000,"type":["string","null"]},"title":{"description":"Interaction card title.","maxLength":300,"minLength":1,"type":"string"}},"required":["idempotencyKey","interactionKind","title","prompt","continuationPolicy"],"type":"object"},"name":"request_human_input","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"register_deliverable","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Register mock attachment metadata and its artifact work product without credentials or bytes in the tool result.","inputSchema":{"additionalProperties":false,"properties":{"byteSize":{"maximum":100000000,"minimum":0,"type":"integer"},"contentRef":{"description":"Opaque package-local content reference.","maxLength":2000,"minLength":1,"type":"string"},"contentType":{"description":"Media type.","maxLength":200,"minLength":1,"type":"string"},"filename":{"description":"Display filename.","maxLength":500,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"sha256":{"pattern":"^[a-fA-F0-9]{64}$","type":"string"},"title":{"description":"Work-product title.","maxLength":500,"minLength":1,"type":"string"}},"required":["idempotencyKey","filename","contentType","byteSize","sha256","contentRef","title"],"type":"object"},"name":"register_deliverable","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"finish_task","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Finish the active mock task with a durable summary.","inputSchema":{"additionalProperties":false,"properties":{"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"summary":{"description":"Completion summary.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","summary"],"type":"object"},"name":"finish_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"block_task","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Block the active mock task with a durable reason and optional first-class dependencies.","inputSchema":{"additionalProperties":false,"properties":{"blockedByTaskIds":{"description":"Internal mock task ids that block this task.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"reason":{"description":"Block reason.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","reason"],"type":"object"},"name":"block_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"request_review","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Move the active mock task to review with a durable summary.","inputSchema":{"additionalProperties":false,"properties":{"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"summary":{"description":"Review handoff summary.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","summary"],"type":"object"},"name":"request_review","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_agents","requiredClaims":["discovery:agents:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List redacted mock actor profiles.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_agents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_agent","requiredClaims":["discovery:agents:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read one redacted mock actor profile.","inputSchema":{"additionalProperties":false,"properties":{"actorId":{"description":"Mock actor id.","maxLength":200,"minLength":1,"type":"string"}},"required":["actorId"],"type":"object"},"name":"get_agent","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"search_tasks","requiredClaims":["discovery:tasks:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Search mock tasks by text and status within the run company.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"},"query":{"maxLength":500,"type":"string"},"statuses":{"items":{"enum":["backlog","todo","in_progress","in_review","done","blocked","cancelled"]},"maxItems":7,"type":"array","uniqueItems":true}},"required":[],"type":"object"},"name":"search_tasks","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_approvals","requiredClaims":["governance:approvals:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List mock approvals in the run company.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_approvals","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_approval","requiredClaims":["governance:approvals:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read one mock approval without protected data.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Mock approval id.","maxLength":200,"minLength":1,"type":"string"}},"required":["approvalId"],"type":"object"},"name":"get_approval","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_approval_context","requiredClaims":["governance:approvals:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read one approval, its comments, and linked mock tasks.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Mock approval id.","maxLength":200,"minLength":1,"type":"string"}},"required":["approvalId"],"type":"object"},"name":"get_approval_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_workspace_runtime","requiredClaims":["workspace:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read active-task mock workspace services.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_workspace_runtime","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"control_workspace_service","requiredClaims":["workspace:control"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Start, stop, or fault one active-task mock workspace service.","inputSchema":{"additionalProperties":false,"properties":{"action":{"enum":["start","stop","fail"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"serviceId":{"description":"Mock workspace service id.","maxLength":200,"minLength":1,"type":"string"},"url":{"description":"Optional mock service URL.","maxLength":20000,"type":["string","null"]}},"required":["idempotencyKey","serviceId","action"],"type":"object"},"name":"control_workspace_service","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"set_dependencies","requiredClaims":["dependencies:write"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Replace the active task's first-class blocker set.","inputSchema":{"additionalProperties":false,"properties":{"blockedByTaskIds":{"description":"Replacement blocker task ids.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","blockedByTaskIds"],"type":"object"},"name":"set_dependencies","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"create_task","requiredClaims":["delegation:tasks:create"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create one durable standard child under the active task. Use only when a real ownership, parallelism, dependency, review, or lifecycle boundary justifies delegation.","inputSchema":{"additionalProperties":false,"properties":{"assigneeActorId":{"description":"Optional agent assignee. Omit to assign the current agent.","maxLength":20000,"type":["string","null"]},"blockedByTaskIds":{"description":"Initial blocker task ids.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"description":{"description":"Child task description.","maxLength":20000,"type":["string","null"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"priority":{"enum":["critical","high","medium","low"]},"title":{"description":"Child task title.","maxLength":500,"minLength":1,"type":"string"}},"required":["idempotencyKey","title"],"type":"object"},"name":"create_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"},"task":{"additionalProperties":false,"properties":{"assigneeActorId":{"type":["string","null"]},"id":{"minLength":1,"type":"string"},"identifier":{"type":["string","null"]},"parentId":{"minLength":1,"type":"string"},"status":{"minLength":1,"type":"string"}},"required":["id","identifier","parentId","status","assigneeActorId"],"type":"object"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds","task"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"request_approval","requiredClaims":["governance:approvals:request"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a governed mock approval and waiting posture.","inputSchema":{"additionalProperties":false,"properties":{"approvalType":{"description":"Stable approval type.","maxLength":200,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"payload":{"additionalProperties":true,"type":"object"}},"required":["idempotencyKey","approvalType","payload"],"type":"object"},"name":"request_approval","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"decide_approval","requiredClaims":["governance:approvals:decide"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Decide a mock approval as an explicitly authorized approver.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Mock approval id.","maxLength":200,"minLength":1,"type":"string"},"decision":{"enum":["approved","rejected","cancelled"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"note":{"description":"Decision note.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","approvalId","decision","note"],"type":"object"},"name":"decide_approval","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"comment_on_approval","requiredClaims":["governance:approvals:comment"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Add a durable comment to a mock approval.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Mock approval id.","maxLength":200,"minLength":1,"type":"string"},"body":{"description":"Approval comment.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","approvalId","body"],"type":"object"},"name":"comment_on_approval","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"schedule_wake","requiredClaims":["control_plane:wakes"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Schedule a deterministic mock continuation wake.","inputSchema":{"additionalProperties":false,"properties":{"delayTicks":{"maximum":10000,"minimum":1,"type":"integer"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"payload":{"additionalProperties":true,"type":"object"},"reason":{"enum":["manual","issue_commented","interaction_resolved","approval_resolved","blockers_resolved","scheduled_retry","resume"]}},"required":["idempotencyKey","reason","delayTicks"],"type":"object"},"name":"schedule_wake","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"generic_api_request","requiredClaims":["test:generic_api_request"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Test-only escape hatch. Disabled unless the scenario and explicit claim both enable it.","inputSchema":{"additionalProperties":false,"properties":{"body":{"additionalProperties":true,"type":"object"},"method":{"enum":["GET","POST","PATCH"]},"path":{"maxLength":500,"pattern":"^/mock/","type":"string"}},"required":["method","path"],"type":"object"},"name":"generic_api_request","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"search_api","requiredClaims":["api:discover"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Fallback only: discover Paperclip API operations when the available dedicated tools cannot express the task. Prefer dedicated tools for common operations; do not search before using them.","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"maxLength":200,"type":"string"},"limit":{"default":5,"maximum":10,"minimum":1,"type":"integer"},"query":{"maxLength":500,"minLength":1,"type":"string"}},"required":["query"],"type":"object"},"name":"search_api","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"call_api","requiredClaims":["api:call"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Fallback only: call a discovered Paperclip API operation when dedicated tools lack the required operation or parameters. Uses your existing permissions. Prefer dedicated tools; never bypass a denial or runner lifecycle tool.","inputSchema":{"additionalProperties":false,"properties":{"body":{"anyOf":[{"additionalProperties":true,"type":"object"},{"items":{},"type":"array"},{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}],"description":"Request value matching the discovered schema. For JSON object or array requests, pass the object or array directly, never a JSON-encoded string. Strings are for text bodies or endpoints whose schema explicitly accepts a string."},"contentType":{"maxLength":120,"type":"string"},"files":{"items":{"additionalProperties":false,"oneOf":[{"properties":{"artifactId":{}},"required":["artifactId"]},{"properties":{"path":{}},"required":["path"]}],"properties":{"artifactId":{"type":"string"},"field":{"type":"string"},"path":{"description":"File relative to the active issue workspace. Remote files must first be uploaded as an artifact.","type":"string"}},"type":"object"},"maxItems":10,"type":"array"},"operationId":{"description":"Exact operationId returned by search_api, for example GET /api/projects/{id}. Do not guess identifiers.","maxLength":500,"minLength":1,"type":"string"},"pathParams":{"additionalProperties":{"type":"string"},"type":"object"},"query":{"additionalProperties":true,"type":"object"}},"required":["operationId"],"type":"object"},"name":"call_api","outputSchema":{"additionalProperties":true,"type":"object"}}] +[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active mock task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active mock task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active mock task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create or update an active-task document with optimistic revision safety.","inputSchema":{"additionalProperties":false,"properties":{"baseRevisionId":{"description":"Current revision id, or null when creating.","maxLength":20000,"type":["string","null"]},"body":{"description":"Markdown document body.","maxLength":200000,"minLength":1,"type":"string"},"changeSummary":{"description":"Optional revision summary.","maxLength":20000,"type":["string","null"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"title":{"description":"Document title.","maxLength":300,"minLength":1,"type":"string"}},"required":["idempotencyKey","key","title","body","baseRevisionId"],"type":"object"},"name":"write_document","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"request_human_input","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a typed, durable interaction on the active mock task.","inputSchema":{"additionalProperties":false,"properties":{"continuationPolicy":{"enum":["none","wake_assignee","wake_assignee_on_accept"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"interactionKind":{"enum":["confirmation","checkbox","questions","suggest_tasks","item_verdicts"]},"payload":{"additionalProperties":true,"description":"Kind-specific interaction data. For interactionKind='questions', use exactly {version:1, questions:[{id,prompt,selectionMode:'single'|'multi',required?,options:[{id,label,description?,freeText?}]}]}; option keys are id/label, not value, and question choice cardinality is selectionMode, not type. For confirmation, payload may be {}. Keep all ids stable across retries.","type":"object"},"prompt":{"description":"Question or decision prompt.","maxLength":10000,"minLength":1,"type":"string"},"targetRevisionId":{"description":"Optional bound document revision.","maxLength":20000,"type":["string","null"]},"title":{"description":"Interaction card title.","maxLength":300,"minLength":1,"type":"string"}},"required":["idempotencyKey","interactionKind","title","prompt","continuationPolicy"],"type":"object"},"name":"request_human_input","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"register_deliverable","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Register mock attachment metadata and its artifact work product without credentials or bytes in the tool result.","inputSchema":{"additionalProperties":false,"properties":{"byteSize":{"maximum":100000000,"minimum":0,"type":"integer"},"contentRef":{"description":"Opaque package-local content reference.","maxLength":2000,"minLength":1,"type":"string"},"contentType":{"description":"Media type.","maxLength":200,"minLength":1,"type":"string"},"filename":{"description":"Display filename.","maxLength":500,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"sha256":{"pattern":"^[a-fA-F0-9]{64}$","type":"string"},"title":{"description":"Work-product title.","maxLength":500,"minLength":1,"type":"string"}},"required":["idempotencyKey","filename","contentType","byteSize","sha256","contentRef","title"],"type":"object"},"name":"register_deliverable","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"finish_task","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Finish the active mock task with a durable summary.","inputSchema":{"additionalProperties":false,"properties":{"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"summary":{"description":"Completion summary.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","summary"],"type":"object"},"name":"finish_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"block_task","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Block the active mock task with a durable reason and optional first-class dependencies.","inputSchema":{"additionalProperties":false,"properties":{"blockedByTaskIds":{"description":"Internal mock task ids that block this task.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"reason":{"description":"Block reason.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","reason"],"type":"object"},"name":"block_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"request_review","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Move the active mock task to review with a durable summary.","inputSchema":{"additionalProperties":false,"properties":{"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"summary":{"description":"Review handoff summary.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","summary"],"type":"object"},"name":"request_review","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_agents","requiredClaims":["discovery:agents:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List redacted mock actor profiles.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_agents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_agent","requiredClaims":["discovery:agents:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read one redacted mock actor profile.","inputSchema":{"additionalProperties":false,"properties":{"actorId":{"description":"Mock actor id.","maxLength":200,"minLength":1,"type":"string"}},"required":["actorId"],"type":"object"},"name":"get_agent","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"search_tasks","requiredClaims":["discovery:tasks:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Search mock tasks by text and status within the run company.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"},"query":{"maxLength":500,"type":"string"},"statuses":{"items":{"enum":["backlog","todo","in_progress","in_review","done","blocked","cancelled"]},"maxItems":7,"type":"array","uniqueItems":true}},"required":[],"type":"object"},"name":"search_tasks","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_approvals","requiredClaims":["governance:approvals:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List mock approvals in the run company.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_approvals","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_approval","requiredClaims":["governance:approvals:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read one mock approval without protected data.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Mock approval id.","maxLength":200,"minLength":1,"type":"string"}},"required":["approvalId"],"type":"object"},"name":"get_approval","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_approval_context","requiredClaims":["governance:approvals:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read one approval, its comments, and linked mock tasks.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Mock approval id.","maxLength":200,"minLength":1,"type":"string"}},"required":["approvalId"],"type":"object"},"name":"get_approval_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_workspace_runtime","requiredClaims":["workspace:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read active-task mock workspace services.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_workspace_runtime","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"control_workspace_service","requiredClaims":["workspace:control"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Start, stop, or fault one active-task mock workspace service.","inputSchema":{"additionalProperties":false,"properties":{"action":{"enum":["start","stop","fail"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"serviceId":{"description":"Mock workspace service id.","maxLength":200,"minLength":1,"type":"string"},"url":{"description":"Optional mock service URL.","maxLength":20000,"type":["string","null"]}},"required":["idempotencyKey","serviceId","action"],"type":"object"},"name":"control_workspace_service","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"set_dependencies","requiredClaims":["dependencies:write"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Replace the active task's first-class blocker set.","inputSchema":{"additionalProperties":false,"properties":{"blockedByTaskIds":{"description":"Replacement blocker task ids.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","blockedByTaskIds"],"type":"object"},"name":"set_dependencies","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"create_task","requiredClaims":["delegation:tasks:create"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a project task from a conversation, or a child from an ordinary task. Persist initialPlan before execution.","inputSchema":{"additionalProperties":false,"properties":{"assigneeActorId":{"description":"Optional agent assignee. Omit to assign the current agent.","maxLength":20000,"type":["string","null"]},"blockedByTaskIds":{"description":"Initial blocker task ids.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"description":{"description":"Child task description.","maxLength":20000,"type":["string","null"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"initialPlan":{"description":"Relevant markdown plan saved on the new task before execution starts.","maxLength":200000,"type":["string","null"]},"priority":{"enum":["critical","high","medium","low"]},"projectId":{"description":"Project ID for the task.","type":["string","null"]},"title":{"description":"Child task title.","maxLength":500,"minLength":1,"type":"string"}},"required":["idempotencyKey","title"],"type":"object"},"name":"create_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"},"task":{"additionalProperties":false,"properties":{"assigneeActorId":{"type":["string","null"]},"id":{"minLength":1,"type":"string"},"identifier":{"type":["string","null"]},"parentId":{"minLength":1,"type":["string","null"]},"projectId":{"minLength":1,"type":["string","null"]},"status":{"minLength":1,"type":"string"}},"required":["id","identifier","parentId","status","assigneeActorId"],"type":"object"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds","task"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"request_approval","requiredClaims":["governance:approvals:request"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a governed mock approval and waiting posture.","inputSchema":{"additionalProperties":false,"properties":{"approvalType":{"description":"Stable approval type.","maxLength":200,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"payload":{"additionalProperties":true,"type":"object"}},"required":["idempotencyKey","approvalType","payload"],"type":"object"},"name":"request_approval","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"decide_approval","requiredClaims":["governance:approvals:decide"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Decide a mock approval as an explicitly authorized approver.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Mock approval id.","maxLength":200,"minLength":1,"type":"string"},"decision":{"enum":["approved","rejected","cancelled"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"note":{"description":"Decision note.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","approvalId","decision","note"],"type":"object"},"name":"decide_approval","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"comment_on_approval","requiredClaims":["governance:approvals:comment"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Add a durable comment to a mock approval.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Mock approval id.","maxLength":200,"minLength":1,"type":"string"},"body":{"description":"Approval comment.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","approvalId","body"],"type":"object"},"name":"comment_on_approval","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"schedule_wake","requiredClaims":["control_plane:wakes"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Schedule a deterministic mock continuation wake.","inputSchema":{"additionalProperties":false,"properties":{"delayTicks":{"maximum":10000,"minimum":1,"type":"integer"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"payload":{"additionalProperties":true,"type":"object"},"reason":{"enum":["manual","issue_commented","interaction_resolved","approval_resolved","blockers_resolved","scheduled_retry","resume"]}},"required":["idempotencyKey","reason","delayTicks"],"type":"object"},"name":"schedule_wake","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"generic_api_request","requiredClaims":["test:generic_api_request"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Test-only escape hatch. Disabled unless the scenario and explicit claim both enable it.","inputSchema":{"additionalProperties":false,"properties":{"body":{"additionalProperties":true,"type":"object"},"method":{"enum":["GET","POST","PATCH"]},"path":{"maxLength":500,"pattern":"^/mock/","type":"string"}},"required":["method","path"],"type":"object"},"name":"generic_api_request","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"search_api","requiredClaims":["api:discover"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Fallback only: discover Paperclip API operations when the available dedicated tools cannot express the task. Prefer dedicated tools for common operations; do not search before using them.","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"maxLength":200,"type":"string"},"limit":{"default":5,"maximum":10,"minimum":1,"type":"integer"},"query":{"maxLength":500,"minLength":1,"type":"string"}},"required":["query"],"type":"object"},"name":"search_api","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"call_api","requiredClaims":["api:call"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Fallback only: call a discovered Paperclip API operation when dedicated tools lack the required operation or parameters. Uses your existing permissions. Prefer dedicated tools; never bypass a denial or runner lifecycle tool.","inputSchema":{"additionalProperties":false,"properties":{"body":{"anyOf":[{"additionalProperties":true,"type":"object"},{"items":{},"type":"array"},{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}],"description":"Request value matching the discovered schema. For JSON object or array requests, pass the object or array directly, never a JSON-encoded string. Strings are for text bodies or endpoints whose schema explicitly accepts a string."},"contentType":{"maxLength":120,"type":"string"},"files":{"items":{"additionalProperties":false,"oneOf":[{"properties":{"artifactId":{}},"required":["artifactId"]},{"properties":{"path":{}},"required":["path"]}],"properties":{"artifactId":{"type":"string"},"field":{"type":"string"},"path":{"description":"File relative to the active issue workspace. Remote files must first be uploaded as an artifact.","type":"string"}},"type":"object"},"maxItems":10,"type":"array"},"operationId":{"description":"Exact operationId returned by search_api, for example GET /api/projects/{id}. Do not guess identifiers.","maxLength":500,"minLength":1,"type":"string"},"pathParams":{"additionalProperties":{"type":"string"},"type":"object"},"query":{"additionalProperties":true,"type":"object"}},"required":["operationId"],"type":"object"},"name":"call_api","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"create_project","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a project after considering existing projects and available repositories. repositoryIds and repositoryUrls accept multiple existing repositories. Use HTTPS GitHub repositoryUrls when an accessible repo is not in the catalog; this registers project repositories, not remote GitHub repositories. Non-code projects may omit repositories. Cannot combine repositoryIds/repositoryUrls with workspace. Reuse the idempotency key on retries.","inputSchema":{"additionalProperties":false,"properties":{"archivedAt":{"description":"Archive timestamp.","maxLength":20000,"type":["string","null"]},"color":{"description":"Project color.","maxLength":20000,"type":["string","null"]},"description":{"description":"Project outcome and context.","maxLength":20000,"type":["string","null"]},"env":{"additionalProperties":true,"type":"object"},"executionWorkspacePolicy":{"additionalProperties":true,"type":"object"},"goalId":{"description":"Goal ID.","maxLength":20000,"type":["string","null"]},"goalIds":{"description":"Goal IDs.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"icon":{"description":"Project icon.","maxLength":20000,"type":["string","null"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"leadAgentId":{"description":"Lead agent ID.","maxLength":20000,"type":["string","null"]},"name":{"description":"Project name.","maxLength":500,"minLength":1,"type":"string"},"repositoryIds":{"description":"Authorized repository IDs from list_project_repositories; may contain multiple repositories.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"repositoryUrls":{"description":"Existing HTTPS GitHub repository URLs, including repos absent from the catalog.","items":{"format":"uri","type":"string"},"maxItems":100,"type":"array"},"status":{"enum":["backlog","planned","in_progress","completed","cancelled"]},"targetDate":{"description":"Target date.","maxLength":20000,"type":["string","null"]},"workspace":{"additionalProperties":true,"type":"object"}},"required":["idempotencyKey","name"],"type":"object"},"name":"create_project","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_project_repositories","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List authorized repositories with stable IDs and names. Consider appropriate repositories before creating a project; never invent IDs.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_project_repositories","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_projects","requiredClaims":["discovery:projects:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Inspect available company projects before selecting a project for new work.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_projects","outputSchema":{"additionalProperties":true,"type":"object"}}] diff --git a/packages/paperclip-runner/generated/semantic-action-catalog.json b/packages/paperclip-runner/generated/semantic-action-catalog.json index 2d9126bcad..946b38de40 100644 --- a/packages/paperclip-runner/generated/semantic-action-catalog.json +++ b/packages/paperclip-runner/generated/semantic-action-catalog.json @@ -1556,9 +1556,210 @@ { "allowedModes": [ "standard", + "ask", + "planning", "skill_test" ], - "description": "Create one child task under the active task.", + "description": "Inspect available company projects before selecting a project for new work.", + "effect": "read", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "required": [], + "type": "object" + }, + "operationId": "list_projects", + "outputSchema": { + "additionalProperties": true, + "type": "object" + }, + "placement": "optional", + "requiredClaims": [ + "discovery:projects:read" + ], + "schema": "paperclip.semantic-action.v1", + "title": "List projects", + "version": 1 + }, + { + "allowedModes": [ + "standard", + "ask", + "planning", + "skill_test" + ], + "description": "List authorized repositories with stable IDs and names. Consider appropriate repositories before creating a project; never invent IDs.", + "effect": "read", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "required": [], + "type": "object" + }, + "operationId": "list_project_repositories", + "outputSchema": { + "additionalProperties": true, + "type": "object" + }, + "placement": "optional", + "requiredClaims": [], + "schema": "paperclip.semantic-action.v1", + "title": "List available repositories", + "version": 1 + }, + { + "allowedModes": [ + "standard", + "skill_test" + ], + "description": "Create a project after considering existing projects and available repositories. repositoryIds and repositoryUrls accept multiple existing repositories. Use HTTPS GitHub repositoryUrls when an accessible repo is not in the catalog; this registers project repositories, not remote GitHub repositories. Non-code projects may omit repositories. Cannot combine repositoryIds/repositoryUrls with workspace. Reuse the idempotency key on retries.", + "effect": "write", + "inputSchema": { + "additionalProperties": false, + "properties": { + "archivedAt": { + "description": "Archive timestamp.", + "maxLength": 20000, + "type": [ + "string", + "null" + ] + }, + "color": { + "description": "Project color.", + "maxLength": 20000, + "type": [ + "string", + "null" + ] + }, + "description": { + "description": "Project outcome and context.", + "maxLength": 20000, + "type": [ + "string", + "null" + ] + }, + "env": { + "additionalProperties": true, + "type": "object" + }, + "executionWorkspacePolicy": { + "additionalProperties": true, + "type": "object" + }, + "goalId": { + "description": "Goal ID.", + "maxLength": 20000, + "type": [ + "string", + "null" + ] + }, + "goalIds": { + "description": "Goal IDs.", + "items": { + "minLength": 1, + "type": "string" + }, + "maxItems": 200, + "type": "array", + "uniqueItems": true + }, + "icon": { + "description": "Project icon.", + "maxLength": 20000, + "type": [ + "string", + "null" + ] + }, + "idempotencyKey": { + "description": "Caller-stable retry key.", + "maxLength": 240, + "minLength": 1, + "type": "string" + }, + "leadAgentId": { + "description": "Lead agent ID.", + "maxLength": 20000, + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "Project name.", + "maxLength": 500, + "minLength": 1, + "type": "string" + }, + "repositoryIds": { + "description": "Authorized repository IDs from list_project_repositories; may contain multiple repositories.", + "items": { + "minLength": 1, + "type": "string" + }, + "maxItems": 200, + "type": "array", + "uniqueItems": true + }, + "repositoryUrls": { + "description": "Existing HTTPS GitHub repository URLs, including repos absent from the catalog.", + "items": { + "minLength": 1, + "type": "string" + }, + "maxItems": 200, + "type": "array", + "uniqueItems": true + }, + "status": { + "enum": [ + "backlog", + "planned", + "in_progress", + "completed", + "cancelled" + ] + }, + "targetDate": { + "description": "Target date.", + "maxLength": 20000, + "type": [ + "string", + "null" + ] + }, + "workspace": { + "additionalProperties": true, + "type": "object" + } + }, + "required": [ + "idempotencyKey", + "name" + ], + "type": "object" + }, + "operationId": "create_project", + "outputSchema": { + "additionalProperties": true, + "type": "object" + }, + "placement": "optional", + "requiredClaims": [], + "schema": "paperclip.semantic-action.v1", + "title": "Create project", + "version": 1 + }, + { + "allowedModes": [ + "standard", + "skill_test" + ], + "description": "Create an assigned task. In a conversation, create a project task with no parent; otherwise create a child of the active task. Include initialPlan to persist its plan before execution.", "effect": "write", "inputSchema": { "additionalProperties": false, @@ -1595,6 +1796,14 @@ "minLength": 1, "type": "string" }, + "initialPlan": { + "description": "Relevant markdown plan to persist on the new task before it starts.", + "maxLength": 20000, + "type": [ + "string", + "null" + ] + }, "priority": { "enum": [ "critical", @@ -1603,8 +1812,16 @@ "low" ] }, + "projectId": { + "description": "Project identifier for the new task.", + "maxLength": 20000, + "type": [ + "string", + "null" + ] + }, "title": { - "description": "Child task title.", + "description": "Task title.", "maxLength": 500, "minLength": 1, "type": "string" @@ -1671,7 +1888,7 @@ "delegation:tasks:create" ], "schema": "paperclip.semantic-action.v1", - "title": "Create child task", + "title": "Create task", "version": 1 }, { diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_event_payload.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_event_payload.rs index bdd651f7f3..7ab36472b1 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_event_payload.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_event_payload.rs @@ -4,7 +4,7 @@ use serde_json::Value; use crate::acpx_event_scope::AcpxEventScope; use crate::acpx_sidecar_transport::AcpxSidecarEvent; -use crate::durable::{redact_text, sanitize_value}; +use crate::durable::{redact_text, sanitize_semantic_tool_input, sanitize_value}; use crate::generated_acpx_sidecar_contract::{ classify_generated_acpx_tool_operation, GeneratedAcpxSidecarEventType, }; @@ -133,11 +133,19 @@ pub fn decode_acpx_event( "ACPX tool call input must be an object", )); } + let operation_id = required_id(&event.payload, "operationId", "tool operation")?; + // This input is dispatched as a mutation, not merely displayed in + // the event feed. Use the same declared-prose policy as native + // semantic_tool.input before any generic diagnostic scrub can + // irreversibly change the task's requirements. + let safe_input = sanitize_semantic_tool_input(&operation_id, &input) + .map_err(|error| LocalRunnerError::invalid(error.to_string()))?; Ok(AcpxEventPayload::ToolCalled { call_id: required_id(&event.payload, "callId", "tool call")?, - operation_id: required_id(&event.payload, "operationId", "tool operation")?, + operation_id, + // Keep the original digest for the sidecar's result binding. input_digest: semantic_value_digest(&input), - input: sanitize_value(&input), + input: safe_input, }) } GeneratedAcpxSidecarEventType::RuntimeTurnTerminal => { diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs index 3b0b63cbed..d4032505c9 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs @@ -632,7 +632,9 @@ impl AcpxCommandExecutor { event_type: "run.terminal".to_owned(), priority: EventPriority::P0, payload: json!({ + "schema": "paperclip.prp.terminal.v1", "status": "failed", + "turnTerminalState": "failed", "runTerminalState": "failed", "reportedWorkDisposition": "unknown", "provider": "acpx", @@ -1393,11 +1395,11 @@ impl AcpxCommandExecutor { { continue; } - let status = match event_type.as_str() { - "turn.completed" => "succeeded", - "turn.cancelled" => "cancelled", - "turn.interrupted" => "interrupted", - _ => "failed", + let (turn_terminal_state, status) = match event_type.as_str() { + "turn.completed" => ("completed", "succeeded"), + "turn.cancelled" => ("cancelled", "cancelled"), + "turn.interrupted" => ("interrupted", "cancelled"), + _ => ("failed", "failed"), }; let disposition = goal_terminal_disposition( state @@ -1415,7 +1417,9 @@ impl AcpxCommandExecutor { event_type: "run.terminal".to_owned(), priority: EventPriority::P0, payload: json!({ + "schema": "paperclip.prp.terminal.v1", "status": status, + "turnTerminalState": turn_terminal_state, "runTerminalState": status, "reportedWorkDisposition": disposition, "provider": "acpx", @@ -1527,6 +1531,13 @@ impl CommandExecutor for AcpxCommandExecutor { return Ok(Vec::new()); } self.poll_provider()?; + self.retained_events() + } + + fn retained_events(&mut self) -> Result, DurableRunnerError> { + // Explicit drain runs while control traffic suppresses provider polling. + // Expose the already-retained suffix so runnerd can commit and ACK it + // before suspension, without restoring or advancing the provider. Ok(self .state .as_ref() @@ -1805,6 +1816,57 @@ mod tests { }) } + #[test] + fn retained_events_exposes_terminal_suffix_without_restoring_provider() { + let directory = temporary_directory("retained-terminal-suffix"); + let config = test_config(&directory, None); + let mut executor = AcpxCommandExecutor::with_runner_config(&directory, &config); + // Invalid on-disk state would fail restoration. Retained-only reads + // must neither restore a provider nor inspect a different state owner. + fs::write(executor.state_path(), b"not provider state").unwrap(); + assert!(executor.retained_events().unwrap().is_empty()); + + let operations = Vec::new(); + let tool_set = AuthorizedToolSet { + schema: TOOL_SET_SCHEMA.to_owned(), + schema_version: 1, + catalog_digest: authorized_tool_catalog_digest(&operations).unwrap(), + operations, + }; + let mut state = AcpxDurableState::new( + serde_json::from_value(descriptor("claude")).unwrap(), + tool_set, + "retained-only-test".to_owned(), + ); + state.lifecycle = "session_open".to_owned(); + for event_type in ["turn.completed", "run.usage", "run.completed"] { + state + .push(NormalizedProviderEvent { + event_type: event_type.to_owned(), + priority: EventPriority::P0, + payload: json!({}), + }) + .unwrap(); + } + executor.state = Some(state); + let suffix = executor.retained_events().unwrap(); + assert_eq!( + suffix + .iter() + .map(|event| event.event_type.as_str()) + .collect::>(), + vec!["turn.completed", "run.usage", "run.completed"], + ); + // Reading is not acknowledgement: a retry sees the exact same FIFO. + assert_eq!(executor.retained_events().unwrap(), suffix); + assert!(executor.session.is_none()); + assert_eq!( + fs::read(executor.state_path()).unwrap(), + b"not provider state" + ); + fs::remove_dir_all(directory).unwrap(); + } + #[test] fn admits_only_exact_qualified_claude_and_codex_descriptors() { for agent in ["claude", "codex"] { @@ -2169,6 +2231,8 @@ mod tests { assert_eq!(events[0].event_type, "turn.failed"); assert_eq!(events[0].payload["providerShutdownFailed"], true); assert_eq!(events[1].event_type, "run.terminal"); + assert_eq!(events[1].payload["schema"], "paperclip.prp.terminal.v1"); + assert_eq!(events[1].payload["turnTerminalState"], "failed"); let cleanup_error = recovered .shutdown() .expect_err("cleanup must not succeed while the original lifetime remains active"); diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs index 55f7b25335..6dc1e606cd 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs @@ -1,4 +1,4 @@ -use std::collections::VecDeque; +use std::collections::{BTreeSet, VecDeque}; use std::path::PathBuf; use std::sync::mpsc::RecvTimeoutError; use std::time::{Duration, Instant}; @@ -82,6 +82,7 @@ pub struct AcpxSidecarTransport { last_event_sequence: u64, buffered_events: VecDeque, stderr_tail: BoundedLogBuffer, + stderr_categories: BTreeSet<&'static str>, poisoned: bool, } @@ -154,6 +155,7 @@ impl AcpxSidecarTransport { last_event_sequence: 0, buffered_events: VecDeque::new(), stderr_tail: BoundedLogBuffer::new(32, 8 * 1024), + stderr_categories: BTreeSet::new(), poisoned: false, }) } @@ -323,7 +325,7 @@ impl AcpxSidecarTransport { match self.process.recv_timeout(remaining) { Ok(ProcessOutput::Stdout(line)) => return Ok(Some(line)), Ok(ProcessOutput::Stderr(line)) => { - self.stderr_tail.push(redact_diagnostic(&line)); + self.record_stderr(&line); } Ok(ProcessOutput::StdoutError(message)) => { return Err(LocalRunnerError::invalid(format!( @@ -412,7 +414,7 @@ impl AcpxSidecarTransport { }; match output { Some(ProcessOutput::Stderr(line)) => { - self.stderr_tail.push(redact_diagnostic(&line)); + self.record_stderr(&line); } Some(ProcessOutput::StderrClosed) | None => break, Some(ProcessOutput::Stdout(_)) @@ -424,13 +426,33 @@ impl AcpxSidecarTransport { fn diagnostic_suffix(&self) -> String { let diagnostics = self.stderr_tail.snapshot().lines.join("\n"); - if diagnostics.is_empty() { + let categories = if self.stderr_categories.is_empty() { String::new() } else { - format!(" stderrTail={diagnostics:?}") + format!( + " stderrCategories={}", + self.stderr_categories + .iter() + .copied() + .collect::>() + .join(",") + ) + }; + if diagnostics.is_empty() { + categories + } else { + format!("{categories} stderrTail={diagnostics:?}") } } + fn record_stderr(&mut self, line: &str) { + // Only fixed categories cross this boundary. Raw errors, stack paths, + // identifiers, and credential-bearing strings remain fully redacted. + self.stderr_categories + .extend(stderr_diagnostic_categories(line)); + self.stderr_tail.push(redact_diagnostic(line)); + } + fn poison(&mut self) { if self.poisoned { return; @@ -598,6 +620,53 @@ fn redact_diagnostic(value: &str) -> String { } } +fn stderr_diagnostic_categories(value: &str) -> BTreeSet<&'static str> { + const CATEGORIES: &[(&str, &str)] = &[ + ("TypeError", "javascript_type_error"), + ("ReferenceError", "javascript_reference_error"), + ("SyntaxError", "javascript_syntax_error"), + ("RangeError", "javascript_range_error"), + ("AssertionError", "javascript_assertion_error"), + ("UnhandledPromiseRejection", "unhandled_rejection"), + ("ERR_UNHANDLED_REJECTION", "unhandled_rejection"), + ("ERR_UNHANDLED_ERROR", "unhandled_event_error"), + ("ERR_INVALID_ARG_TYPE", "invalid_argument_type"), + ("ERR_INVALID_ARG_VALUE", "invalid_argument_value"), + ("ERR_STREAM_WRITE_AFTER_END", "stream_write_after_end"), + ("ERR_STREAM_DESTROYED", "stream_destroyed"), + ("ERR_IPC_CHANNEL_CLOSED", "ipc_channel_closed"), + ("ERR_SOCKET_CLOSED", "socket_closed"), + ("ERR_MODULE_NOT_FOUND", "module_not_found"), + ("MODULE_NOT_FOUND", "module_not_found"), + ("EPIPE", "broken_pipe"), + ("ECONNRESET", "connection_reset"), + ("EADDRINUSE", "address_in_use"), + ("ENOENT", "file_not_found"), + ("EACCES", "permission_denied"), + ("EPERM", "permission_denied"), + ( + "ACPX_PERSISTED_SESSION_IDENTITY_MISMATCH", + "persisted_session_identity_mismatch", + ), + ("SESSION_RESUME_REQUIRED", "session_resume_required"), + ]; + let mut categories: BTreeSet<&'static str> = value + .split(|character: char| !character.is_ascii_alphanumeric() && character != '_') + .filter_map(|token| { + CATEGORIES + .iter() + .find_map(|(known, category)| (token == *known).then_some(*category)) + }) + .collect(); + if value.contains("triggerUncaughtException") && value.contains("fromPromise") { + categories.insert("unhandled_rejection"); + } + if value.contains("ACPX provider spawned after ownership admission was sealed") { + categories.insert("provider_spawn_after_ownership_seal"); + } + categories +} + fn response_error_classification(error: &ResponseError) -> &'static str { match error.code.as_str() { "ACP_MODEL_UNSUPPORTED" => return "requested_model_unsupported", @@ -642,6 +711,17 @@ fn response_error_classification(error: &ResponseError) -> &'static str { _ => {} } match error.message.as_str() { + "ACPX provider spawned after ownership admission was sealed" => { + "provider_spawn_after_ownership_seal" + } + "ACPX recovery identity conflicts with the immutable session configuration" => { + "recovery_configuration_mismatch" + } + "ACPX recovery identity does not match the persisted runtime record" => { + "recovery_identity_mismatch" + } + "ACPX provider lifetime lease is unavailable" => "provider_lifetime_unavailable", + "Managed Codex credential home already has an active lease" => "provider_lifetime_owned", "ACPX session handshake exceeded its admission deadline" => "session_handshake_timeout", "ACPX provider lifetime guardian exited before ownership transfer" => { "provider_guardian_exit" @@ -734,6 +814,39 @@ mod tests { )), "session_handshake_timeout" ); + for (message, classification) in [ + ( + "ACPX recovery identity conflicts with the immutable session configuration", + "recovery_configuration_mismatch", + ), + ( + "ACPX recovery identity does not match the persisted runtime record", + "recovery_identity_mismatch", + ), + ( + "ACPX provider lifetime lease is unavailable", + "provider_lifetime_unavailable", + ), + ] { + assert_eq!( + response_error_classification(&error("acpx_sidecar_command_failed", message)), + classification + ); + assert_eq!( + response_error_classification(&error( + "acpx_sidecar_command_failed", + &format!("{message}: private-provider-detail") + )), + "unclassified" + ); + } + assert_eq!( + response_error_classification(&error( + "acpx_sidecar_command_failed", + "Managed Codex credential home already has an active lease" + )), + "provider_lifetime_owned" + ); let admission_failures = [ ( "ACPX_RUNTIME_ADMISSION_VERIFICATION_TIMEOUT", diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/claude_managed_provider.rs b/packages/paperclip-runner/runner/crates/runner-core/src/claude_managed_provider.rs index 8d68b2212b..da44421ad4 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/claude_managed_provider.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/claude_managed_provider.rs @@ -2670,6 +2670,9 @@ mod tests { } fn read_http_request(socket: &mut TcpStream) -> Result { + // Darwin inherits the listener's nonblocking flag on accept. Wait for + // request bytes within the timeout instead of dropping an early accept. + socket.set_nonblocking(false)?; socket.set_read_timeout(Some(Duration::from_secs(2)))?; let mut bytes = Vec::new(); let mut buffer = [0_u8; 4096]; @@ -2722,6 +2725,34 @@ mod tests { }) } + #[test] + fn fake_service_waits_for_request_bytes_on_an_accepted_nonblocking_socket() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap(); + let (mut accepted, _) = listener.accept().unwrap(); + // Reproduce Darwin's inherited listener flag on every test platform. + accepted.set_nonblocking(true).unwrap(); + let (result_tx, result_rx) = mpsc::channel(); + let reader = thread::spawn(move || { + result_tx.send(read_http_request(&mut accepted)).unwrap(); + }); + assert!(matches!( + result_rx.recv_timeout(Duration::from_millis(25)), + Err(mpsc::RecvTimeoutError::Timeout) + )); + client + .write_all(b"POST /delayed HTTP/1.1\r\nContent-Length: 2\r\n\r\n{}") + .unwrap(); + let request = result_rx + .recv_timeout(Duration::from_secs(3)) + .unwrap() + .unwrap(); + reader.join().unwrap(); + assert_eq!(request.method, "POST"); + assert_eq!(request.path, "/delayed"); + assert_eq!(request.body, "{}"); + } + fn send_json_response(socket: &mut TcpStream, status: &str, value: &Value) { let body = serde_json::to_string(value).unwrap(); let _ = write!(socket, "HTTP/1.1 {status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()); diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/durable/state.rs b/packages/paperclip-runner/runner/crates/runner-core/src/durable/state.rs index 27801cf59e..e137a20af7 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/durable/state.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/durable/state.rs @@ -1597,6 +1597,28 @@ pub(crate) fn sanitize_semantic_tool_input( input: &Value, ) -> Result { let mut sanitized = sanitize_value(input); + // Mutation prose is the user's intended work, not a diagnostic. Preserve + // ordinary references to a token in these declared text fields; credential + // syntax and high-confidence secret values are still scrubbed. All other + // fields and operations retain the strict diagnostic policy. + let prose_fields: &[&str] = match operation_id { + "create_task" => &["title", "description", "initialPlan"], + "create_project" => &["name", "description"], + "write_document" => &["title", "body", "changeSummary"], + _ => &[], + }; + if let Some(sanitized_input) = sanitized.as_object_mut() { + for field in prose_fields { + if let Some(text) = input.get(*field).and_then(Value::as_str) { + sanitized_input.insert( + (*field).to_owned(), + // The tool/API schema bounds business content. A diagnostic + // preview limit must never truncate a plan or document. + Value::String(redact_sensitive_text_values_with_context(text, true)), + ); + } + } + } if !matches!(operation_id, "paperclip_finish" | "paperclip_block") { return Ok(sanitized); } @@ -1720,6 +1742,10 @@ pub(crate) fn redact_text(input: &str) -> String { } fn redact_sensitive_text_values(input: &str) -> String { + redact_sensitive_text_values_with_context(input, false) +} + +fn redact_sensitive_text_values_with_context(input: &str, semantic_prose: bool) -> String { let normalized = input.to_ascii_lowercase(); let bytes = normalized.as_bytes(); let mut ranges: Vec<(usize, usize)> = Vec::new(); @@ -1893,6 +1919,7 @@ fn redact_sensitive_text_values(input: &str) -> String { ("ghu_", 20), ("ghs_", 20), ("ghr_", 20), + ("github_pat_", 20), ] { for (start, _) in normalized.match_indices(prefix) { if start > 0 && is_name_byte(bytes[start - 1]) { @@ -2088,6 +2115,35 @@ fn redact_sensitive_text_values(input: &str) -> String { .any(|delimiter| before.ends_with(delimiter)) }; let has_hyphenated_count_lead = token_phrase_has_lead("one-"); + // A bare token reference in declared mutation prose can be an output + // requirement. Auth/access/session context, explicit assignment, + // quoted credentials and CLI/compound names remain credential pairs. + // Known key/JWT/Bearer values are independently scrubbed above. + let is_semantic_token_reference = semantic_prose + && key == "token" + && !key_is_compound + && whitespace_start == start + key.len() + && separator > whitespace_start + && !has_assignment_separator + && bytes[whitespace_start..separator] + .iter() + .all(|value| matches!(value, b' ' | b'\t')) + && quoted_value_start(separator).1.is_none() + && ![ + "auth ", + "authentication ", + "authorization ", + "access ", + "refresh ", + "session ", + "api ", + "security ", + "secret ", + "credential ", + "bearer ", + ] + .iter() + .any(|lead| token_phrase_has_lead(lead)); let is_benign_token_noun_phrase = key == "token" && (!key_is_compound || has_hyphenated_count_lead) && whitespace_start == start + key.len() @@ -2150,7 +2206,8 @@ fn redact_sensitive_text_values(input: &str) -> String { || (token_phrase_has_tail("can equal") && token_phrase_has_lead("one "))); let has_whitespace_separator = separator > whitespace_start && (key != "authorization" || key_is_compound || has_authorization_scheme) - && !is_benign_token_noun_phrase; + && !is_benign_token_noun_phrase + && !is_semantic_token_reference; if !has_assignment_separator && !has_whitespace_separator { continue; } @@ -3159,6 +3216,148 @@ mod tests { assert_eq!(sanitized["accessToken"], json!("[REDACTED]")); } + #[test] + fn semantic_handoff_preserves_acceptance_identifiers_in_declared_prose() { + let description = + "The document body must contain the token CHAT250ed7e4dc071. No code changes needed."; + let plan = format!( + "## Plan\n{}\n- The token CHAT250ed7e4dc071 included somewhere in the body.\n- Save the output document.", + "Relevant task context. ".repeat(300), + ); + assert!(plan.len() > 4096); + let input = json!({ + "title": "Write project description", + "description": description, + "initialPlan": plan, + "idempotencyKey": "write-description-1", + }); + assert_eq!( + sanitize_semantic_tool_input("create_task", &input).unwrap(), + input + ); + for text in [ + description, + plan.as_str(), + "Must include the literal token `CHAT66e7813a4f9d1` somewhere in the text.", + "Include the exact token ACCEPTANCE-42 in the final output.", + ] { + assert_eq!( + sanitize_semantic_tool_input("write_document", &json!({"body": text})).unwrap(), + json!({"body": text}) + ); + assert_ne!( + redact_text(text), + text, + "diagnostics keep their strict policy" + ); + } + let config = config(PathBuf::from("unused")); + let mut state = DurableState::new(&config); + state + .enqueue_executor_event( + &config, + "provider-create-task".to_owned(), + "semantic_tool.input".to_owned(), + EventPriority::P0, + json!({"semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", + "schemaVersion": 1, + "phase": "input", + "operationId": "create_task", + "content": {"digest": semantic_value_digest(&input)}, + "input": input, + }}), + ) + .unwrap(); + let transmitted = state.outbox[0] + .envelope + .pointer("/payload/payload/semantic_tool/input") + .unwrap(); + assert_eq!(transmitted, &input); + assert_eq!( + state.outbox[0] + .envelope + .pointer("/payload/payload/semantic_tool/content/digest"), + Some(&json!(semantic_value_digest(transmitted))), + ); + let document = format!( + "{}\nAuthorization: Bearer late-credential\nFINAL-ACCEPTANCE-42", + "Document content. ".repeat(400) + ); + let safe = + sanitize_semantic_tool_input("write_document", &json!({"body": document})).unwrap(); + let body = safe["body"].as_str().unwrap(); + assert!(body.len() > 4096); + assert!(body.ends_with("FINAL-ACCEPTANCE-42")); + assert!(!body.contains("late-credential")); + } + + #[test] + fn semantic_prose_does_not_exempt_credential_syntax_or_shapes() { + for text in [ + "auth token opaque-credential", + "access token opaque-credential", + "session token opaque-credential", + "refresh token opaque-credential", + "authentication token opaque-credential", + "literal token=opaque-credential", + "literal token:opaque-credential", + "literal --token opaque-credential", + "literal access_token opaque-credential", + "literal \"token\" opaque-credential", + "literal token \"opaque-credential\"", + ] { + assert!(!redact_text(text).contains("opaque-credential"), "{text}"); + let input = json!({"description": text, "initialPlan": text}); + assert!( + !sanitize_semantic_tool_input("create_task", &input) + .unwrap() + .to_string() + .contains("opaque-credential"), + "{text}" + ); + } + for secret in [ + "sk-proj-secretvalue123456", + "ghp_secretvalue12345678901234567890", + "github_pat_secretvalue12345678901234567890", + "eyJhbGciOiJIUzI1NiJ9.c2VjcmV0LWNsYWlt.signaturesecret", + ] { + let text = format!("Include the literal token {secret} in the document."); + assert!(!redact_text(&text).contains(secret), "{text}"); + assert!( + !sanitize_semantic_tool_input("write_document", &json!({"body": text})) + .unwrap() + .to_string() + .contains(secret) + ); + } + let input = json!({ + "description": "Include the literal token ACCEPTANCE-42. Authorization: Bearer opaque-credential", + "token": "opaque-credential", + }); + let safe = sanitize_semantic_tool_input("create_task", &input).unwrap(); + assert!(safe["description"] + .as_str() + .unwrap() + .contains("ACCEPTANCE-42")); + assert!(!safe.to_string().contains("opaque-credential")); + let diagnostic = json!({"description": "the token opaque-credential"}); + assert!( + !sanitize_semantic_tool_input("get_task_context", &diagnostic) + .unwrap() + .to_string() + .contains("opaque-credential") + ); + assert!(!sanitize_semantic_tool_input( + "create_task", + &json!({"diagnostic": "token opaque-credential"}) + ) + .unwrap() + .to_string() + .contains("opaque-credential")); + } + #[test] fn semantic_redaction_preserves_benign_token_system_prose() { let prose = "Offer a simple token system so guests can exchange items even when their contributions differ in quantity."; diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_provider_state.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_provider_state.rs index 9177443d93..4c3361de93 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_provider_state.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_provider_state.rs @@ -495,3 +495,116 @@ fn terminal_events_clear_pending_requests_and_reject_late_turn_events() { )) .is_err()); } + +#[test] +fn mutation_prose_survives_sidecar_decode_pending_state_and_semantic_projection() { + let mut state = AcpxProviderState::new("run-1").unwrap(); + state.begin_turn("turn-1").unwrap(); + let plan = format!( + "{}\nThe token CHAT8322bda781b81 must be included in the document.", + "Relevant context. ".repeat(400) + ); + let input = json!({ + "title": "Write project description", + "description": "The document must contain the token CHAT8322bda781b81.", + "initialPlan": plan, + "idempotencyKey": "CHAT8322bda781b81-task", + "apiToken": "actual-credential", + }); + let mut expected = input.clone(); + expected["apiToken"] = json!("[REDACTED]"); + let emitted = state + .accept_event(&event( + 1, + GeneratedAcpxSidecarEventType::RuntimeToolCalled, + Some("turn-1"), + json!({"callId": "call-1", "operationId": "create_task", "input": input}), + )) + .unwrap(); + assert_eq!(state.pending_tool("call-1").unwrap().input, expected); + let projected = project_acpx_state_event( + &AcpxEventProjectionContext { + run_id: "run-1".to_owned(), + normalized_session_id: "session-1".to_owned(), + turn_id: "turn-1".to_owned(), + provider_turn_id: None, + item_id: "call-1".to_owned(), + }, + &emitted[0], + ) + .unwrap(); + assert_eq!(projected[0].event_type, "semantic_tool.input"); + assert_eq!(projected[0].payload["semantic_tool"]["input"], expected); + assert_eq!( + projected[0].payload["semantic_tool"]["content"]["digest"], + json!(paperclip_runner_core::provider_bridge::semantic_value_digest(&expected)) + ); + + for (operation, field, prose, preserved) in [ + ( + "write_document", + "body", + "Include the token CHAT8322bda781b81.", + true, + ), + ( + "create_project", + "description", + "Include the token CHAT8322bda781b81.", + true, + ), + ( + "get_task_context", + "description", + "Include the token CHAT8322bda781b81.", + false, + ), + ( + "mcp__untrusted__create_task", + "description", + "Include the token CHAT8322bda781b81.", + false, + ), + ( + "create_task", + "description", + "Authorization: Bearer actual-credential", + false, + ), + ( + "create_task", + "initialPlan", + "access token actual-credential", + false, + ), + ] { + state + .complete_tool( + "call-1", + state + .pending_tool("call-1") + .unwrap() + .operation_id + .clone() + .as_str(), + ) + .unwrap(); + let emitted = state + .accept_event(&event( + 2, + GeneratedAcpxSidecarEventType::RuntimeToolCalled, + Some("turn-1"), + json!({"callId": "call-1", "operationId": operation, "input": {field: prose}}), + )) + .unwrap(); + let AcpxProviderStateEvent::ToolCall { input, .. } = &emitted[0] else { + panic!("expected tool call"); + }; + assert_eq!( + input[field] == json!(prose), + preserved, + "{operation}: {prose}" + ); + assert!(!input.to_string().contains("actual-credential")); + } +} diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_sidecar_transport.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_sidecar_transport.rs index 2299a71e62..a1c2b27415 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_sidecar_transport.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_sidecar_transport.rs @@ -180,3 +180,37 @@ fn redacts_sidecar_stderr_when_the_process_exits() { assert!(message.contains("[REDACTED]")); assert!(!message.contains("amber-signal-7305")); } + +#[cfg(unix)] +#[test] +fn preserves_only_allowlisted_stderr_categories_when_the_process_exits() { + let mut transport = AcpxSidecarTransport::start(&AcpxSidecarTransportConfig { + command: PathBuf::from("/bin/sh"), + args: vec![ + "-c".to_owned(), + "printf '%s\n' 'TypeError [ERR_INVALID_ARG_TYPE]: token=amber-signal-7305' ' at /private/secret-project/session-123.js:42' 'triggerUncaughtException(err, true /* fromPromise */);' 'Error: ACPX provider spawned after ownership admission was sealed' 'code: EPIPE' 'UnknownProviderError: private-value' 'prefixECONNRESETsuffix' >&2; exit 1".to_owned(), + ], + verified_launch: None, + request_timeout: Duration::from_secs(1), + shutdown_grace: Duration::from_millis(50), + }) + .expect("diagnostic fixture should start"); + let error = transport + .poll_event(Duration::from_secs(1)) + .expect_err("exited sidecar must fail"); + let message = error.to_string(); + assert!(message.contains("stderrCategories=broken_pipe,invalid_argument_type,javascript_type_error,provider_spawn_after_ownership_seal,unhandled_rejection")); + assert!(message.contains("stderrTail=")); + assert!(message.contains("[REDACTED]")); + for sensitive in [ + "amber-signal-7305", + "secret-project", + "session-123", + "private-value", + "UnknownProviderError", + "connection_reset", + "TypeError", + ] { + assert!(!message.contains(sensitive)); + } +} diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs index 8c965e0cc3..b64131d23f 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs @@ -124,6 +124,30 @@ fn opencode_call_count(state_dir: &Path, method: &str) -> usize { .count() } +fn assert_valid_terminal(payload: &Value) { + let schema: Value = serde_json::from_str(include_str!( + "../../../../protocol/schemas/terminal.schema.json" + )) + .unwrap(); + let stop_reason: Value = serde_json::from_str(include_str!( + "../../../../protocol/schemas/stop-reason.schema.json" + )) + .unwrap(); + let registry = jsonschema::Registry::new() + .add( + "https://paperclip.dev/schemas/prp/v1/stop-reason.schema.json", + stop_reason, + ) + .unwrap() + .prepare() + .unwrap(); + let validator = jsonschema::options() + .with_registry(®istry) + .build(&schema) + .unwrap(); + validator.validate(payload).unwrap(); +} + fn command(sequence: u64, command_type: &str, payload: Value) -> Command { Command { schema: "paperclip.prp.command.v1".to_owned(), @@ -216,6 +240,7 @@ fn preserves_acpx_semantic_disposition_in_the_run_terminal() { .iter() .find(|event| event.event_type == "run.terminal") .expect("ACPX blocked result must become terminal"); + assert_valid_terminal(&terminal.payload); assert_eq!(terminal.payload["runTerminalState"], "succeeded"); assert_eq!(terminal.payload["reportedWorkDisposition"], "blocked"); @@ -360,7 +385,19 @@ fn executes_a_qualified_acpx_profile_through_the_native_selector() { assert!(events .iter() .any(|event| event.event_type == "run.terminal")); + assert_valid_terminal( + &events + .iter() + .find(|event| event.event_type == "run.terminal") + .unwrap() + .payload, + ); + // runner.drain must see this exact terminal suffix without polling the + // provider again. An empty default implementation strands the suffix and + // makes shared native transport closure fail after a successful reply. + assert_eq!(executor.retained_events().unwrap(), events); executor.acknowledge_events(events.len()).unwrap(); + assert!(executor.retained_events().unwrap().is_empty()); executor .execute(&command(4, "session.close", json!({}))) .unwrap(); @@ -368,6 +405,57 @@ fn executes_a_qualified_acpx_profile_through_the_native_selector() { fs::remove_dir_all(directory).unwrap(); } +#[test] +fn resumes_an_idle_acpx_session_in_a_cold_replacement_runner() { + let directory = temporary_directory("acpx-cold-idle-recovery"); + let config = acpx_config(&directory, "turns-reserved-result-terminal"); + let mut executor = NativeProviderCommandExecutor::with_runner_config(&directory, &config); + executor + .execute(&command( + 1, + "run.prepare", + prepare_payload(&directory, "codex"), + )) + .unwrap(); + let original = executor + .execute(&command(2, "session.open", json!({}))) + .unwrap(); + executor + .execute(&command( + 3, + "turn.start", + json!({"text":"Acknowledge.", "turnId":"provider-turn-first"}), + )) + .unwrap(); + let events = executor.poll_events().unwrap(); + executor.acknowledge_events(events.len()).unwrap(); + executor + .execute(&command(4, "runner.suspend", json!({}))) + .unwrap(); + executor.shutdown().unwrap(); + drop(executor); + + let mut replacement_config = config.clone(); + replacement_config.run_id = "run-2".to_owned(); + replacement_config.turn_id = "turn-2".to_owned(); + let mut replacement = + NativeProviderCommandExecutor::with_runner_config(&directory, &replacement_config); + let mut payload = prepare_payload(&directory, "codex"); + payload["provider"]["runId"] = json!("run-2"); + let resumed = replacement + .execute(&command(1, "run.attach", payload)) + .unwrap(); + assert_eq!(resumed.result["status"], "resumed"); + assert_eq!( + resumed.result["providerSessionId"], + original.result["providerSessionId"] + ); + // Admission itself must preserve the provider identity before any new + // model turn. The fixture's scripted terminal events belong to run-1. + replacement.shutdown().unwrap(); + fs::remove_dir_all(directory).unwrap(); +} + #[test] fn keeps_native_acpx_semantic_events_on_the_durable_controller_turn() { let directory = temporary_directory("acpx-durable-turn-correlation"); diff --git a/packages/paperclip-runner/scripts/check-capability-inventory.test.mjs b/packages/paperclip-runner/scripts/check-capability-inventory.test.mjs index 2b47541f0f..8289388535 100644 --- a/packages/paperclip-runner/scripts/check-capability-inventory.test.mjs +++ b/packages/paperclip-runner/scripts/check-capability-inventory.test.mjs @@ -42,7 +42,7 @@ function validInventories() { schemaVersion: 2, inventoryRole: "normative", generatedFrom: ["skills/paperclip/SKILL.md"], - rows: Array.from({ length: 153 }, (_, index) => row(`capability-${index}`)), + rows: Array.from({ length: 154 }, (_, index) => row(`capability-${index}`)), }, evaluations: { schemaVersion: 2, diff --git a/packages/paperclip-runner/scripts/lib/capability-inventory.mjs b/packages/paperclip-runner/scripts/lib/capability-inventory.mjs index 7a1beb762e..0a2cccaf36 100644 --- a/packages/paperclip-runner/scripts/lib/capability-inventory.mjs +++ b/packages/paperclip-runner/scripts/lib/capability-inventory.mjs @@ -247,7 +247,7 @@ export async function buildMcpInventory(repoRoot) { export function validateInventories(inventories) { const errors = []; - const expectedCounts = { capabilities: 153, evaluations: 106, legacyMcpAliases: 42 }; + const expectedCounts = { capabilities: 154, evaluations: 106, legacyMcpAliases: 42 }; const normativeNames = ["capabilities", "evaluations"]; const normativeRows = new Map(); const globalNormativeIds = new Set(); diff --git a/packages/paperclip-runner/spec/capability/capabilities.yaml b/packages/paperclip-runner/spec/capability/capabilities.yaml index 6296475225..7d0a38040c 100644 --- a/packages/paperclip-runner/spec/capability/capabilities.yaml +++ b/packages/paperclip-runner/spec/capability/capabilities.yaml @@ -59,12 +59,12 @@ ] }, { - "id": "skill:skills/paperclip/SKILL.md:server-verified-external-chat-turns:30", + "id": "skill:skills/paperclip/SKILL.md:conversation-tasks:30", "sourceKind": "skill_heading", "sourceAnchor": "skills/paperclip/SKILL.md:30", - "title": "Server-Verified External Chat Turns", - "expectedSemantics": "Skill guidance headed “Server-Verified External Chat Turns”.", - "primaryDisposition": "control_plane_owned", + "title": "Conversation tasks", + "expectedSemantics": "Skill guidance headed “Conversation tasks”.", + "primaryDisposition": "optional_agent_tool", "requiredGrants": [], "assertionClasses": [ "control_plane_invariant" @@ -74,9 +74,24 @@ ] }, { - "id": "skill:skills/paperclip/SKILL.md:the-heartbeat-procedure:70", + "id": "skill:skills/paperclip/SKILL.md:server-verified-external-chat-turns:47", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:70", + "sourceAnchor": "skills/paperclip/SKILL.md:47", + "title": "Server-Verified External Chat Turns", + "expectedSemantics": "Skill guidance headed “Server-Verified External Chat Turns”.", + "primaryDisposition": "control_plane_owned", + "requiredGrants": [], + "assertionClasses": [ + "control_plane_invariant" + ], + "evidenceIds": [ + "skill:skills/paperclip/SKILL.md:47" + ] + }, + { + "id": "skill:skills/paperclip/SKILL.md:the-heartbeat-procedure:87", + "sourceKind": "skill_heading", + "sourceAnchor": "skills/paperclip/SKILL.md:87", "title": "The Heartbeat Procedure", "expectedSemantics": "Skill guidance headed “The Heartbeat Procedure”.", "primaryDisposition": "optional_agent_tool", @@ -85,13 +100,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:70" + "skill:skills/paperclip/SKILL.md:87" ] }, { - "id": "skill:skills/paperclip/SKILL.md:generated-artifacts-and-work-products:142", + "id": "skill:skills/paperclip/SKILL.md:generated-artifacts-and-work-products:159", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:142", + "sourceAnchor": "skills/paperclip/SKILL.md:159", "title": "Generated Artifacts and Work Products", "expectedSemantics": "Skill guidance headed “Generated Artifacts and Work Products”.", "primaryDisposition": "always_agent_tool", @@ -100,13 +115,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:142" + "skill:skills/paperclip/SKILL.md:159" ] }, { - "id": "skill:skills/paperclip/SKILL.md:status-quick-guide:190", + "id": "skill:skills/paperclip/SKILL.md:status-quick-guide:207", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:190", + "sourceAnchor": "skills/paperclip/SKILL.md:207", "title": "Status Quick Guide", "expectedSemantics": "Skill guidance headed “Status Quick Guide”.", "primaryDisposition": "control_plane_owned", @@ -115,13 +130,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:190" + "skill:skills/paperclip/SKILL.md:207" ] }, { - "id": "skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:200", + "id": "skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:217", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:200", + "sourceAnchor": "skills/paperclip/SKILL.md:217", "title": "Monitors and Watchers (say only what you actually scheduled)", "expectedSemantics": "Skill guidance headed “Monitors and Watchers (say only what you actually scheduled)”.", "primaryDisposition": "optional_agent_tool", @@ -130,13 +145,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:200" + "skill:skills/paperclip/SKILL.md:217" ] }, { - "id": "skill:skills/paperclip/SKILL.md:delegating-review-tasks:213", + "id": "skill:skills/paperclip/SKILL.md:delegating-review-tasks:230", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:213", + "sourceAnchor": "skills/paperclip/SKILL.md:230", "title": "Delegating review tasks", "expectedSemantics": "Skill guidance headed “Delegating review tasks”.", "primaryDisposition": "always_agent_tool", @@ -145,13 +160,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:213" + "skill:skills/paperclip/SKILL.md:230" ] }, { - "id": "skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:224", + "id": "skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:241", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:224", + "sourceAnchor": "skills/paperclip/SKILL.md:241", "title": "Managing A User's Inbox", "expectedSemantics": "Skill guidance headed “Managing A User's Inbox”.", "primaryDisposition": "control_plane_owned", @@ -160,13 +175,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:224" + "skill:skills/paperclip/SKILL.md:241" ] }, { - "id": "skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:232", + "id": "skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:249", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:232", + "sourceAnchor": "skills/paperclip/SKILL.md:249", "title": "Issue Dependencies (Blockers)", "expectedSemantics": "Skill guidance headed “Issue Dependencies (Blockers)”.", "primaryDisposition": "control_plane_owned", @@ -175,13 +190,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:232" + "skill:skills/paperclip/SKILL.md:249" ] }, { - "id": "skill:skills/paperclip/SKILL.md:requesting-board-approval:257", + "id": "skill:skills/paperclip/SKILL.md:requesting-board-approval:274", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:257", + "sourceAnchor": "skills/paperclip/SKILL.md:274", "title": "Requesting Board Approval", "expectedSemantics": "Skill guidance headed “Requesting Board Approval”.", "primaryDisposition": "optional_agent_tool", @@ -190,13 +205,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:257" + "skill:skills/paperclip/SKILL.md:274" ] }, { - "id": "skill:skills/paperclip/SKILL.md:issue-thread-interactions:278", + "id": "skill:skills/paperclip/SKILL.md:issue-thread-interactions:295", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:278", + "sourceAnchor": "skills/paperclip/SKILL.md:295", "title": "Issue-Thread Interactions", "expectedSemantics": "Skill guidance headed “Issue-Thread Interactions”.", "primaryDisposition": "optional_agent_tool", @@ -205,13 +220,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:278" + "skill:skills/paperclip/SKILL.md:295" ] }, { - "id": "skill:skills/paperclip/SKILL.md:standalone-decisions:307", + "id": "skill:skills/paperclip/SKILL.md:standalone-decisions:324", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:307", + "sourceAnchor": "skills/paperclip/SKILL.md:324", "title": "Standalone Decisions", "expectedSemantics": "Skill guidance headed “Standalone Decisions”.", "primaryDisposition": "optional_agent_tool", @@ -220,13 +235,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:307" + "skill:skills/paperclip/SKILL.md:324" ] }, { - "id": "skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:411", + "id": "skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:428", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:411", + "sourceAnchor": "skills/paperclip/SKILL.md:428", "title": "MCP Tool Approval Gates", "expectedSemantics": "Skill guidance headed “MCP Tool Approval Gates”.", "primaryDisposition": "optional_agent_tool", @@ -235,13 +250,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:411" + "skill:skills/paperclip/SKILL.md:428" ] }, { - "id": "skill:skills/paperclip/SKILL.md:niche-workflow-pointers:453", + "id": "skill:skills/paperclip/SKILL.md:niche-workflow-pointers:470", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:453", + "sourceAnchor": "skills/paperclip/SKILL.md:470", "title": "Niche Workflow Pointers", "expectedSemantics": "Skill guidance headed “Niche Workflow Pointers”.", "primaryDisposition": "optional_agent_tool", @@ -250,13 +265,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:453" + "skill:skills/paperclip/SKILL.md:470" ] }, { - "id": "skill:skills/paperclip/SKILL.md:cases:463", + "id": "skill:skills/paperclip/SKILL.md:cases:480", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:463", + "sourceAnchor": "skills/paperclip/SKILL.md:480", "title": "Cases", "expectedSemantics": "Skill guidance headed “Cases”.", "primaryDisposition": "optional_agent_tool", @@ -265,13 +280,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:463" + "skill:skills/paperclip/SKILL.md:480" ] }, { - "id": "skill:skills/paperclip/SKILL.md:company-skills-workflow:468", + "id": "skill:skills/paperclip/SKILL.md:company-skills-workflow:485", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:468", + "sourceAnchor": "skills/paperclip/SKILL.md:485", "title": "Company Skills Workflow", "expectedSemantics": "Skill guidance headed “Company Skills Workflow”.", "primaryDisposition": "optional_agent_tool", @@ -280,13 +295,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:468" + "skill:skills/paperclip/SKILL.md:485" ] }, { - "id": "skill:skills/paperclip/SKILL.md:routines:479", + "id": "skill:skills/paperclip/SKILL.md:routines:496", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:479", + "sourceAnchor": "skills/paperclip/SKILL.md:496", "title": "Routines", "expectedSemantics": "Skill guidance headed “Routines”.", "primaryDisposition": "optional_agent_tool", @@ -295,13 +310,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:479" + "skill:skills/paperclip/SKILL.md:496" ] }, { - "id": "skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:490", + "id": "skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:507", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:490", + "sourceAnchor": "skills/paperclip/SKILL.md:507", "title": "Issue Workspace Runtime Controls", "expectedSemantics": "Skill guidance headed “Issue Workspace Runtime Controls”.", "primaryDisposition": "optional_agent_tool", @@ -310,13 +325,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:490" + "skill:skills/paperclip/SKILL.md:507" ] }, { - "id": "skill:skills/paperclip/SKILL.md:proposing-credentials-safely:497", + "id": "skill:skills/paperclip/SKILL.md:proposing-credentials-safely:514", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:497", + "sourceAnchor": "skills/paperclip/SKILL.md:514", "title": "Proposing Credentials Safely", "expectedSemantics": "Skill guidance headed “Proposing Credentials Safely”.", "primaryDisposition": "optional_agent_tool", @@ -325,13 +340,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:497" + "skill:skills/paperclip/SKILL.md:514" ] }, { - "id": "skill:skills/paperclip/SKILL.md:reading-granted-secrets:504", + "id": "skill:skills/paperclip/SKILL.md:reading-granted-secrets:521", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:504", + "sourceAnchor": "skills/paperclip/SKILL.md:521", "title": "Reading Granted Secrets", "expectedSemantics": "Skill guidance headed “Reading Granted Secrets”.", "primaryDisposition": "optional_agent_tool", @@ -340,13 +355,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:504" + "skill:skills/paperclip/SKILL.md:521" ] }, { - "id": "skill:skills/paperclip/SKILL.md:critical-rules:530", + "id": "skill:skills/paperclip/SKILL.md:critical-rules:547", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:530", + "sourceAnchor": "skills/paperclip/SKILL.md:547", "title": "Critical Rules", "expectedSemantics": "Skill guidance headed “Critical Rules”.", "primaryDisposition": "optional_agent_tool", @@ -355,13 +370,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:530" + "skill:skills/paperclip/SKILL.md:547" ] }, { - "id": "skill:skills/paperclip/SKILL.md:comment-style-required:554", + "id": "skill:skills/paperclip/SKILL.md:comment-style-required:571", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:554", + "sourceAnchor": "skills/paperclip/SKILL.md:571", "title": "Comment Style (Required)", "expectedSemantics": "Skill guidance headed “Comment Style (Required)”.", "primaryDisposition": "always_agent_tool", @@ -370,13 +385,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:554" + "skill:skills/paperclip/SKILL.md:571" ] }, { - "id": "skill:skills/paperclip/SKILL.md:update:586", + "id": "skill:skills/paperclip/SKILL.md:update:603", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:586", + "sourceAnchor": "skills/paperclip/SKILL.md:603", "title": "Update", "expectedSemantics": "Skill guidance headed “Update”.", "primaryDisposition": "optional_agent_tool", @@ -385,13 +400,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:586" + "skill:skills/paperclip/SKILL.md:603" ] }, { - "id": "skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:596", + "id": "skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:613", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:596", + "sourceAnchor": "skills/paperclip/SKILL.md:613", "title": "Planning (Required when planning requested)", "expectedSemantics": "Skill guidance headed “Planning (Required when planning requested)”.", "primaryDisposition": "optional_agent_tool", @@ -400,13 +415,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:596" + "skill:skills/paperclip/SKILL.md:613" ] }, { - "id": "skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:629", + "id": "skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:646", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:629", + "sourceAnchor": "skills/paperclip/SKILL.md:646", "title": "Key Endpoints (Hot Routes)", "expectedSemantics": "Skill guidance headed “Key Endpoints (Hot Routes)”.", "primaryDisposition": "optional_agent_tool", @@ -415,13 +430,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:629" + "skill:skills/paperclip/SKILL.md:646" ] }, { - "id": "skill:skills/paperclip/SKILL.md:searching-issues:658", + "id": "skill:skills/paperclip/SKILL.md:searching-issues:675", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:658", + "sourceAnchor": "skills/paperclip/SKILL.md:675", "title": "Searching Issues", "expectedSemantics": "Skill guidance headed “Searching Issues”.", "primaryDisposition": "optional_agent_tool", @@ -430,13 +445,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:658" + "skill:skills/paperclip/SKILL.md:675" ] }, { - "id": "skill:skills/paperclip/SKILL.md:full-reference:668", + "id": "skill:skills/paperclip/SKILL.md:full-reference:685", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:668", + "sourceAnchor": "skills/paperclip/SKILL.md:685", "title": "Full Reference", "expectedSemantics": "Skill guidance headed “Full Reference”.", "primaryDisposition": "optional_agent_tool", @@ -445,7 +460,7 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:668" + "skill:skills/paperclip/SKILL.md:685" ] }, { @@ -1979,9 +1994,9 @@ ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:option-a-one-call-create-with-workspace:787", + "id": "skill:skills/paperclip/references/api-reference.md:option-a-one-call-create-with-workspace:807", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:787", + "sourceAnchor": "skills/paperclip/references/api-reference.md:807", "title": "Option A: One-call create with workspace", "expectedSemantics": "Skill guidance headed “Option A: One-call create with workspace”.", "primaryDisposition": "optional_agent_tool", @@ -1990,13 +2005,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:787" + "skill:skills/paperclip/references/api-reference.md:807" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:option-b-two-calls-project-first-then-workspace:806", + "id": "skill:skills/paperclip/references/api-reference.md:option-b-two-calls-project-first-then-workspace:826", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:806", + "sourceAnchor": "skills/paperclip/references/api-reference.md:826", "title": "Option B: Two calls (project first, then workspace)", "expectedSemantics": "Skill guidance headed “Option B: Two calls (project first, then workspace)”.", "primaryDisposition": "optional_agent_tool", @@ -2005,13 +2020,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:806" + "skill:skills/paperclip/references/api-reference.md:826" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:governance-and-approvals:835", + "id": "skill:skills/paperclip/references/api-reference.md:governance-and-approvals:855", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:835", + "sourceAnchor": "skills/paperclip/references/api-reference.md:855", "title": "Governance and Approvals", "expectedSemantics": "Skill guidance headed “Governance and Approvals”.", "primaryDisposition": "optional_agent_tool", @@ -2020,30 +2035,15 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:835" + "skill:skills/paperclip/references/api-reference.md:855" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:requesting-a-hire-management-only:839", - "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:839", - "title": "Requesting a hire (management only)", - "expectedSemantics": "Skill guidance headed “Requesting a hire (management only)”.", - "primaryDisposition": "optional_agent_tool", - "requiredGrants": [], - "assertionClasses": [ - "control_plane_invariant" - ], - "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:839" - ] - }, - { - "id": "skill:skills/paperclip/references/api-reference.md:ceo-strategy-approval:859", + "id": "skill:skills/paperclip/references/api-reference.md:requesting-a-hire-management-only:859", "sourceKind": "skill_heading", "sourceAnchor": "skills/paperclip/references/api-reference.md:859", - "title": "CEO strategy approval", - "expectedSemantics": "Skill guidance headed “CEO strategy approval”.", + "title": "Requesting a hire (management only)", + "expectedSemantics": "Skill guidance headed “Requesting a hire (management only)”.", "primaryDisposition": "optional_agent_tool", "requiredGrants": [], "assertionClasses": [ @@ -2054,9 +2054,24 @@ ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:868", + "id": "skill:skills/paperclip/references/api-reference.md:ceo-strategy-approval:879", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:868", + "sourceAnchor": "skills/paperclip/references/api-reference.md:879", + "title": "CEO strategy approval", + "expectedSemantics": "Skill guidance headed “CEO strategy approval”.", + "primaryDisposition": "optional_agent_tool", + "requiredGrants": [], + "assertionClasses": [ + "control_plane_invariant" + ], + "evidenceIds": [ + "skill:skills/paperclip/references/api-reference.md:879" + ] + }, + { + "id": "skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:888", + "sourceKind": "skill_heading", + "sourceAnchor": "skills/paperclip/references/api-reference.md:888", "title": "Issue-thread confirmations", "expectedSemantics": "Skill guidance headed “Issue-thread confirmations”.", "primaryDisposition": "always_agent_tool", @@ -2065,13 +2080,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:868" + "skill:skills/paperclip/references/api-reference.md:888" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:checkbox-confirmations:926", + "id": "skill:skills/paperclip/references/api-reference.md:checkbox-confirmations:946", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:926", + "sourceAnchor": "skills/paperclip/references/api-reference.md:946", "title": "Checkbox confirmations", "expectedSemantics": "Skill guidance headed “Checkbox confirmations”.", "primaryDisposition": "always_agent_tool", @@ -2080,13 +2095,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:926" + "skill:skills/paperclip/references/api-reference.md:946" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:item-verdict-requests:1041", + "id": "skill:skills/paperclip/references/api-reference.md:item-verdict-requests:1061", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1041", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1061", "title": "Item verdict requests", "expectedSemantics": "Skill guidance headed “Item verdict requests”.", "primaryDisposition": "optional_agent_tool", @@ -2095,13 +2110,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1041" + "skill:skills/paperclip/references/api-reference.md:1061" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:checking-approval-status:1151", + "id": "skill:skills/paperclip/references/api-reference.md:checking-approval-status:1171", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1151", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1171", "title": "Checking approval status", "expectedSemantics": "Skill guidance headed “Checking approval status”.", "primaryDisposition": "optional_agent_tool", @@ -2110,13 +2125,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1151" + "skill:skills/paperclip/references/api-reference.md:1171" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:approval-follow-up-requesting-agent:1157", + "id": "skill:skills/paperclip/references/api-reference.md:approval-follow-up-requesting-agent:1177", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1157", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1177", "title": "Approval follow-up (requesting agent)", "expectedSemantics": "Skill guidance headed “Approval follow-up (requesting agent)”.", "primaryDisposition": "always_agent_tool", @@ -2125,13 +2140,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1157" + "skill:skills/paperclip/references/api-reference.md:1177" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:issue-lifecycle:1175", + "id": "skill:skills/paperclip/references/api-reference.md:issue-lifecycle:1195", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1175", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1195", "title": "Issue Lifecycle", "expectedSemantics": "Skill guidance headed “Issue Lifecycle”.", "primaryDisposition": "always_agent_tool", @@ -2140,13 +2155,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1175" + "skill:skills/paperclip/references/api-reference.md:1195" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:error-handling:1205", + "id": "skill:skills/paperclip/references/api-reference.md:error-handling:1225", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1205", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1225", "title": "Error Handling", "expectedSemantics": "Skill guidance headed “Error Handling”.", "primaryDisposition": "control_plane_owned", @@ -2155,13 +2170,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1205" + "skill:skills/paperclip/references/api-reference.md:1225" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:full-api-reference:1219", + "id": "skill:skills/paperclip/references/api-reference.md:full-api-reference:1239", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1219", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1239", "title": "Full API Reference", "expectedSemantics": "Skill guidance headed “Full API Reference”.", "primaryDisposition": "optional_agent_tool", @@ -2170,13 +2185,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1219" + "skill:skills/paperclip/references/api-reference.md:1239" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:agents:1221", + "id": "skill:skills/paperclip/references/api-reference.md:agents:1241", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1221", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1241", "title": "Agents", "expectedSemantics": "Skill guidance headed “Agents”.", "primaryDisposition": "optional_agent_tool", @@ -2185,13 +2200,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1221" + "skill:skills/paperclip/references/api-reference.md:1241" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:issues-tasks:1242", + "id": "skill:skills/paperclip/references/api-reference.md:issues-tasks:1262", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1242", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1262", "title": "Issues (Tasks)", "expectedSemantics": "Skill guidance headed “Issues (Tasks)”.", "primaryDisposition": "optional_agent_tool", @@ -2200,13 +2215,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1242" + "skill:skills/paperclip/references/api-reference.md:1262" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1282", + "id": "skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1302", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1282", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1302", "title": "Companies, Projects, Goals", "expectedSemantics": "Skill guidance headed “Companies, Projects, Goals”.", "primaryDisposition": "optional_agent_tool", @@ -2215,13 +2230,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1282" + "skill:skills/paperclip/references/api-reference.md:1302" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:routines:1306", + "id": "skill:skills/paperclip/references/api-reference.md:routines:1326", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1306", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1326", "title": "Routines", "expectedSemantics": "Skill guidance headed “Routines”.", "primaryDisposition": "optional_agent_tool", @@ -2230,13 +2245,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1306" + "skill:skills/paperclip/references/api-reference.md:1326" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1322", + "id": "skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1342", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1322", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1342", "title": "Approvals, Costs, Activity, Dashboard", "expectedSemantics": "Skill guidance headed “Approvals, Costs, Activity, Dashboard”.", "primaryDisposition": "optional_agent_tool", @@ -2245,13 +2260,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1322" + "skill:skills/paperclip/references/api-reference.md:1342" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:secrets:1344", + "id": "skill:skills/paperclip/references/api-reference.md:secrets:1364", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1344", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1364", "title": "Secrets", "expectedSemantics": "Skill guidance headed “Secrets”.", "primaryDisposition": "optional_agent_tool", @@ -2260,13 +2275,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1344" + "skill:skills/paperclip/references/api-reference.md:1364" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1357", + "id": "skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1377", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1357", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1377", "title": "Agent secret proposals", "expectedSemantics": "Skill guidance headed “Agent secret proposals”.", "primaryDisposition": "optional_agent_tool", @@ -2275,13 +2290,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1357" + "skill:skills/paperclip/references/api-reference.md:1377" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:agent-secret-access:1457", + "id": "skill:skills/paperclip/references/api-reference.md:agent-secret-access:1477", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1457", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1477", "title": "Agent secret access", "expectedSemantics": "Skill guidance headed “Agent secret access”.", "primaryDisposition": "optional_agent_tool", @@ -2290,13 +2305,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1457" + "skill:skills/paperclip/references/api-reference.md:1477" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:common-mistakes:1497", + "id": "skill:skills/paperclip/references/api-reference.md:common-mistakes:1517", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1497", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1517", "title": "Common Mistakes", "expectedSemantics": "Skill guidance headed “Common Mistakes”.", "primaryDisposition": "optional_agent_tool", @@ -2305,7 +2320,7 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1497" + "skill:skills/paperclip/references/api-reference.md:1517" ] } ] diff --git a/packages/paperclip-runner/spec/capability/protocol-coverage.json b/packages/paperclip-runner/spec/capability/protocol-coverage.json index b71d9de2dc..9e10df0247 100644 --- a/packages/paperclip-runner/spec/capability/protocol-coverage.json +++ b/packages/paperclip-runner/spec/capability/protocol-coverage.json @@ -7,10 +7,48 @@ "src/scenarios/scenario-plan.ts" ], "counts": { - "actions": 43, + "actions": 45, "legacyRequirements": 106 }, "actions": [ + { + "id": "create_project", + "ownership": "optional_agent_tool", + "surfaces": [ + "live" + ], + "legacyAliases": [], + "contractCase": "protocol-action:create_project", + "contractOwner": "src/catalog/protocol-action-contracts.test.ts::create_project has a schema-valid canonical example and every declared projection", + "legacyBehavioralCases": [], + "deterministicCases": [ + "protocol-action:create_project" + ], + "legacyRequirementCases": [], + "deterministicOwners": [ + "src/catalog/protocol-action-contracts.test.ts::create_project has a schema-valid canonical example and every declared projection", + "src/scenarios/scenario-explorer.test.ts::renders every scenario with exposure, control plane, authorization, diff, and parity" + ] + }, + { + "id": "list_project_repositories", + "ownership": "optional_agent_tool", + "surfaces": [ + "live" + ], + "legacyAliases": [], + "contractCase": "protocol-action:list_project_repositories", + "contractOwner": "src/catalog/protocol-action-contracts.test.ts::list_project_repositories has a schema-valid canonical example and every declared projection", + "legacyBehavioralCases": [], + "deterministicCases": [ + "protocol-action:list_project_repositories" + ], + "legacyRequirementCases": [], + "deterministicOwners": [ + "src/catalog/protocol-action-contracts.test.ts::list_project_repositories has a schema-valid canonical example and every declared projection", + "src/scenarios/scenario-explorer.test.ts::renders every scenario with exposure, control plane, authorization, diff, and parity" + ] + }, { "id": "search_api", "ownership": "optional_agent_tool", @@ -913,7 +951,8 @@ "id": "list_projects", "ownership": "optional_agent_tool", "surfaces": [ - "scenario" + "scenario", + "live" ], "legacyAliases": [], "contractCase": "protocol-action:list_projects", diff --git a/packages/paperclip-runner/src/catalog/canonical-operations.ts b/packages/paperclip-runner/src/catalog/canonical-operations.ts index 2d37b260f4..b2f4f5be29 100644 --- a/packages/paperclip-runner/src/catalog/canonical-operations.ts +++ b/packages/paperclip-runner/src/catalog/canonical-operations.ts @@ -21,7 +21,7 @@ export const CAPABILITY_CANONICAL_OPERATIONS: readonly CapabilityCanonicalOperat .sort((left, right) => left.operationId.localeCompare(right.operationId)), ); const byId = new Map(CAPABILITY_CANONICAL_OPERATIONS.map((operation) => [operation.operationId, operation])); -if (byId.size !== 43) throw new Error(`expected 43 canonical semantic operations, found ${byId.size}`); +if (byId.size !== PAPERCLIP_PROTOCOL_ACTIONS.length) throw new Error("Duplicate canonical semantic operation ID"); export function capabilityCanonicalOperation(operationId: string): CapabilityCanonicalOperation | undefined { return byId.get(operationId); } export function capabilityCanonicalOperationsForSurface(surface: CapabilityCatalogSurface): readonly CapabilityCanonicalOperation[] { return CAPABILITY_CANONICAL_OPERATIONS.filter((operation) => operation.surfaces.includes(surface)); } export function capabilityCanonicalOperationIds(): readonly string[] { return CAPABILITY_CANONICAL_OPERATIONS.map((operation) => operation.operationId); } diff --git a/packages/paperclip-runner/src/catalog/reconciliation.test.ts b/packages/paperclip-runner/src/catalog/reconciliation.test.ts index 5445dbcc69..417d38739f 100644 --- a/packages/paperclip-runner/src/catalog/reconciliation.test.ts +++ b/packages/paperclip-runner/src/catalog/reconciliation.test.ts @@ -20,16 +20,18 @@ describe("canonical semantic-catalog reconciliation authority", () => { it("pins the reconciled op-set relationship between the two catalogs", () => { const summary = capabilityCatalogReconciliation(); expect(summary.scenarioCount).toBe(37); - expect(summary.liveCount).toBe(30); - expect(summary.sharedCount).toBe(24); - expect(summary.unionCount).toBe(43); + expect(summary.liveCount).toBe(33); + expect(summary.sharedCount).toBe(25); + expect(summary.unionCount).toBe(45); // Any operation added to or removed from either catalog without a // reconciliation decision changes these exact sets and fails the gate. expect(summary.liveOnly).toEqual([ "call_api", + "create_project", "get_agent", "get_approval", "get_approval_context", + "list_project_repositories", "schedule_wake", "search_api", ]); @@ -40,7 +42,6 @@ describe("canonical semantic-catalog reconciliation authority", () => { "list_cases", "list_company_skills", "list_goals", - "list_projects", "list_routines", "list_secret_metadata", "manage_routine", @@ -51,7 +52,7 @@ describe("canonical semantic-catalog reconciliation authority", () => { }); it("is the single source both catalogs derive their operation set from", () => { - expect(CAPABILITY_CANONICAL_OPERATIONS).toHaveLength(43); + expect(CAPABILITY_CANONICAL_OPERATIONS).toHaveLength(45); const canonicalIds = new Set(CAPABILITY_CANONICAL_OPERATIONS.map((operation) => operation.operationId)); // Neither catalog may contain an operation absent from the canonical source. for (const tool of SCENARIO_CATALOG) expect(canonicalIds.has(tool.operationId)).toBe(true); @@ -85,7 +86,7 @@ describe("canonical semantic-catalog reconciliation authority", () => { }); it("names placement, claims, task modes, side-effect class, idempotency, redaction, mock mapping, real binding status, and PRP evidence for every operation", () => { - expect(CAPABILITY_CANONICAL_CATALOG).toHaveLength(43); + expect(CAPABILITY_CANONICAL_CATALOG).toHaveLength(45); for (const operation of CAPABILITY_CANONICAL_CATALOG) { expect(operation.placement).toMatch(/^(always|optional)_agent_tool$/); expect(Array.isArray(operation.requiredClaims)).toBe(true); @@ -111,8 +112,8 @@ describe("canonical semantic-catalog reconciliation authority", () => { it("classifies real binding status so generic_api_request is never product coverage", () => { const summary = capabilityCatalogReconciliation(); expect(summary.byRealBindingStatus).toEqual({ - live_codex: 29, - scenario_mock: 13, + live_codex: 32, + scenario_mock: 12, test_only: 1, }); expect(capabilityCanonicalOperation("generic_api_request")?.realBindingStatus).toBe("test_only"); diff --git a/packages/paperclip-runner/src/catalog/semantic-action-catalog.test.ts b/packages/paperclip-runner/src/catalog/semantic-action-catalog.test.ts index 6c62a21573..48b2cbb9b0 100644 --- a/packages/paperclip-runner/src/catalog/semantic-action-catalog.test.ts +++ b/packages/paperclip-runner/src/catalog/semantic-action-catalog.test.ts @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"; import Ajv2020 from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; +import { createTaskAction } from "../protocol-actions/create-task.js"; import { PAPERCLIP_SEMANTIC_ACTION_CATALOG, @@ -18,12 +19,27 @@ const packageRoot = resolve( ); describe("semantic action catalog", () => { + it("accepts project handoff receipts and preserves ordinary child task receipts", () => { + const ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true, strict: true }); + const validate = ajv.compile(createTaskAction.live.descriptor.outputSchema); + const receipt = { + commandId: "create-task-1", disposition: "applied", stateRevision: 1, + entityRefs: ["task-1"], scheduledWakeIds: ["wake-1"], + task: { id: "task-1", identifier: "CHAT-1", parentId: null, projectId: "project-1", status: "todo", assigneeActorId: "agent-1" }, + }; + expect(validate(receipt), JSON.stringify(validate.errors)).toBe(true); + const { projectId: _projectId, ...childTask } = receipt.task; + expect(validate({ ...receipt, task: { ...childTask, parentId: "parent-1" } })).toBe(true); + expect(validate({ ...receipt, task: { ...receipt.task, projectId: 42 } })).toBe(false); + expect(validate({ ...receipt, task: { ...receipt.task, parentId: "" } })).toBe(false); + }); + it("defines one immutable v1 declaration for each Codex-spine action", () => { const operationIds = PAPERCLIP_SEMANTIC_ACTION_CATALOG.map( (action) => action.operationId, ); - expect(operationIds).toHaveLength(29); + expect(operationIds).toHaveLength(32); expect(new Set(operationIds).size).toBe(operationIds.length); expect(operationIds).not.toContain("generic_api_request"); expect(Object.isFrozen(PAPERCLIP_SEMANTIC_ACTION_CATALOG)).toBe(true); diff --git a/packages/paperclip-runner/src/catalog/semantic-action-catalog.ts b/packages/paperclip-runner/src/catalog/semantic-action-catalog.ts index 3d4306b352..8b33ecf6ff 100644 --- a/packages/paperclip-runner/src/catalog/semantic-action-catalog.ts +++ b/packages/paperclip-runner/src/catalog/semantic-action-catalog.ts @@ -428,10 +428,41 @@ const descriptors: readonly PaperclipSemanticActionDescriptor[] = [ ), outputSchema: operationReceipt, }), + descriptor({ + operationId: "list_projects", + title: "List projects", + requiredClaims: ["discovery:projects:read"], + description: "Inspect available company projects before selecting a project for new work.", + placement: "optional", + inputSchema: object({}), + }), + descriptor({ + operationId: "list_project_repositories", + title: "List available repositories", + description: "List authorized repositories with stable IDs and names. Consider appropriate repositories before creating a project; never invent IDs.", + placement: "optional", + inputSchema: object({}), + }), + descriptor({ + operationId: "create_project", + title: "Create project", + description: "Create a project after considering existing projects and available repositories. repositoryIds and repositoryUrls accept multiple existing repositories. Use HTTPS GitHub repositoryUrls when an accessible repo is not in the catalog; this registers project repositories, not remote GitHub repositories. Non-code projects may omit repositories. Cannot combine repositoryIds/repositoryUrls with workspace. Reuse the idempotency key on retries.", + placement: "optional", effect: "write", allowedModes: STANDARD_MODE, + inputSchema: object({ + ...idempotency, name: text("Project name.", 500), description: nullableText("Project outcome and context."), + repositoryIds: stringArray("Authorized repository IDs from list_project_repositories; may contain multiple repositories."), + repositoryUrls: stringArray("Existing HTTPS GitHub repository URLs, including repos absent from the catalog."), + workspace: openObject, status: { enum: ["backlog", "planned", "in_progress", "completed", "cancelled"] }, + goalId: nullableText("Goal ID."), goalIds: stringArray("Goal IDs."), leadAgentId: nullableText("Lead agent ID."), + targetDate: nullableText("Target date."), color: nullableText("Project color."), icon: nullableText("Project icon."), + env: openObject, executionWorkspacePolicy: openObject, archivedAt: nullableText("Archive timestamp."), + }, ["idempotencyKey", "name"]), + outputSchema: openObject, + }), descriptor({ operationId: "create_task", - title: "Create child task", - description: "Create one child task under the active task.", + title: "Create task", + description: "Create an assigned task. In a conversation, create a project task with no parent; otherwise create a child of the active task. Include initialPlan to persist its plan before execution.", placement: "optional", effect: "write", requiredClaims: ["delegation:tasks:create"], @@ -439,7 +470,9 @@ const descriptors: readonly PaperclipSemanticActionDescriptor[] = [ inputSchema: object( { ...idempotency, - title: text("Child task title.", 500), + title: text("Task title.", 500), + projectId: nullableText("Project identifier for the new task."), + initialPlan: nullableText("Relevant markdown plan to persist on the new task before it starts."), description: nullableText("Child task description."), assigneeActorId: nullableText("Optional actor assignee.", 200), priority: { enum: ["critical", "high", "medium", "low"] }, diff --git a/packages/paperclip-runner/src/catalog/semantic-action-types.ts b/packages/paperclip-runner/src/catalog/semantic-action-types.ts index 0aa055bf54..360bbfd356 100644 --- a/packages/paperclip-runner/src/catalog/semantic-action-types.ts +++ b/packages/paperclip-runner/src/catalog/semantic-action-types.ts @@ -23,6 +23,9 @@ export type PaperclipSemanticActionId = | "get_workspace_runtime" | "control_workspace_service" | "set_dependencies" + | "create_project" + | "list_project_repositories" + | "list_projects" | "create_task" | "request_approval" | "decide_approval" diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts index fa7fdab8f2..e9ce49fbdf 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts @@ -13,6 +13,7 @@ import { describe, expect, it, vi } from "vitest"; import type { VerifiedAcpxCommandLease } from "./installation-integrity.js"; import { openCodexAcpxRuntime } from "./codex-runtime-adapter.js"; +import { createAcpxCommandLeaseOwner } from "./command-lease-owner.js"; import { resolveQualifiedAcpxProfile } from "./qualified-profiles.js"; import type { AcpxRuntimePortOpenOptions } from "./runtime-host.js"; @@ -1311,6 +1312,116 @@ describe("Codex ACPX runtime adapter", () => { }); }); + it("observes prompt admission rejection when the sidecar consumes only events and the result", async () => { + const runtime = fakeRuntime(); + const failure = new Error("Recovered provider could not start the prompt"); + vi.mocked(runtime.startTurn).mockImplementation(() => ({ + requestId: "turn-recovered-failure", + promptStarted: Promise.reject(failure), + events: { async *[Symbol.asyncIterator]() { throw failure; } }, + result: Promise.reject(failure), + cancel: vi.fn(), + closeStream: vi.fn(), + })); + const port = await openCodexAcpxRuntime(openOptions(fakeCommand()), { + createRegistry: () => registry(), createStore: () => store(), createRuntime: () => runtime, + }); + const turn = port.startTurn({ text: "Resume", requestId: "turn-recovered-failure" }); + const eventDrain = (async () => { for await (const _event of turn.events) { /* drain */ } })(); + await expect(eventDrain).rejects.toBe(failure); + // The sidecar does not await promptStarted. Leave it unconsumed across a + // full event-loop turn so an unobserved derived rejection fails this test. + await new Promise((resolve) => setImmediate(resolve)); + // Observing internally must not replace failure with successful admission. + await expect(turn.result).rejects.toBe(failure); + await expect(turn.promptStarted).rejects.toBe(failure); + await port.close({ reason: "test complete" }); + }); + + it("verifies a lazy recovered provider spawned by model selection before returning", async () => { + const runtime = fakeRuntime(); + const command = fakeCommand(); + vi.mocked(command.spawn).mockReturnValue(fakeChild()); + let runtimeOptions: AcpRuntimeOptions | undefined; + let acknowledgeOwnership!: () => void; + const ownership = new Promise((resolve) => { acknowledgeOwnership = resolve; }); + vi.mocked(runtime.setConfigOption!).mockImplementation(async () => { + await Promise.resolve(); + runtimeOptions?.spawnAgent?.({ command: "ignored", args: ["--stdio"], options: {} }); + }); + const port = await openCodexAcpxRuntime(openOptions(command), { + createRegistry: () => registry(), createStore: () => store(), + awaitProviderOwnership: () => ownership, + awaitProviderExit: providerOwnershipEstablished, + createRuntime: (options) => { runtimeOptions = options; return runtime; }, + }); + let admitted = false; + const selection = port.setModel!("gpt-5.6-sol").then(() => { admitted = true; }); + void selection.catch(() => undefined); + await vi.waitFor(() => expect(command.spawn).toHaveBeenCalledOnce()); + expect(admitted).toBe(false); + acknowledgeOwnership(); + await selection; + expect(admitted).toBe(true); + expect(() => runtimeOptions?.spawnAgent?.({ command: "ignored", args: [], options: {} })) + .toThrow("provider spawned after ownership admission was sealed"); + await port.close({ reason: "test complete" }); + }); + + it("uses a fresh single-use command after a cold model control consumes its launch", async () => { + const runtime = fakeRuntime(); + const freshCommand = () => { + const command = fakeCommand(); + vi.mocked(command.spawn).mockReturnValueOnce(fakeChild()).mockImplementation(() => { + throw new Error("Verified ACPX command lease is closed"); + }); + return command; + }; + const first = freshCommand(); + const second = freshCommand(); + const openCommand = vi.fn(async () => second); + const owner = createAcpxCommandLeaseOwner(first, openCommand); + let runtimeOptions: AcpRuntimeOptions; + vi.mocked(runtime.setConfigOption!).mockImplementation(async () => { + runtimeOptions.spawnAgent!({ command: "ignored", args: [], options: {} }); + }); + vi.mocked(runtime.startTurn).mockImplementation(() => { + runtimeOptions.spawnAgent!({ command: "ignored", args: [], options: {} }); + return { + requestId: "cold-turn", + promptStarted: Promise.resolve(), + events: { async *[Symbol.asyncIterator]() {} }, + result: Promise.resolve({ status: "completed" }), + cancel: vi.fn(), + closeStream: vi.fn(), + }; + }); + const port = await openCodexAcpxRuntime( + { + ...openOptions(owner.command), + refreshConsumedCommand: owner.refreshConsumedCommand, + }, + { + createRegistry: () => registry(), + createStore: () => store(), + awaitProviderOwnership: providerOwnershipEstablished, + awaitProviderExit: providerOwnershipEstablished, + createRuntime: (options) => { + runtimeOptions = options; + return runtime; + }, + }, + ); + await port.setModel!("gpt-5.6-sol"); + expect(openCommand).toHaveBeenCalledOnce(); + const turn = port.startTurn({ text: "Resume", requestId: "cold-turn" }); + await expect(turn.result).resolves.toMatchObject({ status: "completed" }); + expect(first.spawn).toHaveBeenCalledOnce(); + expect(second.spawn).toHaveBeenCalledOnce(); + await port.close({ reason: "test complete" }); + await owner.command.close(); + }); + it("admits a verified provider that starts with the first recovered turn", async () => { const runtime = fakeRuntime(); const child = fakeChild(); diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts index 88927177cb..808033fbf0 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts @@ -265,6 +265,7 @@ export async function openQualifiedAcpxRuntime( update.goal === null ? null : structuredClone(update.goal), ); }; + const commandLaunches = { count: 0, refreshConsumedCommand: options.refreshConsumedCommand }; const runtimeOptions: GoalAwareAcpRuntimeOptions = { cwd: options.cwd, sessionStore, @@ -327,6 +328,7 @@ export async function openQualifiedAcpxRuntime( // handshake cannot create a provider process after authority is gone. options.signal?.throwIfAborted(); options.assertWorkspaceHeld?.(); + commandLaunches.count += 1; return children.add( options.command.spawn(input.args, input.options, { credentialFenceFds, @@ -431,6 +433,7 @@ export async function openQualifiedAcpxRuntime( children, runtimeCloseTimeoutMs, goalState, + commandLaunches, ); } catch (error) { const cleanupReason = "ACPX runtime identity validation failed"; @@ -857,6 +860,7 @@ function runtimePort( children: SpawnedChildSet, runtimeCloseTimeoutMs: number, goalState: AcpxRuntimeGoalState, + commandLaunches: { count: number; refreshConsumedCommand?: () => Promise }, ): AcpxRuntimePort { type RuntimeCloseAttempt = { readonly outcome: Promise; @@ -1156,11 +1160,26 @@ function runtimePort( ...(runtime.setConfigOption ? { async setModel(model: string) { - await runtime.setConfigOption?.({ - handle, - key: "model", - value: model, - }); + // A restored handle can be lazy: selecting the pinned model may + // launch its first provider before any prompt. Admit that spawn + // only for this control call, and verify ownership before return. + const finishOwnershipAdmission = + children.beginLifetimeOwnershipAdmission(); + const spawnsBeforeControl = commandLaunches.count; + try { + await runtime.setConfigOption?.({ + handle, + key: "model", + value: model, + }); + } finally { + await finishOwnershipAdmission(); + } + // Cold ACP config calls open and close a temporary connection. + // A later prompt needs a newly verified single-use launch snapshot. + if (commandLaunches.count > spawnsBeforeControl) { + await commandLaunches.refreshConsumedCommand?.(); + } }, } : {}), @@ -1212,11 +1231,20 @@ function turnWithVerifiedLifetimeOwnership( finishOwnershipAdmission(), ); void ownershipVerified.catch(() => undefined); + const promptStarted = ownershipVerified.then(() => turn.promptStarted); + const result = ownershipVerified.then(() => turn.result); + // Some consumers (including the sidecar) drain events and await the result + // without awaiting this optional admission signal. Observe its rejection + // immediately so a failed cold start cannot terminate the host process as an + // unhandled rejection. Keep the original rejected promise for consumers. + void promptStarted.catch(() => undefined); + // Event drains can fail before their caller reaches the result promise. + void result.catch(() => undefined); return { requestId: turn.requestId, - promptStarted: ownershipVerified.then(() => turn.promptStarted), + promptStarted, events: eventsAfterLifetimeOwnership(turn.events, ownershipVerified), - result: ownershipVerified.then(() => turn.result), + result, cancel: (input) => turn.cancel(input), closeStream: (input) => turn.closeStream(input), }; diff --git a/packages/paperclip-runner/src/drivers/acpx/command-lease-owner.test.ts b/packages/paperclip-runner/src/drivers/acpx/command-lease-owner.test.ts new file mode 100644 index 0000000000..1fd684fe19 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/command-lease-owner.test.ts @@ -0,0 +1,79 @@ +import type { ChildProcess } from "node:child_process"; +import { describe, expect, it, vi } from "vitest"; +import { createAcpxCommandLeaseOwner } from "./command-lease-owner.js"; +import type { VerifiedAcpxCommandLease } from "./installation-integrity.js"; + +function lease() { + let consumed = false; + return { + spawn: vi.fn(() => { + if (consumed) throw new Error("single-use command already consumed"); + consumed = true; + return {} as ChildProcess; + }), + close: vi.fn(async () => { + consumed = true; + }), + } satisfies VerifiedAcpxCommandLease; +} + +describe("ACPX verified command lease owner", () => { + it("refreshes only consumed snapshots and preserves single-use spawn enforcement", async () => { + const first = lease(); + const second = lease(); + const open = vi.fn(async () => second); + const owner = createAcpxCommandLeaseOwner(first, open); + await owner.refreshConsumedCommand(); + expect(open).not.toHaveBeenCalled(); + owner.command.spawn(); + expect(() => owner.command.spawn()).toThrow("already consumed"); + await Promise.all([owner.refreshConsumedCommand(), owner.refreshConsumedCommand()]); + expect(open).toHaveBeenCalledOnce(); + owner.command.spawn(); + expect(second.spawn).toHaveBeenCalledOnce(); + expect(() => owner.command.spawn()).toThrow("already consumed"); + await owner.command.close(); + expect(first.close).toHaveBeenCalledOnce(); + expect(second.close).toHaveBeenCalledOnce(); + expect(() => owner.command.spawn()).toThrow("closing"); + await expect(owner.refreshConsumedCommand()).rejects.toThrow("closing"); + }); + + it("retains a replacement acquired during shutdown and retries its failed cleanup", async () => { + const first = lease(); + const replacement = lease(); + replacement.close.mockRejectedValueOnce(new Error("close failed")); + let acquired!: (value: VerifiedAcpxCommandLease) => void; + const owner = createAcpxCommandLeaseOwner( + first, + () => new Promise((resolve) => { + acquired = resolve; + }), + ); + owner.command.spawn(); + const refresh = owner.refreshConsumedCommand(); + const rejectedRefresh = expect(refresh).rejects.toThrow("closed during refresh"); + await Promise.resolve(); + const close = owner.command.close(); + const rejectedClose = expect(close).rejects.toThrow("leases did not close"); + acquired(replacement); + await rejectedRefresh; + await rejectedClose; + expect(replacement.spawn).not.toHaveBeenCalled(); + await owner.command.close(); + expect(replacement.close).toHaveBeenCalledTimes(2); + expect(first.close).toHaveBeenCalledOnce(); + }); + + it("fails closed when fresh command verification fails", async () => { + const initial = lease(); + const owner = createAcpxCommandLeaseOwner(initial, async () => { + throw new Error("installation changed"); + }); + owner.command.spawn(); + await expect(owner.refreshConsumedCommand()).rejects.toThrow("installation changed"); + expect(() => owner.command.spawn()).toThrow("already consumed"); + await owner.command.close(); + expect(initial.close).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/paperclip-runner/src/drivers/acpx/command-lease-owner.ts b/packages/paperclip-runner/src/drivers/acpx/command-lease-owner.ts new file mode 100644 index 0000000000..38896668ca --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/command-lease-owner.ts @@ -0,0 +1,58 @@ +import type { VerifiedAcpxCommandLease } from "./installation-integrity.js"; + +/** Keep each launch single-use while owning replacements for transient ACP controls. */ +export function createAcpxCommandLeaseOwner( + initial: VerifiedAcpxCommandLease, + openCommand: () => Promise, +) { + const leases = new Set([initial]); + let current = initial; + let consumed = false; + let closing = false; + let refresh: Promise | null = null; + const command: VerifiedAcpxCommandLease = { + spawn(...args) { + if (closing) throw new Error("Verified ACPX command owner is closing"); + consumed = true; + return current.spawn(...args); + }, + async close() { + closing = true; + // Late acquisitions remain owned. Retry every lease whose close fails. + await refresh?.catch(() => undefined); + const failures: unknown[] = []; + for (const lease of leases) { + try { + await lease.close(); + leases.delete(lease); + } catch (error) { + failures.push(error); + } + } + if (failures.length) throw new AggregateError(failures, "ACPX command leases did not close"); + }, + }; + return { + command, + async refreshConsumedCommand(): Promise { + if (closing) throw new Error("Verified ACPX command owner is closing"); + if (!consumed) return; + if (!refresh) { + refresh = Promise.resolve() + .then(openCommand) + .then((replacement) => { + leases.add(replacement); + if (closing) throw new Error("Verified ACPX command owner closed during refresh"); + current = replacement; + consumed = false; + }); + } + const pending = refresh; + try { + await pending; + } finally { + if (refresh === pending) refresh = null; + } + }, + }; +} diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts index ebc3cb482c..996e1d4761 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts @@ -20,6 +20,7 @@ import { type VerifiedAcpxCommandLease, type VerifiedAcpxInstallation, } from "./installation-integrity.js"; +import { createAcpxCommandLeaseOwner } from "./command-lease-owner.js"; import { requireVerifiedAcpxModel, type AcpxModelStatus, @@ -117,6 +118,8 @@ export interface AcpxRuntimePort { export interface AcpxRuntimePortOpenOptions { command: VerifiedAcpxCommandLease; + /** Replace a consumed launch snapshot after an ephemeral control session. */ + refreshConsumedCommand?: () => Promise; profile: QualifiedAcpxProfile; cwd: string; stateDirectory: string; @@ -401,6 +404,11 @@ export class AcpxRuntimeHost { reportFailure: (failure) => dependencies.reportRetainedCleanupFailure(failure), }); + const commandOwner = createAcpxCommandLeaseOwner( + command, + () => installation.openCommand(), + ); + command = commandOwner.command; toolBridge = options.semanticTools ? await acquireAbortableAdmissionResource({ signal: options.signal, @@ -421,6 +429,7 @@ export class AcpxRuntimeHost { options.assertWorkspaceHeld?.(); return dependencies.openRuntime({ command: command!, + refreshConsumedCommand: commandOwner.refreshConsumedCommand, profile, cwd: binding.workspacePath, stateDirectory: sandbox.stateDirectory, diff --git a/packages/paperclip-runner/src/generated/capability-contract.ts b/packages/paperclip-runner/src/generated/capability-contract.ts index ba129500d2..d554374a11 100644 --- a/packages/paperclip-runner/src/generated/capability-contract.ts +++ b/packages/paperclip-runner/src/generated/capability-contract.ts @@ -6,9 +6,9 @@ export type CapabilityPrimaryDisposition = | "optional_agent_tool"; export const capabilityInventoryCounts = { - "skillReferenceCapabilities": 153, + "skillReferenceCapabilities": 154, "evalCases": 106, - "normativeRows": 259, + "normativeRows": 260, "legacyMcpAliases": 42 } as const; diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts index ba6ac7897e..9d7e700559 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts @@ -4032,8 +4032,18 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { // A failed drain still proceeds through bounded suspension/containment, // but can never authorize a reusable checkpoint or deletion of evidence. } + const lastDrain = [...core.store.state.commands] + .reverse() + .find((command) => command.type === "runner.drain"); this.#diagnostic( - "provider suffix did not prove durable drain before bounded runner suspension", + "provider suffix did not prove durable drain before bounded runner suspension: " + + JSON.stringify({ + providerState: this.#providerDrainState(), + semanticResultsSettled: core.semanticToolResultsSettled(), + drainStatus: lastDrain?.status ?? null, + retainedEventsDrained: + record(record(lastDrain?.result).result).retainedEventsDrained ?? null, + }), ); return false; } diff --git a/packages/paperclip-runner/src/protocol-actions/create-project.ts b/packages/paperclip-runner/src/protocol-actions/create-project.ts new file mode 100644 index 0000000000..53f0e11343 --- /dev/null +++ b/packages/paperclip-runner/src/protocol-actions/create-project.ts @@ -0,0 +1,198 @@ +/** Canonical project tool definition. */ +export const createProjectAction = { + "id": "create_project", + "canonical": { + "operationId": "create_project", + "surfaces": [ + "live" + ], + "placement": "optional_agent_tool", + "optionalGroup": "discovery", + "requiredClaims": [], + "taskModes": [ + "standard", + "skill_test" + ], + "sideEffectClass": "company_write", + "idempotency": "required", + "disabledByDefault": false, + "realBindingStatus": "live_codex", + "realServiceBinding": "PaperclipRunnerToolAuthority", + "prpEvidence": "Authenticated project tools, persisted projects and repository workspaces, and run-bound activity.", + "prpBindingStatus": "bound", + "legacyAliases": [] + }, + "documentation": { + "title": "Create project", + "description": "Create a project after considering existing projects and available repositories. repositoryIds and repositoryUrls accept multiple existing repositories. Use HTTPS GitHub repositoryUrls when an accessible repo is not in the catalog; this registers project repositories, not remote GitHub repositories. Non-code projects may omit repositories. Cannot combine repositoryIds/repositoryUrls with workspace. Reuse the idempotency key on retries.", + "note": null + }, + "examples": { + "call": { + "operationId": "create_project", + "input": { + "name": "Example", + "repositoryIds": [ + "1", + "2" + ], + "idempotencyKey": "example" + } + }, + "success": { + "ok": true, + "operationId": "create_project", + "result": {} + } + }, + "live": { + "order": 43, + "descriptor": { + "schema": "paperclip.semantic-tool.v1", + "operationId": "create_project", + "version": 1, + "title": "Create project", + "description": "Create a project after considering existing projects and available repositories. repositoryIds and repositoryUrls accept multiple existing repositories. Use HTTPS GitHub repositoryUrls when an accessible repo is not in the catalog; this registers project repositories, not remote GitHub repositories. Non-code projects may omit repositories. Cannot combine repositoryIds/repositoryUrls with workspace. Reuse the idempotency key on retries.", + "effect": "write", + "requiredClaims": [], + "allowedModes": [ + "standard", + "skill_test" + ], + "inputSchema": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "description": "Caller-stable retry key.", + "minLength": 1, + "maxLength": 240 + }, + "name": { + "type": "string", + "description": "Project name.", + "minLength": 1, + "maxLength": 500 + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Project outcome and context.", + "maxLength": 20000 + }, + "repositoryIds": { + "type": "array", + "description": "Authorized repository IDs from list_project_repositories; may contain multiple repositories.", + "items": { + "type": "string", + "minLength": 1 + }, + "maxItems": 200, + "uniqueItems": true + }, + "workspace": { + "type": "object", + "additionalProperties": true + }, + "status": { + "enum": [ + "backlog", + "planned", + "in_progress", + "completed", + "cancelled" + ] + }, + "goalId": { + "type": [ + "string", + "null" + ], + "description": "Goal ID.", + "maxLength": 20000 + }, + "goalIds": { + "type": "array", + "description": "Goal IDs.", + "items": { + "type": "string", + "minLength": 1 + }, + "maxItems": 200, + "uniqueItems": true + }, + "leadAgentId": { + "type": [ + "string", + "null" + ], + "description": "Lead agent ID.", + "maxLength": 20000 + }, + "targetDate": { + "type": [ + "string", + "null" + ], + "description": "Target date.", + "maxLength": 20000 + }, + "color": { + "type": [ + "string", + "null" + ], + "description": "Project color.", + "maxLength": 20000 + }, + "icon": { + "type": [ + "string", + "null" + ], + "description": "Project icon.", + "maxLength": 20000 + }, + "env": { + "type": "object", + "additionalProperties": true + }, + "executionWorkspacePolicy": { + "type": "object", + "additionalProperties": true + }, + "archivedAt": { + "type": [ + "string", + "null" + ], + "description": "Archive timestamp.", + "maxLength": 20000 + }, + "repositoryUrls": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "maxItems": 100, + "description": "Existing HTTPS GitHub repository URLs, including repos absent from the catalog." + } + }, + "required": [ + "idempotencyKey", + "name" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "additionalProperties": true + }, + "exposure": "optional" + } + }, + "scenario": null +} as const; diff --git a/packages/paperclip-runner/src/protocol-actions/create-task.ts b/packages/paperclip-runner/src/protocol-actions/create-task.ts index 1c63348faf..b7110b69b1 100644 --- a/packages/paperclip-runner/src/protocol-actions/create-task.ts +++ b/packages/paperclip-runner/src/protocol-actions/create-task.ts @@ -20,7 +20,7 @@ export const createTaskAction = { "idempotency": "required", "disabledByDefault": false, "realBindingStatus": "live_codex", - "realServiceBinding": "issues.createChild", + "realServiceBinding": "issues.create / issues.createChild", "prpEvidence": "semantic-operation item event plus company-entity state diff and audit record", "prpBindingStatus": "bound", "legacyAliases": [ @@ -28,8 +28,8 @@ export const createTaskAction = { ] }, "documentation": { - "title": "Create child task", - "description": "Create one durable standard child under the active task.", + "title": "Create task", + "description": "Create a project task from a conversation, or a child from an ordinary task. Persist initialPlan before execution.", "note": null }, "examples": { @@ -76,8 +76,8 @@ export const createTaskAction = { "schema": "paperclip.semantic-tool.v1", "operationId": "create_task", "version": 1, - "title": "Create child task", - "description": "Create one durable standard child under the active task. Use only when a real ownership, parallelism, dependency, review, or lifecycle boundary justifies delegation.", + "title": "Create task", + "description": "Create a project task from a conversation, or a child from an ordinary task. Persist initialPlan before execution.", "exposure": "optional", "requiredClaims": [ "delegation:tasks:create" @@ -134,6 +134,21 @@ export const createTaskAction = { }, "maxItems": 200, "uniqueItems": true + }, + "projectId": { + "type": [ + "string", + "null" + ], + "description": "Project ID for the task." + }, + "initialPlan": { + "type": [ + "string", + "null" + ], + "maxLength": 200000, + "description": "Relevant markdown plan saved on the new task before execution starts." } }, "required": [ @@ -184,13 +199,42 @@ export const createTaskAction = { "task": { "type": "object", "properties": { - "id": { "type": "string", "minLength": 1 }, - "identifier": { "type": ["string", "null"] }, - "parentId": { "type": "string", "minLength": 1 }, - "status": { "type": "string", "minLength": 1 }, - "assigneeActorId": { "type": ["string", "null"] } + "id": { + "type": "string", + "minLength": 1 + }, + "identifier": { + "type": [ + "string", + "null" + ] + }, + "parentId": { + "type": ["string", "null"], + "minLength": 1 + }, + "projectId": { + "type": ["string", "null"], + "minLength": 1 + }, + "status": { + "type": "string", + "minLength": 1 + }, + "assigneeActorId": { + "type": [ + "string", + "null" + ] + } }, - "required": ["id", "identifier", "parentId", "status", "assigneeActorId"], + "required": [ + "id", + "identifier", + "parentId", + "status", + "assigneeActorId" + ], "additionalProperties": false } }, diff --git a/packages/paperclip-runner/src/protocol-actions/index.ts b/packages/paperclip-runner/src/protocol-actions/index.ts index 7350882445..5b2bb29eee 100644 --- a/packages/paperclip-runner/src/protocol-actions/index.ts +++ b/packages/paperclip-runner/src/protocol-actions/index.ts @@ -1,3 +1,5 @@ +import { createProjectAction } from "./create-project.js"; +import { listProjectRepositoriesAction } from "./list-project-repositories.js"; import { searchApiAction } from "./search-api.js"; import { callApiAction } from "./call-api.js"; import { administerCompanyAction } from "./administer-company.js"; @@ -44,6 +46,8 @@ import { writeDocumentAction } from "./write-document.js"; import { deepFreezeProtocolAction } from "./freeze.js"; export const PAPERCLIP_PROTOCOL_ACTIONS = deepFreezeProtocolAction([ + createProjectAction, + listProjectRepositoriesAction, searchApiAction, callApiAction, administerCompanyAction, diff --git a/packages/paperclip-runner/src/protocol-actions/list-project-repositories.ts b/packages/paperclip-runner/src/protocol-actions/list-project-repositories.ts new file mode 100644 index 0000000000..9730da5341 --- /dev/null +++ b/packages/paperclip-runner/src/protocol-actions/list-project-repositories.ts @@ -0,0 +1,73 @@ +/** Canonical project tool definition. */ +export const listProjectRepositoriesAction = { + "id": "list_project_repositories", + "canonical": { + "operationId": "list_project_repositories", + "surfaces": [ + "live" + ], + "placement": "optional_agent_tool", + "optionalGroup": "discovery", + "requiredClaims": [], + "taskModes": [ + "standard", + "ask", + "planning", + "skill_test" + ], + "sideEffectClass": "read", + "idempotency": "none", + "disabledByDefault": false, + "realBindingStatus": "live_codex", + "realServiceBinding": "PaperclipRunnerToolAuthority", + "prpEvidence": "Authenticated project tools, persisted projects and repository workspaces, and run-bound activity.", + "prpBindingStatus": "bound", + "legacyAliases": [] + }, + "documentation": { + "title": "List available repositories", + "description": "List authorized repositories with stable IDs and names. Consider appropriate repositories before creating a project; never invent IDs.", + "note": null + }, + "examples": { + "call": { + "operationId": "list_project_repositories", + "input": {} + }, + "success": { + "ok": true, + "operationId": "list_project_repositories", + "result": {} + } + }, + "live": { + "order": 44, + "descriptor": { + "schema": "paperclip.semantic-tool.v1", + "operationId": "list_project_repositories", + "version": 1, + "title": "List available repositories", + "description": "List authorized repositories with stable IDs and names. Consider appropriate repositories before creating a project; never invent IDs.", + "effect": "read", + "requiredClaims": [], + "allowedModes": [ + "standard", + "ask", + "planning", + "skill_test" + ], + "inputSchema": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "additionalProperties": true + }, + "exposure": "optional" + } + }, + "scenario": null +} as const; diff --git a/packages/paperclip-runner/src/protocol-actions/list-projects.ts b/packages/paperclip-runner/src/protocol-actions/list-projects.ts index ff4b117dce..6d79fc088a 100644 --- a/packages/paperclip-runner/src/protocol-actions/list-projects.ts +++ b/packages/paperclip-runner/src/protocol-actions/list-projects.ts @@ -1,10 +1,11 @@ -/** Canonical definition and documentation for `list_projects`. */ +/** Canonical project discovery definition. */ export const listProjectsAction = { "id": "list_projects", "canonical": { "operationId": "list_projects", "surfaces": [ - "scenario" + "scenario", + "live" ], "placement": "optional_agent_tool", "optionalGroup": "discovery", @@ -13,22 +14,23 @@ export const listProjectsAction = { ], "taskModes": [ "standard", + "ask", + "planning", "skill_test" ], "sideEffectClass": "read", "idempotency": "none", "disabledByDefault": false, - "realBindingStatus": "scenario_mock", - "realServiceBinding": "unbound", - "prpEvidence": "read projection surfaced via a tool-result item event; no control-plane state diff", - "prpBindingStatus": "audit_pending", - "legacyAliases": [], - "note": "Scenario/eval-only discovery via mock extension; no live dispatcher binding yet." + "realBindingStatus": "live_codex", + "realServiceBinding": "PaperclipRunnerToolAuthority", + "prpEvidence": "Authenticated project tools, persisted projects and repository workspaces, and run-bound activity.", + "prpBindingStatus": "bound", + "legacyAliases": [] }, "documentation": { - "title": "List Projects", - "description": "List Projects through the Capability discovery capability set.", - "note": "Scenario/eval-only discovery via mock extension; no live dispatcher binding yet." + "title": "List projects", + "description": "Inspect available company projects before selecting a project for new work.", + "note": null }, "examples": { "call": { @@ -38,18 +40,40 @@ export const listProjectsAction = { "success": { "ok": true, "operationId": "list_projects", - "result": { - "schema": "paperclip.capability.tool-result.v1", - "ok": true, - "operationId": "list_projects", - "operationResultId": "example", - "value": "example", - "commandResult": "example", - "authorization": "example" - } + "result": {} + } + }, + "live": { + "order": 45, + "descriptor": { + "schema": "paperclip.semantic-tool.v1", + "operationId": "list_projects", + "version": 1, + "title": "List projects", + "description": "Inspect available company projects before selecting a project for new work.", + "effect": "read", + "requiredClaims": [ + "discovery:projects:read" + ], + "allowedModes": [ + "standard", + "ask", + "planning", + "skill_test" + ], + "inputSchema": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "additionalProperties": true + }, + "exposure": "optional" } }, - "live": null, "scenario": { "order": 16, "descriptor": { @@ -104,6 +128,8 @@ export const listProjectsAction = { ], "taskModes": [ "standard", + "ask", + "planning", "skill_test" ], "sideEffectClass": "read", diff --git a/packages/paperclip-runner/src/semantic-tools/discovery.ts b/packages/paperclip-runner/src/semantic-tools/discovery.ts index 3b498df8e8..52ac9a809b 100644 --- a/packages/paperclip-runner/src/semantic-tools/discovery.ts +++ b/packages/paperclip-runner/src/semantic-tools/discovery.ts @@ -27,7 +27,7 @@ const NAMESPACE: Readonly> = Objec report_progress: "active_task", answer_status_question: "active_task", write_document: "documents", request_human_input: "documents", register_deliverable: "documents", finish_task: "active_task", block_task: "active_task", request_review: "active_task", search_tasks: "discovery", - list_agents: "discovery", get_agent: "discovery", create_task: "delegation", + list_agents: "discovery", get_agent: "discovery", create_task: "delegation", create_project: "projects", list_project_repositories: "projects", list_projects: "projects", set_dependencies: "delegation", list_approvals: "governance", get_approval: "governance", get_approval_context: "governance", request_approval: "governance", decide_approval: "governance", comment_on_approval: "governance", diff --git a/packages/paperclip-runner/src/semantic-tools/paperclip-discovery.ts b/packages/paperclip-runner/src/semantic-tools/paperclip-discovery.ts index 65aff89cf9..8ec7255b0d 100644 --- a/packages/paperclip-runner/src/semantic-tools/paperclip-discovery.ts +++ b/packages/paperclip-runner/src/semantic-tools/paperclip-discovery.ts @@ -36,7 +36,7 @@ const NAMESPACE: Readonly> = get_workspace_runtime: "workspace", control_workspace_service: "workspace", set_dependencies: "delegation", - create_task: "delegation", + create_task: "delegation", create_project: "projects", list_project_repositories: "projects", list_projects: "projects", request_approval: "governance", decide_approval: "governance", comment_on_approval: "governance", diff --git a/packages/paperclip-runner/src/semantic-tools/types.ts b/packages/paperclip-runner/src/semantic-tools/types.ts index fe5f8e76d8..535a985489 100644 --- a/packages/paperclip-runner/src/semantic-tools/types.ts +++ b/packages/paperclip-runner/src/semantic-tools/types.ts @@ -40,6 +40,9 @@ export type CapabilitySemanticOperationId = | "get_workspace_runtime" | "control_workspace_service" | "set_dependencies" + | "create_project" + | "list_project_repositories" + | "list_projects" | "create_task" | "request_approval" | "decide_approval" diff --git a/packages/paperclip-runner/test/capability-contract.test.mjs b/packages/paperclip-runner/test/capability-contract.test.mjs index b9cd7f3305..cbb8c78ed3 100644 --- a/packages/paperclip-runner/test/capability-contract.test.mjs +++ b/packages/paperclip-runner/test/capability-contract.test.mjs @@ -17,7 +17,7 @@ test("generated Capability inventory has full source coverage", async () => { readRows("eval-traceability.yaml"), ]); - assert.equal(capabilities.length, 152); + assert.equal(capabilities.length, 155); assert.equal(tools.length, 42); assert.equal(evals.length, 106); assert.equal(new Set(evals.map((row) => row.group)).size, 16); diff --git a/skills/paperclip/SKILL.md b/skills/paperclip/SKILL.md index ac5e26a1cb..21b6a7abca 100644 --- a/skills/paperclip/SKILL.md +++ b/skills/paperclip/SKILL.md @@ -27,6 +27,23 @@ Manual local CLI mode (outside heartbeat runs): use `paperclipai agent local-cli **Run audit trail:** You MUST include `-H 'X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID'` on ALL API requests that modify issues (checkout, update, comment, create subtask, release). This links your actions to the current heartbeat run for traceability. +## Conversation tasks + +When the task context says **Chat mode** (the issue has `conversationAgentId`), +follow that directive for the conversation lifecycle. Research, clarify, and +revise the conversation's `plan` document here. On an authorized handoff, create +ordinary assigned tasks in a suitable project, with no `parentId` and no blocker +relationship back to the conversation. Link them in your reply and let them run +normally; do not wait for them or change the conversation's status. + +Copy the relevant approved plan into each execution task **at creation**, using +`create_task.initialPlan` or the HTTP issue-creation body's `initialPlan` field. +Include an `idempotencyKey`. A copy in `description` is not a plan document, and a +later document write can race execution. Verify the created task's `plan` +document before claiming handoff. Preserve the source plan in this conversation. +The ordinary completion, child-task, and blocker instructions below apply to +execution tasks; they do not override chat mode. + ## Server-Verified External Chat Turns Paperclip may identify an ordinary external-chat turn as already checked out and @@ -208,7 +225,7 @@ Because of that, follow these rules: - **Never imply a live watcher on a task you are marking `done`.** `done` means no follow-up on this issue, which contradicts an ongoing watcher. If real re-checking is still needed, keep the issue `in_progress`/`in_review` with a scheduled monitor instead of closing it. - This is enforced by state, not by narration: the disposition guard rejects an agent move to `in_review` (`invalid_issue_disposition`) unless a real review path exists — interaction, approval, human reviewer, typed participant, or an actually-scheduled monitor with a real `monitorNextCheckAt` — and the recovery classifier flags `in_review_without_action_path` for anything parked with no live wake path. Keep your comments consistent with that real state. -**Step 9 — Delegate if needed.** Create subtasks with `POST /api/companies/{companyId}/issues`. Always set `parentId` and `goalId`. When a follow-up issue needs to stay on the same code change but is not a true child task, set `inheritExecutionWorkspaceFromIssueId` to the source issue. Set `billingCode` for cross-team work. +**Step 9 — Delegate if needed.** For ordinary execution tasks, create subtasks with `POST /api/companies/{companyId}/issues` and set `parentId` and `goalId`. For conversation tasks, use the project handoff above instead. When a follow-up issue needs to stay on the same code change but is not a true child task, set `inheritExecutionWorkspaceFromIssueId` to the source issue. Set `billingCode` for cross-team work. ### Delegating review tasks @@ -624,7 +641,7 @@ PUT /api/issues/{issueId}/documents/plan } ``` -If `plan` already exists, fetch the current document first and send its latest `baseRevisionId` when you update it. +If `plan` already exists, first `GET /api/issues/{issueId}/documents/plan` and read its current body and `latestRevisionId`. Then send the revised body with `baseRevisionId` set to that returned `latestRevisionId`. The GET field is `latestRevisionId`; the PUT field is `baseRevisionId`. Omitting it on an update returns `409`. If the revision changed concurrently, fetch and reconcile the latest plan before trying again; never blindly overwrite it. ## Key Endpoints (Hot Routes) diff --git a/skills/paperclip/references/api-reference.md b/skills/paperclip/references/api-reference.md index 7c618cc2c1..e7a9fc5371 100644 --- a/skills/paperclip/references/api-reference.md +++ b/skills/paperclip/references/api-reference.md @@ -784,6 +784,26 @@ PATCH /api/agents/{agentId}/instructions-path When a CEO/manager task asks you to "set up a new project" and wire local + GitHub context, use this sequence. +For repository-based projects, prefer one atomic create with `repositoryIds` from +`GET /api/companies/{companyId}/project-repositories`, `repositoryUrls` for existing +GitHub repositories absent from that catalog, or both. These arrays support +multiple repositories. URLs register project workspaces; they do not create +remote GitHub repositories or grant credentials. Use HTTPS URLs without credentials. +Do not combine either array with an explicit `workspace`. Reuse the same +`idempotencyKey` and body when retrying a creation. + +``` +POST /api/companies/{companyId}/projects +{ + "name": "Web and API", + "repositoryUrls": ["https://github.com/acme/web", "https://github.com/acme/api"], + "idempotencyKey": "web-api-project" +} +``` + +Omit repository inputs for non-code work. The explicit workspace alternatives +below remain available when local workspace configuration is needed. + ### Option A: One-call create with workspace ``` @@ -1291,7 +1311,7 @@ Terminal states: `done`, `cancelled` | POST | `/api/companies/:companyId/archive` | Archive company | | GET | `/api/companies/:companyId/projects` | List projects | | GET | `/api/projects/:projectId` | Project details | -| POST | `/api/companies/:companyId/projects` | Create project (optional inline `workspace`) | +| POST | `/api/companies/:companyId/projects` | Create project (`repositoryIds`/`repositoryUrls` arrays or inline `workspace`; optional `idempotencyKey`) | | PATCH | `/api/projects/:projectId` | Update project | | GET | `/api/projects/:projectId/workspaces` | List project workspaces | | POST | `/api/projects/:projectId/workspaces` | Create project workspace | diff --git a/tests/runner-e2e/workflow-security.test.ts b/tests/runner-e2e/workflow-security.test.ts index 4dce7c374a..6f58700018 100644 --- a/tests/runner-e2e/workflow-security.test.ts +++ b/tests/runner-e2e/workflow-security.test.ts @@ -46,7 +46,9 @@ describe("public repository paid workflow security", () => { .split(/\n(?= {6}- )/u) .filter((step) => step.includes("uses: pnpm/action-setup@")); - expect(pnpmSetupSteps, workflowName).toHaveLength(7); + expect(pnpmSetupSteps, workflowName).toHaveLength( + workflowName === "pr-trusted.yml" ? 8 : 7, + ); for (const step of pnpmSetupSteps) { expect(step, workflowName).toContain('NPM_CONFIG_AUDIT: "false"'); expect(step, workflowName).toContain('NPM_CONFIG_FUND: "false"'); @@ -75,7 +77,7 @@ describe("public repository paid workflow security", () => { }, { name: "pr-trusted.yml", - expectedCachedSetupNodeSteps: 7, + expectedCachedSetupNodeSteps: 8, }, ]; @@ -89,7 +91,9 @@ describe("public repository paid workflow security", () => { step.includes("uses: pnpm/action-setup@") ? [index] : [], ); - expect(pnpmSetupStepIndexes, name).toHaveLength(7); + expect(pnpmSetupStepIndexes, name).toHaveLength( + name === "pr-trusted.yml" ? 8 : 7, + ); for (const pnpmSetupStepIndex of pnpmSetupStepIndexes) { const pnpmSetupStep = steps[pnpmSetupStepIndex]!; const nodeBootstrapStep = steps[pnpmSetupStepIndex - 1]!;