feat(runner): support project handoff tools and durable chat turns

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-11 17:00:16 -05:00
parent 2083bf6f9a
commit 63a69d0414
42 changed files with 2068 additions and 400 deletions

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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.

File diff suppressed because one or more lines are too long

View File

@ -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
},
{

View File

@ -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 => {

View File

@ -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<Vec<PolledEvent>, 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<_>>(),
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");

View File

@ -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<AcpxSidecarEvent>,
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::<Vec<_>>()
.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",

View File

@ -2670,6 +2670,9 @@ mod tests {
}
fn read_http_request(socket: &mut TcpStream) -> Result<CapturedRequest, std::io::Error> {
// 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());

View File

@ -1597,6 +1597,28 @@ pub(crate) fn sanitize_semantic_tool_input(
input: &Value,
) -> Result<Value, DurableRunnerError> {
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.";

View File

@ -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"));
}
}

View File

@ -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));
}
}

View File

@ -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(&registry)
.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");

View File

@ -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,

View File

@ -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();

View File

@ -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"
]
}
]

View File

@ -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",

View File

@ -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); }

View File

@ -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");

View File

@ -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);

View File

@ -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"] },

View File

@ -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"

View File

@ -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<void>((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<void>((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();

View File

@ -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<void> },
): AcpxRuntimePort {
type RuntimeCloseAttempt = {
readonly outcome: Promise<unknown | null>;
@ -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),
};

View File

@ -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();
});
});

View File

@ -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<VerifiedAcpxCommandLease>,
) {
const leases = new Set([initial]);
let current = initial;
let consumed = false;
let closing = false;
let refresh: Promise<void> | 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<void> {
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;
}
},
};
}

View File

@ -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<void>;
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,

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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
}
},

View File

@ -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,

View File

@ -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;

View File

@ -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",

View File

@ -27,7 +27,7 @@ const NAMESPACE: Readonly<Record<CapabilitySemanticOperationId, string>> = 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",

View File

@ -36,7 +36,7 @@ const NAMESPACE: Readonly<Record<PaperclipSemanticActionId, string>> =
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",

View File

@ -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"

View File

@ -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);

View File

@ -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)

View File

@ -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 |

View File

@ -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]!;